diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 5a40d794ac4..ac307fef2db 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -165,7 +165,6 @@ 1 test-no-real-time-wait:mobile/src/terminal/terminal-web-document-mount.test.ts 1 test-no-real-time-wait:mobile/src/terminal/terminal-webview-tap-routing.test.ts 1 test-no-real-time-wait:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts -1 test-no-real-time-wait:mobile/src/transport/mobile-relay-pairing-journal-store.test.ts 1 test-no-real-time-wait:mobile/src/transport/mobile-relay-runtime-failover.test.ts 4 test-no-real-time-wait:mobile/src/transport/rpc-client-live-recovery.test.ts 1 test-no-real-time-wait:mobile/src/transport/rpc-operation-barrier.test.ts diff --git a/config/scripts/source-control-path-sort-benchmark.mjs b/config/scripts/source-control-path-sort-benchmark.mjs index ba0bc977a5b..3b5c9716ef1 100644 --- a/config/scripts/source-control-path-sort-benchmark.mjs +++ b/config/scripts/source-control-path-sort-benchmark.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node // Compare per-comparison localeCompare options with hoisted Intl.Collator on Source Control refresh; assert identical sorted output before timing. import { execFileSync } from 'node:child_process' +import { build } from 'esbuild' import { performance } from 'node:perf_hooks' import { fileURLToPath } from 'node:url' @@ -26,26 +27,40 @@ function conflictRank(entry) { return 2 } -// Pre-fix: resolves a collator per comparison. +// Per-call baseline preserves production's locale and deterministic numeric ties. function compareBefore(a, b) { return ( - conflictRank(a) - conflictRank(b) || a.path.localeCompare(b.path, undefined, { numeric: true }) + conflictRank(a) - conflictRank(b) || + a.path.localeCompare(b.path, 'en', { numeric: true }) || + (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ) } -// Post-fix: one hoisted collator, mirroring source-control-status-sort.ts. -const collator = new Intl.Collator(undefined, { numeric: true }) -function compareAfter(a, b) { - return conflictRank(a) - conflictRank(b) || collator.compare(a.path, b.path) -} +const { outputFiles } = await build({ + entryPoints: [ + fileURLToPath( + new URL( + '../../src/renderer/src/components/right-sidebar/source-control-status-sort.ts', + import.meta.url + ) + ) + ], + bundle: true, + platform: 'node', + format: 'esm', + write: false +}) +const { compareGitStatusEntries: compareAfter } = await import( + `data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}` +) // Use real byte-sorted Git paths: shuffled input inflates win over nearly sorted production arrays. -const REPO_PATHS = execFileSync('git', ['ls-files'], { +const REPO_PATHS = execFileSync('git', ['ls-files', '-z'], { cwd: fileURLToPath(new URL('../..', import.meta.url)), maxBuffer: 256 * 1024 * 1024 }) .toString() - .split('\n') + .split('\0') .filter(Boolean) function makeEntries(count) { @@ -88,7 +103,7 @@ for (const count of [3, 17, 26, 63, 308, 1000]) { const entries = makeEntries(count) const before = [...entries].sort(compareBefore).map((entry) => entry.path) const after = [...entries].sort(compareAfter).map((entry) => entry.path) - if (before.join('\n') !== after.join('\n')) { + if (before.length !== after.length || before.some((path, index) => path !== after[index])) { throw new Error(`sort order differs at ${count} files`) } const beforeMs = measure(compareBefore, entries) diff --git a/config/scripts/ssh-cleanup-tab-map-benchmark.mjs b/config/scripts/ssh-cleanup-tab-map-benchmark.mjs index 43a2f9ba160..9422a5df717 100644 --- a/config/scripts/ssh-cleanup-tab-map-benchmark.mjs +++ b/config/scripts/ssh-cleanup-tab-map-benchmark.mjs @@ -11,20 +11,7 @@ assert( sources.every((source) => source.includes('export function buildRemovedSshTargetCleanupPatch')) ) -async function load(source, instrument = false) { - if (instrument) { - const spread = '...nextTabsByWorktree' - assert.equal(source.split(spread).length, 2) - source = source.replace(spread, '...countTabMapCopy(nextTabsByWorktree)') - source += ` - export const tabMapCopies = { count: 0, entries: 0 }; - function countTabMapCopy(map) { - tabMapCopies.count++; - tabMapCopies.entries += Reflect.ownKeys(map).length; - return map; - } - ` - } +async function load(source) { source += "\nexport { toAppSshPtyId } from '../../../../shared/ssh-pty-id';" const result = await build({ entryPoints: [entry], @@ -62,7 +49,7 @@ function emptyState() { tabsByWorktree: {}, ptyIdsByTabId: {}, lastKnownRelayPtyIdByTabId: {}, - pendingCodexPaneRestartIds: {}, + pendingPaneAccountRestarts: {}, codexRestartNoticeByPtyId: {}, deferredSshSessionIdsByTabId: {}, pendingReconnectPtyIdByTabId: {}, @@ -72,6 +59,7 @@ function emptyState() { sshTargetLabels: new Map(), sshTargetGenerations: new Map(), portForwardsByConnection: {}, + unrestoredPortForwardsByConnection: {}, detectedPortsByConnection: {}, sshCredentialQueue: [] } @@ -201,7 +189,7 @@ for (let iteration = 0; iteration < 3000; iteration++) { state.pendingReconnectPtyIdByTabId[id] = toAppSshPtyId(owner, 'reconnect') } if (ptyId) { - state.pendingCodexPaneRestartIds[ptyId] = true + state.pendingPaneAccountRestarts[ptyId] = 'codex' state.codexRestartNoticeByPtyId[ptyId] = { previousAccountLabel: 'old', nextAccountLabel: 'new' @@ -227,35 +215,13 @@ for (let iteration = 0; iteration < 3000; iteration++) { } console.log('3,000 frozen-state full-patch / identity differential cases passed') -// Instrument only the copy site for counts; timing arms above remain uninstrumented. -const counted = await Promise.all(sources.map((source) => load(source, true))) -for (const stride of [1, 10, 0]) { - const state = timedState(100, stride, false) - const expectedCopies = stride === 0 ? [0, 0] : [100 / stride, 1] - counted.forEach((module, index) => { - module.tabMapCopies.count = 0 - module.tabMapCopies.entries = 0 - assert.deepEqual( - module.buildRemovedSshTargetCleanupPatch(state, 'removed'), - arms[index](state, 'removed') - ) - assert.deepEqual(module.tabMapCopies, { - count: expectedCopies[index], - entries: expectedCopies[index] * 100 - }) - }) - console.log( - JSON.stringify({ stride, copiedEntries: counted.map((module) => module.tabMapCopies.entries) }) - ) -} - -function sample(arm, state, repeats) { +function sample(arm, state, repeats, expectedChanged) { const start = performance.now() let changed = 0 for (let i = 0; i < repeats; i++) { changed += arm(state, 'removed') !== null ? 1 : 0 } - assert(changed === 0 || changed === repeats) + assert.equal(changed, expectedChanged ? repeats : 0) return (performance.now() - start) / repeats } @@ -281,19 +247,22 @@ for (const [count, stride, catalog, tabsPerWorkspace] of [ [500, 1, true, 4] ]) { const state = timedState(count, stride, catalog, tabsPerWorkspace) - compare(state, 'removed') + const expectedChanged = compare(state, 'removed')[0] !== null for (const arm of arms) { const until = performance.now() + 80 while (performance.now() < until) { - sample(arm, state, 1) + sample(arm, state, 1, expectedChanged) } } - const repeats = Math.max(1, Math.min(20000, Math.ceil(40 / sample(arms[0], state, 1)))) + const repeats = Math.max( + 1, + Math.min(20000, Math.ceil(40 / sample(arms[0], state, 1, expectedChanged))) + ) /** @type {number[][]} */ const samples = [[], []] for (let pair = 0; pair < 8; pair++) { for (const index of pair % 2 ? [1, 0] : [0, 1]) { - samples[index].push(sample(arms[index], state, repeats)) + samples[index].push(sample(arms[index], state, repeats, expectedChanged)) } } const median = samples.map((values) => { diff --git a/mobile/src/transport/mobile-relay-direct-upgrade.test.ts b/mobile/src/transport/mobile-relay-direct-upgrade.test.ts index 7fea8917849..2ddb5d4290f 100644 --- a/mobile/src/transport/mobile-relay-direct-upgrade.test.ts +++ b/mobile/src/transport/mobile-relay-direct-upgrade.test.ts @@ -72,10 +72,6 @@ function dependencies(journal: MobileRelayDirectUpgradeJournal | null = null) { describe('existing direct pairing relay upgrade', () => { it('persists pending material before install and publishes only after committed status', async () => { const deps = dependencies() - let journal: MobileRelayDirectUpgradeJournal | null = null - deps.writeJournal.mockImplementation(async (next) => { - journal = next - }) const client = clientWith([ success({ v: 1, relay }), success({ @@ -105,13 +101,34 @@ describe('existing direct pairing relay upgrade', () => { const result = await upgradeDirectMobileRelay({ client, host, dependencies: deps }) - expect(journal).not.toBeNull() + expect(deps.writeJournal).toHaveBeenCalledOnce() + const journal = deps.writeJournal.mock.calls[0]?.[0] + if (!journal) { + throw new Error('Upgrade did not persist pending material') + } + expect(client.sendRequest).toHaveBeenNthCalledWith(1, 'pairing.getEndpoints', { + installReqId: journal.reqId + }) + expect(client.sendRequest).toHaveBeenNthCalledWith(3, 'pairing.getEndpoints', { + installReqId: journal.reqId + }) + expect(deps.writeBundle).toHaveBeenCalledWith({ + v: 1, + hostId: host.id, + deviceToken: host.deviceToken, + current: { + token: journal.pendingResumeToken, + hash: journal.pendingResumeTokenHash, + version: 1, + expiresAt: 9_999_999 + } + }) expect(deps.writeJournal.mock.invocationCallOrder[0]).toBeLessThan( vi.mocked(client.sendRequest).mock.invocationCallOrder[0]! ) expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'pairing.provisionRelay', { - reqId: journal!.reqId, - newResumeTokenHash: journal!.pendingResumeTokenHash + reqId: journal.reqId, + newResumeTokenHash: journal.pendingResumeTokenHash }) expect(deps.writeBundle).toHaveBeenCalledBefore(deps.setRelayRouting) expect(deps.setRelayRouting).toHaveBeenCalledWith(host.id, relay) diff --git a/mobile/src/transport/mobile-relay-invite-director.test.ts b/mobile/src/transport/mobile-relay-invite-director.test.ts index 77a0cdde270..60d7bb18ff7 100644 --- a/mobile/src/transport/mobile-relay-invite-director.test.ts +++ b/mobile/src/transport/mobile-relay-invite-director.test.ts @@ -73,7 +73,7 @@ describe('pairing invite director resolution', () => { type: 'relay-moved', v: 1, cellUrl: 'https://relay-c2.onorca.dev', - assignmentEpoch: 7, + assignmentEpoch: 8, targetFromCell: true }) }) diff --git a/mobile/src/transport/mobile-relay-pairing-journal-store.test.ts b/mobile/src/transport/mobile-relay-pairing-journal-store.test.ts index 03609786aee..c6088ae63e5 100644 --- a/mobile/src/transport/mobile-relay-pairing-journal-store.test.ts +++ b/mobile/src/transport/mobile-relay-pairing-journal-store.test.ts @@ -114,6 +114,15 @@ describe('mobile relay pairing journal store', () => { expect(asyncStorage.setItem.mock.invocationCallOrder[0]).toBeLessThan( secureStore.setItemAsync.mock.invocationCallOrder[0]! ) + expect(asyncStorage.setItem).toHaveBeenCalledWith( + 'orca:mobile-relay:pairing-journal:v1', + JSON.stringify(journal.metadata) + ) + expect(secureStore.setItemAsync).toHaveBeenCalledWith( + 'orca.mobile-relay.pairing-journal.v1', + JSON.stringify(journal.secrets), + expect.objectContaining({ keychainAccessible: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY' }) + ) expect(metadataRaw).not.toContain(offer.deviceToken) expect(metadataRaw).not.toContain(offer.relay.inviteToken) expect(metadataRaw).not.toContain(journal.secrets.pendingResumeToken) @@ -238,12 +247,17 @@ describe('mobile relay pairing journal store', () => { const secretReadGate = new Promise((resolve) => { releaseSecretRead = resolve }) + let enteredSecretRead = () => {} + const secretReadStarted = new Promise((resolve) => { + enteredSecretRead = resolve + }) secureStore.getItemAsync.mockImplementationOnce(async () => { + enteredSecretRead() await secretReadGate return secretRaw }) const loading = loadMobileRelayPairingJournal() - await vi.waitFor(() => expect(secureStore.getItemAsync).toHaveBeenCalled()) + await secretReadStarted const replacement = createMobileRelayPairingJournal({ offer: offer as PairingOffer & { relay: NonNullable }, @@ -379,33 +393,38 @@ describe('mobile relay pairing journal store', () => { }) it('makes a superseding scan wait for a publication in progress, and refuses a stale claim', async () => { - const created = createMobileRelayPairingJournal({ - offer: offer as PairingOffer & { relay: NonNullable }, - hostId: 'host-1', - hostName: 'Blue Whale', - randomBytes: (length) => new Uint8Array(length).fill(10) - }) - await saveMobileRelayPairingJournal(created) - releaseMobileRelayPairingJournal(created.metadata.journalId) - const release = await claimMobileRelayPairingJournalPublication(created.metadata.journalId) - const replacement = createMobileRelayPairingJournal({ - offer: offer as PairingOffer & { relay: NonNullable }, - hostId: 'host-2', - hostName: 'Red Panda', - randomBytes: (length) => new Uint8Array(length).fill(12) - }) - let saved = false - const save = saveMobileRelayPairingJournal(replacement).then(() => (saved = true)) - await new Promise((resolve) => setTimeout(resolve, 10)) - expect(saved).toBe(false) + vi.useFakeTimers() + try { + const created = createMobileRelayPairingJournal({ + offer: offer as PairingOffer & { relay: NonNullable }, + hostId: 'host-1', + hostName: 'Blue Whale', + randomBytes: (length) => new Uint8Array(length).fill(10) + }) + await saveMobileRelayPairingJournal(created) + releaseMobileRelayPairingJournal(created.metadata.journalId) + const release = await claimMobileRelayPairingJournalPublication(created.metadata.journalId) + const replacement = createMobileRelayPairingJournal({ + offer: offer as PairingOffer & { relay: NonNullable }, + hostId: 'host-2', + hostName: 'Red Panda', + randomBytes: (length) => new Uint8Array(length).fill(12) + }) + let saved = false + const save = saveMobileRelayPairingJournal(replacement).then(() => (saved = true)) + await vi.advanceTimersByTimeAsync(0) + expect(saved).toBe(false) - await clearMobileRelayPairingJournal(created.metadata.journalId) - release() - await save - await expect(loadMobileRelayPairingJournal()).resolves.toEqual(replacement) - await expect( - claimMobileRelayPairingJournalPublication(created.metadata.journalId) - ).rejects.toThrow(/stale/) + await clearMobileRelayPairingJournal(created.metadata.journalId) + release() + await save + await expect(loadMobileRelayPairingJournal()).resolves.toEqual(replacement) + await expect( + claimMobileRelayPairingJournalPublication(created.metadata.journalId) + ).rejects.toThrow(/stale/) + } finally { + vi.useRealTimers() + } }) it('self-heals an undecryptable Android secret so the next QR scan can pair', async () => { diff --git a/mobile/src/transport/mobile-relay-resume-director.test.ts b/mobile/src/transport/mobile-relay-resume-director.test.ts index 88322b2c6a1..e45ddb1b52f 100644 --- a/mobile/src/transport/mobile-relay-resume-director.test.ts +++ b/mobile/src/transport/mobile-relay-resume-director.test.ts @@ -53,7 +53,7 @@ describe('mobile relay resume director', () => { ) await expect( resolveMobileRelayEndpoint({ relay, resumeToken: 'A'.repeat(43), fetchImpl: badTarget }) - ).rejects.toThrow() + ).rejects.toThrow(/cellUrl/) const oversized = vi.fn( async () => @@ -62,6 +62,24 @@ describe('mobile relay resume director', () => { await expect( resolveMobileRelayEndpoint({ relay, resumeToken: 'A'.repeat(43), fetchImpl: oversized }) ).rejects.toThrow(/too large/) + + const oversizedBody = + ' '.repeat(16 * 1024) + + JSON.stringify({ + v: 1, + cellUrl: 'https://relay-c2.onorca.dev', + assignmentEpoch: 8, + leaseExpiresAt: 1 + }) + for (const headers of [new Headers(), new Headers({ 'content-length': '1' })]) { + await expect( + resolveMobileRelayEndpoint({ + relay, + resumeToken: 'A'.repeat(43), + fetchImpl: async () => new Response(oversizedBody, { headers }) + }) + ).rejects.toThrow(/too large/) + } }) it('carries the bounded-overload Retry-After off a rejected resolve', async () => { diff --git a/mobile/src/transport/mobile-relay-runtime-failover.test.ts b/mobile/src/transport/mobile-relay-runtime-failover.test.ts index e2c54f82cad..b62cccf8432 100644 --- a/mobile/src/transport/mobile-relay-runtime-failover.test.ts +++ b/mobile/src/transport/mobile-relay-runtime-failover.test.ts @@ -263,7 +263,7 @@ describe('relay runtime recovery without direct connectivity', () => { expect(openRelay).toHaveBeenCalledTimes(2) expect(openRelay).toHaveBeenLastCalledWith( relay, - expect.objectContaining({ version: 3 }), + expect.objectContaining({ token: 'token-v3'.padEnd(43, 'A'), version: 3 }), expect.any(String), expect.any(Function) ) @@ -351,7 +351,7 @@ describe('relay runtime recovery without direct connectivity', () => { expect(deps.openRelay).toHaveBeenCalledOnce() expect(deps.openRelay).toHaveBeenLastCalledWith( relay, - expect.objectContaining({ version: 2 }), + expect.objectContaining({ token: 'token-v2'.padEnd(43, 'A'), version: 2 }), expect.any(String), expect.any(Function) ) @@ -380,7 +380,7 @@ describe('relay runtime recovery without direct connectivity', () => { expect(openRelay).toHaveBeenCalledTimes(2) expect(openRelay).toHaveBeenLastCalledWith( relay, - expect.objectContaining({ version: 1 }), + expect.objectContaining({ token: 'token-v1'.padEnd(43, 'A'), version: 1 }), expect.any(String), expect.any(Function) ) @@ -524,6 +524,7 @@ describe('failover with a real direct rpc-client', () => { await supervisor.start() await vi.advanceTimersByTimeAsync(3_000) + expect(DeadSocket.constructed).toContain(DIRECT_ENDPOINT) expect(deps.openRelay).toHaveBeenCalled() expect(logical.getActivePath()).toBe('relay') supervisor.stop() @@ -548,6 +549,7 @@ describe('failover with a real direct rpc-client', () => { await started await vi.advanceTimersByTimeAsync(3_000) + expect(DeadSocket.constructed).toContain(DIRECT_ENDPOINT) expect(deps.openRelay).toHaveBeenCalled() expect(logical.getActivePath()).toBe('relay') supervisor.stop() diff --git a/mobile/src/transport/pairing-keychain.test.ts b/mobile/src/transport/pairing-keychain.test.ts index 078088bbe7f..509c8c31f8f 100644 --- a/mobile/src/transport/pairing-keychain.test.ts +++ b/mobile/src/transport/pairing-keychain.test.ts @@ -90,6 +90,7 @@ describe('pairing keychain', () => { const rotated = secureStoreMock.setItemAsync.mock.calls.at(-1)! expect(serviceOf(rotated[2] as Options)).toBe('orca.pairing.v1') + expect(rotated[0]).toBe(TOKEN_KEY) expect(rotated[1]).toBe('token') expect(generationRecord).toBe('1') }) @@ -209,6 +210,7 @@ describe('pairing keychain', () => { serviceOf(options) === 'orca.pairing.v1' ? 'token' : null ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) }) it('reads through the rotated service once a rotation has been committed', async () => { @@ -218,6 +220,7 @@ describe('pairing keychain', () => { ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) }) it('falls back to a retired service so rotation does not orphan a still-readable token', async () => { @@ -227,6 +230,7 @@ describe('pairing keychain', () => { ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('legacy-token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(3) }) @@ -241,6 +245,7 @@ describe('pairing keychain', () => { }) await expect(readPairingKeychainItem(TOKEN_KEY)).rejects.toBe(currentError) + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(1) }) @@ -280,11 +285,13 @@ describe('pairing keychain', () => { // Treat Android’s indistinguishable absent/undecryptable null as absent so callers can clean orphans. await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBeNull() + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(1) // Keep presence so later reads cannot resurrect superseded legacy secrets. expect(presenceRecord).toBe('1') await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBeNull() + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(2) }) @@ -295,6 +302,7 @@ describe('pairing keychain', () => { ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('legacy-token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) }) it('probes every bounded service when the generation record is temporarily unreadable', async () => { @@ -309,6 +317,7 @@ describe('pairing keychain', () => { ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(7) }) @@ -328,10 +337,13 @@ describe('pairing keychain', () => { await deletePairingKeychainItem(TOKEN_KEY) - const services = secureStoreMock.deleteItemAsync.mock.calls.map((call) => - serviceOf(call[1] as Options) - ) - expect(services).toEqual(['orca.pairing.v2', 'orca.pairing.v1', undefined]) + expect( + secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [key, serviceOf(options)]) + ).toEqual([ + [TOKEN_KEY, 'orca.pairing.v2'], + [TOKEN_KEY, 'orca.pairing.v1'], + [TOKEN_KEY, undefined] + ]) expect(asyncStorageMock.removeItem).toHaveBeenCalledWith(TOKEN_PRESENCE_KEY) }) @@ -345,25 +357,49 @@ describe('pairing keychain', () => { }) await expect(deletePairingKeychainItem(TOKEN_KEY)).rejects.toBe(deleteError) - expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledTimes(3) + expect( + secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [key, serviceOf(options)]) + ).toEqual([ + [TOKEN_KEY, 'orca.pairing.v2'], + [TOKEN_KEY, 'orca.pairing.v1'], + [TOKEN_KEY, undefined] + ]) expect(asyncStorageMock.removeItem).not.toHaveBeenCalled() }) it('serializes writes that share the global generation', async () => { - let releaseFirst!: () => void - const firstWrite = new Promise((resolve) => { - releaseFirst = resolve + vi.useFakeTimers() + const entered = Promise.withResolvers() + const held = Promise.withResolvers() + const order: string[] = [] + secureStoreMock.setItemAsync.mockImplementation(async (key: string, value: string) => { + order.push(`start:${key}:${value}`) + if (key === TOKEN_KEY) { + entered.resolve() + await held.promise + } + order.push(`finish:${key}:${value}`) }) - secureStoreMock.setItemAsync.mockImplementationOnce(async () => firstWrite) + try { + const first = writePairingKeychainItem(TOKEN_KEY, 'first') + const second = writePairingKeychainItem(`${TOKEN_KEY}.second`, 'second') + await entered.promise + await vi.advanceTimersByTimeAsync(0) + expect(order).toEqual([`start:${TOKEN_KEY}:first`]) - const first = writePairingKeychainItem(TOKEN_KEY, 'first') - const second = writePairingKeychainItem(`${TOKEN_KEY}.second`, 'second') - await vi.waitFor(() => expect(secureStoreMock.setItemAsync).toHaveBeenCalledTimes(1)) + held.resolve() + await Promise.all([first, second]) - releaseFirst() - await Promise.all([first, second]) - - expect(secureStoreMock.setItemAsync).toHaveBeenCalledTimes(2) + expect(order).toEqual([ + `start:${TOKEN_KEY}:first`, + `finish:${TOKEN_KEY}:first`, + `start:${TOKEN_KEY}.second:second`, + `finish:${TOKEN_KEY}.second:second` + ]) + } finally { + held.resolve() + vi.useRealTimers() + } }) it.each(['not-a-number', '', ' ', '0:pending'])( @@ -375,6 +411,7 @@ describe('pairing keychain', () => { ) await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token') + expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object)) } ) @@ -397,7 +434,22 @@ describe('pairing keychain', () => { await deletePairingKeychainItem(TOKEN_KEY) - expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledTimes(9) + expect( + secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [ + key, + serviceOf(options) + ]) + ).toEqual([ + [TOKEN_KEY, 'orca.pairing.v8'], + [TOKEN_KEY, 'orca.pairing.v7'], + [TOKEN_KEY, 'orca.pairing.v6'], + [TOKEN_KEY, 'orca.pairing.v5'], + [TOKEN_KEY, 'orca.pairing.v4'], + [TOKEN_KEY, 'orca.pairing.v3'], + [TOKEN_KEY, 'orca.pairing.v2'], + [TOKEN_KEY, 'orca.pairing.v1'], + [TOKEN_KEY, undefined] + ]) } ) diff --git a/mobile/src/transport/pairing-relay-candidate.test.ts b/mobile/src/transport/pairing-relay-candidate.test.ts index 6a8fc00179f..aaefa7e5e10 100644 --- a/mobile/src/transport/pairing-relay-candidate.test.ts +++ b/mobile/src/transport/pairing-relay-candidate.test.ts @@ -291,7 +291,10 @@ describe('recovering pairing relay candidate', () => { }) await expect(candidate.sendRequest('status.get')).resolves.toEqual(success()) - expect(resolveDirector).toHaveBeenCalledOnce() + expect(resolveDirector).toHaveBeenCalledExactlyOnceWith({ + ...journal.metadata.relay, + inviteToken: journal.secrets.inviteToken + }) }) it('bounds director recovery and applies full jitter to failures and target retries', async () => { diff --git a/mobile/src/transport/pairing-relay-served-recovery.test.ts b/mobile/src/transport/pairing-relay-served-recovery.test.ts index 49cd466f240..f7efad5af1b 100644 --- a/mobile/src/transport/pairing-relay-served-recovery.test.ts +++ b/mobile/src/transport/pairing-relay-served-recovery.test.ts @@ -47,7 +47,10 @@ describe('served relay pairing recovery', () => { }) await expect(candidate.sendRequest('status.get')).resolves.toMatchObject({ ok: true }) - expect(resolveDirector).toHaveBeenCalledOnce() + expect(resolveDirector).toHaveBeenCalledExactlyOnceWith({ + ...journal.metadata.relay, + inviteToken: journal.secrets.inviteToken + }) expect(persistMove).toHaveBeenCalledWith(resolvedRelay) }) diff --git a/src/main/runtime/hook-status-resume-sleep-ordering.test.ts b/src/main/runtime/hook-status-resume-sleep-ordering.test.ts index b42ef1c15a9..7ac41fbbc0d 100644 --- a/src/main/runtime/hook-status-resume-sleep-ordering.test.ts +++ b/src/main/runtime/hook-status-resume-sleep-ordering.test.ts @@ -45,6 +45,7 @@ function makeRuntimeWithSlowFirstWrite() { const firstWriteDurable = new Promise((resolve) => { releaseFirstWrite = resolve }) + const firstWriteEntered = Promise.withResolvers() let writes = 0 const applyLayoutCommand = vi.fn( async ( @@ -59,6 +60,7 @@ function makeRuntimeWithSlowFirstWrite() { } writes += 1 if (writes === 1) { + firstWriteEntered.resolve() await firstWriteDurable } return { ok: true, result: {} } @@ -66,7 +68,7 @@ function makeRuntimeWithSlowFirstWrite() { ) const runtime = new OrcaRuntimeService(Object.assign(runtimeStore, { applyLayoutCommand })) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) - return { runtime, getSession, releaseFirstWrite } + return { runtime, getSession, releaseFirstWrite, firstWriteEntered: firstWriteEntered.promise } } function sleepCapture(updatedAt: number): WorktreeSleepResumeCapture { @@ -98,11 +100,11 @@ function sleepCapture(updatedAt: number): WorktreeSleepResumeCapture { describe('a queued hook status and a workspace sleep', () => { it('keeps the sleep’s record when a status accepted before the sleep is written after it', async () => { - const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite() + const { runtime, getSession, releaseFirstWrite, firstWriteEntered } = + makeRuntimeWithSlowFirstWrite() runtime.recordHookStatusResume(codexRow()) - await vi.waitFor(() => - expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined() - ) + await firstWriteEntered + expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined() runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 })) const slept = runtime['sleepResumeRecords'].persist(TEST_WORKTREE_ID, sleepCapture(11), [ @@ -120,11 +122,11 @@ describe('a queued hook status and a workspace sleep', () => { }) it('keeps a window sleep’s record when a status accepted before it is written after it', async () => { - const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite() + const { runtime, getSession, releaseFirstWrite, firstWriteEntered } = + makeRuntimeWithSlowFirstWrite() runtime.recordHookStatusResume(codexRow()) - await vi.waitFor(() => - expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined() - ) + await firstWriteEntered + expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined() runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 })) const [record] = sleepCapture(11).get(PTY_ID)!.records diff --git a/src/main/runtime/host-account-sign-in-flows.test.ts b/src/main/runtime/host-account-sign-in-flows.test.ts index 70631d2626b..fd13c844845 100644 --- a/src/main/runtime/host-account-sign-in-flows.test.ts +++ b/src/main/runtime/host-account-sign-in-flows.test.ts @@ -38,7 +38,7 @@ describe('HostAccountSignInFlows', () => { const flows = new HostAccountSignInFlows(runner.run, 'linux') const started = flows.start('device-a', 'claude') expect(started.phase).toBe('starting') - await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce()) + expect(runner.run).toHaveBeenCalledOnce() runner.channel().onPrompt({ ...PROMPT, needsCode: true }) const waiting = await flows.wait('device-a', started.flowId, started.revision) @@ -77,7 +77,7 @@ describe('HostAccountSignInFlows', () => { const runner = controllableRunner() const flows = new HostAccountSignInFlows(runner.run, 'linux') const { flowId } = flows.start('device-a', 'claude') - await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce()) + expect(runner.run).toHaveBeenCalledOnce() expect((await flows.cancel('device-a', flowId)).phase).toBe('cancelled') expect(runner.channel().signal.aborted).toBe(true) await expect(runner.channel().nextCode()).rejects.toThrow() @@ -115,7 +115,7 @@ describe('HostAccountSignInFlows', () => { const runner = controllableRunner() const flows = new HostAccountSignInFlows(runner.run, 'linux') const { flowId, revision } = flows.start('device-a', 'claude') - await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce()) + expect(runner.run).toHaveBeenCalledOnce() runner .channel() .onPrompt({ url: 'https://evil.test/?code=true', userCode: null, needsCode: true }) diff --git a/src/main/runtime/host-pane-remount-terminal-handle.test.ts b/src/main/runtime/host-pane-remount-terminal-handle.test.ts index 3793ccbfe51..d5d37bdffc7 100644 --- a/src/main/runtime/host-pane-remount-terminal-handle.test.ts +++ b/src/main/runtime/host-pane-remount-terminal-handle.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { RpcDispatcher } from './rpc/dispatcher' import type { RpcRequest } from './rpc/core' @@ -46,10 +46,12 @@ function graph(ptyId: string | null, extraLeafPtyId?: string) { function makeHost(options: { livePtyIds: Set }) { const writes: string[] = [] + const inputWritten = Promise.withResolvers() const runtime = new OrcaRuntimeService(null) runtime.setPtyController({ write: (_ptyId, data) => { writes.push(data) + inputWritten.resolve() return true }, kill: () => true, @@ -57,7 +59,7 @@ function makeHost(options: { livePtyIds: Set }) { hasPty: (ptyId) => options.livePtyIds.has(ptyId) }) runtime.attachWindow(1) - return { runtime, writes } + return { runtime, writes, inputWritten: inputWritten.promise } } async function handleFor(runtime: OrcaRuntimeService): Promise { @@ -67,6 +69,9 @@ async function handleFor(runtime: OrcaRuntimeService): Promise { function startMultiplex(runtime: OrcaRuntimeService) { const binaryFrames: StreamFrame[] = [] + const controlRegistered = Promise.withResolvers() + const subscribed = Promise.withResolvers() + const writeUnavailable = Promise.withResolvers() const handlers = new Map void>() const messages: { result?: { type?: string; streamId?: number } }[] = [] const request: RpcRequest = { @@ -77,18 +82,29 @@ function startMultiplex(runtime: OrcaRuntimeService) { } void new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }).dispatchStreaming( request, - (message) => messages.push(JSON.parse(message)), + (message) => { + messages.push(JSON.parse(message)) + if (messages.at(-1)?.result?.type === 'subscribed') { + subscribed.resolve() + } + }, { connectionId: 'paired-client', sendBinary: (bytes: Uint8Array) => { const frame = decodeTerminalStreamFrame(bytes) if (frame) { binaryFrames.push(frame) + if (frame.opcode === TerminalStreamOpcode.WriteUnavailable) { + writeUnavailable.resolve() + } } return true }, registerBinaryStreamHandler: (streamId: number, handler: (frame: StreamFrame) => void) => { handlers.set(streamId, handler) + if (streamId === 0) { + controlRegistered.resolve() + } return () => handlers.delete(streamId) } } @@ -97,17 +113,26 @@ function startMultiplex(runtime: OrcaRuntimeService) { handlers.get(streamId)?.( decodeTerminalStreamFrame(encodeTerminalStreamFrame({ opcode, streamId, seq: 0, payload }))! ) - return { binaryFrames, handlers, messages, send } + return { + binaryFrames, + handlers, + messages, + send, + controlRegistered: controlRegistered.promise, + subscribed: subscribed.promise, + writeUnavailable: writeUnavailable.promise + } } describe('a host pane remounting onto its same live PTY (#23759)', () => { it('keeps the handle a paired client is typing into', async () => { - const { runtime, writes } = makeHost({ livePtyIds: new Set([PTY_ID]) }) + const { runtime, writes, inputWritten } = makeHost({ livePtyIds: new Set([PTY_ID]) }) runtime.syncWindowGraph(1, graph(PTY_ID)) const handle = await handleFor(runtime) const client = startMultiplex(runtime) - await vi.waitFor(() => expect(client.handlers.has(0)).toBe(true)) + await client.controlRegistered + expect(client.handlers.has(0)).toBe(true) client.send( 0, TerminalStreamOpcode.Subscribe, @@ -118,16 +143,16 @@ describe('a host pane remounting onto its same live PTY (#23759)', () => { capabilities: { writeUnavailable: 1 } }) ) - await vi.waitFor(() => - expect(client.messages.some((message) => message.result?.type === 'subscribed')).toBe(true) - ) + await client.subscribed + expect(client.messages.some((message) => message.result?.type === 'subscribed')).toBe(true) runtime.syncWindowGraph(1, graph(null)) runtime.syncWindowGraph(1, graph(PTY_ID)) expect(await handleFor(runtime)).toBe(handle) client.send(1, TerminalStreamOpcode.Input, encodeTerminalStreamText('ls\r')) - await vi.waitFor(() => expect(writes).toEqual(['ls\r'])) + await Promise.race([inputWritten, client.writeUnavailable]) + expect(writes).toEqual(['ls\r']) expect( client.binaryFrames.some((frame) => frame.opcode === TerminalStreamOpcode.WriteUnavailable) ).toBe(false)