diff --git a/mobile/src/transport/mobile-e2ee-v2-key-schedule.test.ts b/mobile/src/transport/mobile-e2ee-v2-key-schedule.test.ts index ea148b2dbad..74c90980080 100644 --- a/mobile/src/transport/mobile-e2ee-v2-key-schedule.test.ts +++ b/mobile/src/transport/mobile-e2ee-v2-key-schedule.test.ts @@ -32,7 +32,9 @@ describe('mobile E2EE v2 key schedule', () => { it('derives unique direction keys and session IDs across fresh client nonces', () => { const { hello, ready, sharedSecret } = createMobileE2EEV2Fixture() - const fingerprints = new Set() + const mobileToDesktopKeys = new Set() + const desktopToMobileKeys = new Set() + const sessionIds = new Set() for (let index = 0; index < 128; index++) { const nonce = new Uint8Array(32) new DataView(nonce.buffer).setUint32(28, index, false) @@ -47,12 +49,12 @@ describe('mobile E2EE v2 key schedule', () => { clientNonce: handshake.clientNonce, desktopNonce: handshake.desktopNonce }) - fingerprints.add( - [schedule.mobileToDesktopKey, schedule.desktopToMobileKey, schedule.sessionId] - .map(hex) - .join(':') - ) + mobileToDesktopKeys.add(hex(schedule.mobileToDesktopKey)) + desktopToMobileKeys.add(hex(schedule.desktopToMobileKey)) + sessionIds.add(hex(schedule.sessionId)) } - expect(fingerprints.size).toBe(128) + expect(mobileToDesktopKeys.size).toBe(128) + expect(desktopToMobileKeys.size).toBe(128) + expect(sessionIds.size).toBe(128) }) }) diff --git a/mobile/src/transport/mobile-relay-direct-upgrade-controller.test.ts b/mobile/src/transport/mobile-relay-direct-upgrade-controller.test.ts index 0d99c7d9429..1176cb707ac 100644 --- a/mobile/src/transport/mobile-relay-direct-upgrade-controller.test.ts +++ b/mobile/src/transport/mobile-relay-direct-upgrade-controller.test.ts @@ -56,7 +56,7 @@ function logicalClient(initial: ConnectionState) { } describe('direct pairing upgrade controller', () => { - it('upgrades immediately after an authenticated direct connection', async () => { + it('forwards a completed upgrade for an already connected client', async () => { const logical = logicalClient('connected') const upgrade = vi.fn(async () => upgraded) const onUpgraded = vi.fn(async () => {}) @@ -83,7 +83,9 @@ describe('direct pairing upgrade controller', () => { await controller.start() controller.setForeground(false) controller.setForeground(true) - await vi.waitFor(() => expect(onUpgraded).toHaveBeenCalledOnce()) + await Promise.resolve() + + expect(onUpgraded).toHaveBeenCalledOnce() expect(upgrade).toHaveBeenCalledTimes(2) }) diff --git a/mobile/src/transport/mobile-relay-orphan-cleanup.test.ts b/mobile/src/transport/mobile-relay-orphan-cleanup.test.ts index ceab7e6b412..4365bc37fe7 100644 --- a/mobile/src/transport/mobile-relay-orphan-cleanup.test.ts +++ b/mobile/src/transport/mobile-relay-orphan-cleanup.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { scheduleOrphanedMobileRelayCleanup } from './mobile-relay-orphan-cleanup' describe('mobile relay orphan cleanup', () => { - it('durably schedules credential deletion before removing an orphan overlay pointer', async () => { + it('schedules each orphan credential cleanup before removing its overlay pointer', async () => { const order: string[] = [] const deleteCredential = vi.fn(async () => {}) const scheduleCleanup = vi.fn(async (hostId: string) => { @@ -23,7 +23,7 @@ describe('mobile relay orphan cleanup', () => { expect(scheduleCleanup).toHaveBeenCalledWith('host-1', deleteCredential) }) - it('retains the overlay pointer when durable cleanup scheduling fails', async () => { + it('retains the overlay pointer when the cleanup scheduler rejects', async () => { const removeOverlay = vi.fn(async () => {}) await scheduleOrphanedMobileRelayCleanup({ hostIds: ['host-1'], diff --git a/mobile/src/transport/mobile-relay-recovery-log.test.ts b/mobile/src/transport/mobile-relay-recovery-log.test.ts index e24173629b3..48a3bdac2af 100644 --- a/mobile/src/transport/mobile-relay-recovery-log.test.ts +++ b/mobile/src/transport/mobile-relay-recovery-log.test.ts @@ -1,14 +1,18 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { createRelayRecoveryLog } from './mobile-relay-recovery-log' import type { ConnectionLogEntry } from './types' +afterEach(() => vi.restoreAllMocks()) + describe('createRelayRecoveryLog', () => { it('uses distinct ids across logger instances', () => { + vi.spyOn(Date, 'now').mockReturnValue(1) const entries: ConnectionLogEntry[] = [] const sink = vi.fn((entry: ConnectionLogEntry) => entries.push(entry)) createRelayRecoveryLog(() => 1, sink)('first') createRelayRecoveryLog(() => 1, sink)('second') + expect(entries).toHaveLength(2) expect(entries[0]?.id).not.toBe(entries[1]?.id) }) }) diff --git a/mobile/src/transport/mobile-web-bundle-range-interop.test.ts b/mobile/src/transport/mobile-web-bundle-range-interop.test.ts index 97ce5780b94..869de6a5b05 100644 --- a/mobile/src/transport/mobile-web-bundle-range-interop.test.ts +++ b/mobile/src/transport/mobile-web-bundle-range-interop.test.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'node:crypto' +import { createHash } from 'node:crypto' import { gunzipSync } from 'fflate' import { describe, expect, it } from 'vitest' import { encodeMobileWebBundleRange } from '../../../src/main/runtime/rpc/methods/mobile-web-bundle-range-encoding' @@ -29,7 +29,11 @@ describe('a host-encoded range decoded by the phone', () => { }) it('leaves incompressible bytes as identity, which needs no decoder', async () => { - const raw = randomBytes(4096) + const raw = Buffer.concat( + Array.from({ length: 128 }, (_, index) => + createHash('sha256').update(`range-${index}`).digest() + ) + ) const encoded = await encodeMobileWebBundleRange(raw) expect(encoded.encoding).toBe('identity') diff --git a/mobile/src/transport/mobile-web-bundle-range-reply.test.ts b/mobile/src/transport/mobile-web-bundle-range-reply.test.ts index d231e5f4f66..09829c837b9 100644 --- a/mobile/src/transport/mobile-web-bundle-range-reply.test.ts +++ b/mobile/src/transport/mobile-web-bundle-range-reply.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import { runRpcOperation } from './rpc-operation' import { MOBILE_WEB_BUNDLE_CHUNK_BYTES, MOBILE_WEB_BUNDLE_RANGE_BYTES, @@ -69,10 +70,18 @@ describe('mobile web bundle range reply reader', () => { } }) - it('is a require-result read of the range method', () => { - expect(mobileWebBundleRangeRead.method).toBe(MOBILE_WEB_BUNDLE_RANGE_METHOD) - expect(mobileWebBundleRangeRead.acceptance).toBe('require-result-or-throw') - expect(mobileWebBundleRangeRead.barrier).toBe('on-settle') + it('requests a range and surfaces the host refusal', async () => { + const params = { buildId: 'a'.repeat(64), path: 'index.html', offset: 0 } + const sendRequest = vi.fn(async () => ({ + id: 'range-1', + ok: false as const, + error: { code: 'forbidden', message: 'no' } + })) + + await expect( + runRpcOperation({ sendRequest }, mobileWebBundleRangeRead, params) + ).rejects.toThrow('forbidden: no') + expect(sendRequest).toHaveBeenCalledWith(MOBILE_WEB_BUNDLE_RANGE_METHOD, params, undefined) }) }) diff --git a/src/renderer/src/lib/app-menu-selection-actions.test.ts b/src/renderer/src/lib/app-menu-selection-actions.test.ts index 488702a72a6..b807474d927 100644 --- a/src/renderer/src/lib/app-menu-selection-actions.test.ts +++ b/src/renderer/src/lib/app-menu-selection-actions.test.ts @@ -8,12 +8,20 @@ import { describe('app menu selection actions', () => { it.each(['copy', 'select-all'] as const)('reports whether %s has an owned target', (action) => { - const handler = (event: Event): void => event.preventDefault() + let receivedAction: unknown + const handler = (event: Event): void => { + if (event instanceof CustomEvent) { + receivedAction = event.detail + } + event.preventDefault() + } window.addEventListener(APP_MENU_SELECTION_ACTION_EVENT, handler) - expect(dispatchAppMenuSelectionAction(action)).toBe(true) + const claimed = dispatchAppMenuSelectionAction(action) window.removeEventListener(APP_MENU_SELECTION_ACTION_EVENT, handler) + expect(claimed).toBe(true) + expect(receivedAction).toBe(action) }) it('leaves unowned actions available for native fallback', () => { diff --git a/src/shared/automation-session-reuse.test.ts b/src/shared/automation-session-reuse.test.ts index 20537d2874c..a06a179ceef 100644 --- a/src/shared/automation-session-reuse.test.ts +++ b/src/shared/automation-session-reuse.test.ts @@ -37,13 +37,19 @@ function find( } = {} ): string | null { const livePtyIds = new Set(options.livePtyIds ?? runs.map((entry) => entry.terminalPtyId)) + const statusesByPaneKey = new Map( + runs.map((entry) => [ + entry.terminalPaneKey, + options.status ?? { state: 'done', agentType: 'claude' } + ]) + ) return findReusableAutomationRun({ automationId: 'auto-1', agentId: 'claude', worktreeId: 'wt-1', currentRunId: 'run-current', runs, - agentStatusForPane: () => options.status ?? { state: 'done', agentType: 'claude' }, + agentStatusForPane: (paneKey) => statusesByPaneKey.get(paneKey), liveSession: (candidate) => (livePtyIds.has(candidate.terminalPtyId) ? candidate.id : null) }) } diff --git a/src/shared/calm-status-reveal.test.ts b/src/shared/calm-status-reveal.test.ts index 60485634663..b00a432093f 100644 --- a/src/shared/calm-status-reveal.test.ts +++ b/src/shared/calm-status-reveal.test.ts @@ -33,6 +33,23 @@ describe('stepCalmStatusReveal', () => { it('says an outage that outlasts the delay, and holds it long enough to read', () => { const shownAt = CALM_STATUS_REVEAL_DELAY_MS + const pending = stepCalmStatusReveal(CALM_STATUS_IDLE, { + down: true, + immediate: false, + now: 0 + }) + expect(pending.recheckInMs).toBe(CALM_STATUS_REVEAL_DELAY_MS) + const shown = stepCalmStatusReveal(pending.next, { + down: true, + immediate: false, + now: shownAt + }) + const recovering = stepCalmStatusReveal(shown.next, { + down: false, + immediate: false, + now: shownAt + 100 + }) + expect(recovering.recheckInMs).toBe(CALM_STATUS_MIN_VISIBLE_MS - 100) expect( run([ { at: 0, down: true },