// Bootstrap unit tests. Injects spawn + resolve + fetch stubs so we exercise // every branch (no_devices, no_paired_device, device_locked, healthz timeout, // rotate_failed, success) without needing a real iPhone connected. import { describe, test, expect } from 'bun:test'; import { bootstrapTunnel } from '../src/tunnel-bootstrap'; import { getDeviceTunnelIPv6FromDevicectl, resolveTunnelIPv6, startTunnelKeepalive, type SpawnImpl, } from '../src/devicectl'; import { writeFileSync } from 'fs'; interface ScriptedCall { argsMatch: RegExp; stdout?: string; stderr?: string; exitCode?: number; /** If set, write this content to the JSON output path before returning. */ jsonOutput?: object; /** If set, write this content to the file matching `--destination`. */ destOutput?: string; } /** * Build a spawnImpl that walks through a scripted sequence of expected calls. * Each call matches its args against `argsMatch`. Unmatched calls return * exit-code 1 with an "unexpected call" stderr. */ function makeSpawn(scripts: ScriptedCall[]): SpawnImpl { let idx = 0; return (cmd: string, args: string[]) => { const joined = `${cmd} ${args.join(' ')}`; const script = scripts[idx]; if (!script) { return makeReturn(1, '', `unexpected call beyond scripted: ${joined}`); } if (!script.argsMatch.test(joined)) { return makeReturn(1, '', `unexpected call shape: ${joined} (expected ${script.argsMatch})`); } idx++; // Honor --json-output: write to that path BEFORE returning. if (script.jsonOutput) { const flagIdx = args.indexOf('--json-output'); if (flagIdx !== -1 && args[flagIdx + 1]) { writeFileSync(args[flagIdx + 1]!, JSON.stringify(script.jsonOutput)); } } if (script.destOutput) { const flagIdx = args.indexOf('--destination'); if (flagIdx !== -1 && args[flagIdx + 1]) { writeFileSync(args[flagIdx + 1]!, script.destOutput); } } return makeReturn(script.exitCode ?? 0, script.stdout ?? '', script.stderr ?? ''); }; } function makeReturn(exit: number, stdout: string, stderr: string) { return { pid: 0, output: [null, Buffer.from(stdout), Buffer.from(stderr)], stdout: Buffer.from(stdout), stderr: Buffer.from(stderr), status: exit, signal: null, } as ReturnType; } describe('bootstrapTunnel', () => { test('returns no_devices when devicectl list shows zero', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [] } }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn }); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe('no_devices'); }); test('returns no_paired_device when device is connected but not paired', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST-UDID', connectionProperties: { tunnelState: 'available (pairing)', pairingState: 'unpaired' }, deviceProperties: { name: 'Test iPhone' }, hardwareProperties: { productType: 'iPhone18,2' }, }], }, }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn }); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error).toBe('no_paired_device'); expect(r.detail).toContain('Trust'); } }); test('never auto-selects a connected paired Vision Pro ahead of an iPhone', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [ { identifier: 'VISION', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Vision Pro' }, hardwareProperties: { productType: 'RealityDevice17,1', platform: 'visionOS', deviceType: 'realityDevice', }, }, { identifier: 'IPHONE', connectionProperties: { tunnelState: 'connected', pairingState: 'paired', transportType: 'wired' }, deviceProperties: { name: 'USB iPhone' }, hardwareProperties: { productType: 'iPhone17,1', platform: 'iOS', deviceType: 'iPhone', }, }, ] }, }, }, { argsMatch: /devicectl device info processes -d IPHONE/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:///var/containers/Bundle/Application/X/com.test.app/com.test' }] } }, }, { argsMatch: /devicectl device info details --device IPHONE/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd00::17' } } }, }, { argsMatch: /devicectl device copy from --device IPHONE/, destOutput: 'TOKEN\n', }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, fetchImpl: (async () => new Response('{"ok":true}', { status: 200 })) as typeof fetch, }); expect(r.ok).toBe(true); if (r.ok) expect(r.tunnel.udid).toBe('IPHONE'); }); test('fails clearly when only non-iPhone platforms are connected', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [ { identifier: 'VISION', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Vision Pro' }, hardwareProperties: { productType: 'RealityDevice17,1', platform: 'visionOS', deviceType: 'realityDevice', }, }, { identifier: 'IPAD', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'iPad' }, hardwareProperties: { productType: 'iPad16,6', platform: 'iOS', deviceType: 'iPad', }, }, ] }, }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn }); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error).toBe('device_not_found'); expect(r.detail).toContain('no iPhone'); } }); test('rejects an explicitly targeted non-iPhone device', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'VISION', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Vision Pro' }, hardwareProperties: { productType: 'RealityDevice17,1', platform: 'visionOS', deviceType: 'realityDevice', }, }] }, }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', udid: 'VISION', spawnImpl: spawn }); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error).toBe('device_not_found'); expect(r.detail).toContain('not an iPhone'); } }); test('skips an unavailable paired device for a connected or available paired device', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [ { identifier: 'STALE', connectionProperties: { tunnelState: 'unavailable', pairingState: 'paired' }, deviceProperties: { name: 'Stale iPhone' }, hardwareProperties: { productType: 'iPhone17,1' }, }, { identifier: 'WIRED', connectionProperties: { tunnelState: 'available', pairingState: 'paired', transportType: 'wired' }, deviceProperties: { name: 'Wired iPhone' }, hardwareProperties: { productType: 'iPhone18,2' }, }, ] }, }, }, { argsMatch: /devicectl device info processes -d WIRED/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:///var/containers/Bundle/Application/X/com.test.app/com.test' }] } }, }, { argsMatch: /devicectl device info details --device WIRED/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd00::2' } } }, }, { argsMatch: /devicectl device copy from --device WIRED/, destOutput: 'TOKEN\n', }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, fetchImpl: (async () => new Response('{"ok":true}', { status: 200 })) as typeof fetch, }); expect(r.ok).toBe(true); if (r.ok) expect(r.tunnel.udid).toBe('WIRED'); }); test('accepts a wired iPhone whose tunnel is disconnected until devicectl wakes it', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [ { identifier: 'STALE-VISION', connectionProperties: { tunnelState: 'unavailable', pairingState: 'paired' }, deviceProperties: { name: 'Stale Vision Pro' }, hardwareProperties: { productType: 'RealityDevice14,1' }, }, { identifier: 'WIRED-DISCONNECTED', connectionProperties: { tunnelState: 'disconnected', pairingState: 'paired', transportType: 'wired', }, deviceProperties: { name: 'USB iPhone' }, hardwareProperties: { productType: 'iPhone18,2' }, }, ] }, }, }, { argsMatch: /devicectl device info processes -d WIRED-DISCONNECTED/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:\/\/\/var\/containers\/Bundle\/Application\/X\/com.test.app\/com.test' }] } }, }, { argsMatch: /devicectl device info details --device WIRED-DISCONNECTED/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd00::27' } } }, }, { argsMatch: /devicectl device copy from --device WIRED-DISCONNECTED/, destOutput: 'TOKEN\n', }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, fetchImpl: (async () => new Response('{"ok":true}', { status: 200 })) as typeof fetch, }); expect(r.ok).toBe(true); if (r.ok) expect(r.tunnel.udid).toBe('WIRED-DISCONNECTED'); }); test('returns device_locked when launchApp errors due to lock', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [] } }, }, { argsMatch: /devicectl device process launch/, stderr: 'Locked ("Unable to launch com.test because the device was not, or could not be, unlocked").', exitCode: 1, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn }); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe('device_locked'); }); test('returns state_server_unreachable when healthz never responds', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:///private/var/containers/Bundle/Application/.../com.test.app/com.test', processIdentifier: 1234 }] } }, stdout: 'com.test', }, { // devicectl device info details (devicectl-based IPv6 resolution). // Return no tunnelIPAddress so we fall through to the injected resolver. argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: {} } }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, resolveImpl: async () => ['fd00::1'], // fetch always fails. fetchImpl: (async () => { throw new Error('connection refused'); }) as typeof fetch, startupTimeoutMs: 200, // short, so test runs fast }); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe('state_server_unreachable'); }); test('rejects a StateServer owned by a different app bundle', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [] } }, }, { argsMatch: /devicectl device process launch/, }, { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd00::9' } } }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.expected.app', spawnImpl: spawn, fetchImpl: (async () => new Response( JSON.stringify({ bundle_id: 'com.other.debug-app' }), { status: 200, headers: { 'content-type': 'application/json' } }, )) as typeof fetch, }); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error).toBe('wrong_app'); expect(r.detail).toContain('com.other.debug-app'); } }); test('happy path: returns DeviceTunnel with rotated token', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST-UDID', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test Device' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:///var/containers/Bundle/Application/X/com.test.app/com.test', processIdentifier: 5678 }] } }, stdout: '/com.test.app/', }, { // devicectl-based IPv6 resolution succeeds — returns the tunnel // address directly, so the injected resolveImpl is never called. argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd99::beef' } } }, }, { argsMatch: /devicectl device copy from/, destOutput: 'BOOT-TOKEN-XYZ-123\n', }, ]); const fetchCalls: Array<{ url: string; method: string }> = []; const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, resolveImpl: async () => ['fd99::beef'], fetchImpl: (async (url, init) => { const u = String(url); const method = (init?.method ?? 'GET').toUpperCase(); fetchCalls.push({ url: u, method }); if (u.endsWith('/healthz')) { return new Response('{"version":"1.0.0"}', { status: 200 }) as Response; } if (u.endsWith('/auth/rotate') && method === 'POST') { // Verify the boot token is sent (not the rotated one). const auth = (init?.headers as Record)['Authorization'] ?? ''; if (auth !== 'Bearer BOOT-TOKEN-XYZ-123') { return new Response('wrong bearer', { status: 401 }) as Response; } return new Response('{"ok":true}', { status: 200 }) as Response; } return new Response('not found', { status: 404 }) as Response; }) as typeof fetch, startupTimeoutMs: 1_000, }); expect(r.ok).toBe(true); if (r.ok) { expect(r.tunnel.udid).toBe('TEST-UDID'); expect(r.tunnel.ipv6Addr).toBe('fd99::beef'); expect(r.tunnel.port).toBe(9999); expect(r.tunnel.bootTokenRotated).toMatch(/^[A-Za-z0-9_-]+$/); expect(r.tunnel.bootTokenRotated).not.toBe('BOOT-TOKEN-XYZ-123'); expect(r.tunnel.bootTokenRotated.length).toBeGreaterThan(20); } // Verify the bootstrap sequence: /healthz first, /auth/rotate second. expect(fetchCalls[0]?.url).toContain('/healthz'); expect(fetchCalls[fetchCalls.length - 1]?.url).toContain('/auth/rotate'); }); test('relaunches once when a prior daemon consumed the running app boot token', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST-UDID', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test Device' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:\/\/\/var\/containers\/Bundle\/Application\/X\/com.test.app\/com.test' }] } }, }, { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd99::beef' } } }, }, { // A prior daemon rotated the one-use token, so StateServer deleted it. argsMatch: /devicectl device copy from/, exitCode: 1, stderr: 'source does not exist', }, { argsMatch: /devicectl device process launch --device TEST-UDID --terminate-existing com\.test/, }, { argsMatch: /devicectl device copy from/, destOutput: 'FRESH-BOOT-TOKEN\n', }, ]); const fetchCalls: Array<{ url: string; authorization?: string }> = []; const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, fetchImpl: (async (url, init) => { const u = String(url); const headers = init?.headers as Record | undefined; fetchCalls.push({ url: u, authorization: headers?.Authorization }); if (u.endsWith('/healthz')) { return new Response(JSON.stringify({ bundle_id: 'com.test' }), { status: 200, headers: { 'content-type': 'application/json' }, }); } if (u.endsWith('/auth/rotate') && headers?.Authorization === 'Bearer FRESH-BOOT-TOKEN') { return new Response('{"ok":true}', { status: 200 }); } return new Response('unauthorized', { status: 401 }); }) as typeof fetch, startupTimeoutMs: 1_000, }); expect(r.ok).toBe(true); expect(fetchCalls.filter((call) => call.url.endsWith('/healthz'))).toHaveLength(2); expect(fetchCalls.at(-1)?.authorization).toBe('Bearer FRESH-BOOT-TOKEN'); }); test('rechecks bundle ownership after recovering a consumed boot token', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST-UDID', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test Device' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:\/\/\/var\/containers\/Bundle\/Application\/X\/com.test.app\/com.test' }] } }, }, { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd99::beef' } } }, }, { argsMatch: /devicectl device copy from/, exitCode: 1 }, { argsMatch: /devicectl device process launch --device TEST-UDID --terminate-existing com\.test/, }, { argsMatch: /devicectl device copy from/, destOutput: 'FRESH-BOOT-TOKEN\n' }, ]); let healthChecks = 0; let rotateCalls = 0; const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, fetchImpl: (async (url) => { const u = String(url); if (u.endsWith('/healthz')) { healthChecks++; return new Response(JSON.stringify({ bundle_id: healthChecks === 1 ? 'com.test' : 'com.other.debug-app', }), { status: 200, headers: { 'content-type': 'application/json' }, }); } rotateCalls++; return new Response('{"ok":true}', { status: 200 }); }) as typeof fetch, startupTimeoutMs: 1_000, }); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe('wrong_app'); expect(healthChecks).toBe(2); expect(rotateCalls).toBe(0); }); test('resolve_failed when hostname cant be resolved to an IPv6', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [{ identifier: 'TEST', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'Test' }, hardwareProperties: { productType: 'iPhone18,2' }, }] }, }, }, { argsMatch: /devicectl device info processes/, // jsonOutput body contains the bundle id path, so isAppRunning() returns true. jsonOutput: { result: { runningProcesses: [{ executable: 'file:///var/containers/Bundle/Application/X/com.test.app/com.test' }] } }, }, { // devicectl device info details returns no tunnel address. argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: {} } }, }, ]); const r = await bootstrapTunnel({ bundleId: 'com.test', spawnImpl: spawn, resolveImpl: async () => { throw new Error('ENOTFOUND'); }, }); expect(r.ok).toBe(false); if (!r.ok) expect(r.error).toBe('resolve_failed'); }); test('respects explicit udid when set', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl list devices/, jsonOutput: { result: { devices: [ { identifier: 'A', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'A' }, hardwareProperties: { productType: 'iPhone18,2' } }, { identifier: 'B', connectionProperties: { tunnelState: 'connected', pairingState: 'paired' }, deviceProperties: { name: 'B' }, hardwareProperties: { productType: 'iPhone18,2' } }, ] }, }, }, { argsMatch: /devicectl device info processes -d B/, jsonOutput: { result: { runningProcesses: [{ executable: 'file:///var/containers/Bundle/Application/X/com.test.app/com.test' }] } }, }, { argsMatch: /devicectl device info details --device B/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd00::b' } } }, }, { argsMatch: /devicectl device copy from --device B/, destOutput: 'TOKEN\n', }, ]); const r = await bootstrapTunnel({ udid: 'B', bundleId: 'com.test', spawnImpl: spawn, resolveImpl: async () => ['fd00::b'], fetchImpl: (async () => new Response('{"ok":true}', { status: 200 })) as typeof fetch, }); expect(r.ok).toBe(true); if (r.ok) expect(r.tunnel.udid).toBe('B'); }); }); describe('getDeviceTunnelIPv6FromDevicectl', () => { test('extracts tunnelIPAddress from connectionProperties', () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details --device TEST-UDID/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fde4:2827:528e::1' } } }, }, ]); expect(getDeviceTunnelIPv6FromDevicectl('TEST-UDID', spawn)).toBe('fde4:2827:528e::1'); }); test('falls back to result.tunnel.ipAddress when connectionProperties absent', () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, jsonOutput: { result: { tunnel: { ipAddress: 'fd00::dead:beef' } } }, }, ]); expect(getDeviceTunnelIPv6FromDevicectl('UDID', spawn)).toBe('fd00::dead:beef'); }); test('returns null when devicectl exits non-zero', () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, exitCode: 1, stderr: 'no such device' }, ]); expect(getDeviceTunnelIPv6FromDevicectl('UDID', spawn)).toBeNull(); }); test('returns null when tunnelIPAddress missing or non-string', () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: {} } } }, ]); expect(getDeviceTunnelIPv6FromDevicectl('UDID', spawn)).toBeNull(); }); }); describe('resolveTunnelIPv6 fallback chain', () => { test('prefers devicectl-based resolution', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: { tunnelIPAddress: 'fd11::1' } } }, }, ]); let resolveCalled = false; const addr = await resolveTunnelIPv6({ udid: 'U', deviceName: 'Test', spawn, resolve: async () => { resolveCalled = true; return ['fd99::99']; }, legacyResolve: async () => { resolveCalled = true; return ['fdAA::AA']; }, }); expect(addr).toBe('fd11::1'); expect(resolveCalled).toBe(false); }); test('falls through to dns.lookup when devicectl yields no address', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, jsonOutput: { result: { connectionProperties: {} } } }, ]); let legacyCalled = false; const addr = await resolveTunnelIPv6({ udid: 'U', deviceName: 'Test', spawn, resolve: async () => ['fd22::2'], legacyResolve: async () => { legacyCalled = true; return ['fdAA::AA']; }, }); expect(addr).toBe('fd22::2'); expect(legacyCalled).toBe(false); }); test('falls through to legacy resolve6 when both devicectl and dns.lookup fail', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, exitCode: 1 }, ]); const addr = await resolveTunnelIPv6({ udid: 'U', deviceName: 'Test', spawn, resolve: async () => { throw new Error('ESERVFAIL'); }, legacyResolve: async () => ['fd33::3'], }); expect(addr).toBe('fd33::3'); }); test('returns null when all three strategies fail', async () => { const spawn = makeSpawn([ { argsMatch: /devicectl device info details/, exitCode: 1 }, ]); const addr = await resolveTunnelIPv6({ udid: 'U', deviceName: 'Test', spawn, resolve: async () => { throw new Error('ESERVFAIL'); }, legacyResolve: async () => { throw new Error('ESERVFAIL'); }, }); expect(addr).toBeNull(); }); }); describe('startTunnelKeepalive', () => { test('invokes devicectl on each interval tick', async () => { const calls: string[] = []; const spawn: SpawnImpl = ((cmd: string, args: string[]) => { calls.push(`${cmd} ${args.slice(0, 4).join(' ')}`); return makeReturn(0, '{}', ''); }) as SpawnImpl; const ka = startTunnelKeepalive('UDID-X', { intervalMs: 20, spawn }); await new Promise((res) => setTimeout(res, 75)); ka.stop(); const before = calls.length; // After stop, no more calls. await new Promise((res) => setTimeout(res, 50)); expect(calls.length).toBe(before); expect(before).toBeGreaterThanOrEqual(2); expect(calls[0]).toContain('devicectl'); expect(calls[0]).toContain('device info details'); }); test('stop() is idempotent', () => { const spawn: SpawnImpl = (() => makeReturn(0, '', '')) as SpawnImpl; const ka = startTunnelKeepalive('U', { intervalMs: 1_000, spawn }); ka.stop(); ka.stop(); // no throw }); });