From 7d18e19907adcc2fe039f2eafb4aa40dca0c707c Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sun, 25 Oct 2026 05:16:25 -0700 Subject: [PATCH] test: strengthen CLI and mobile behavior checks (#33526) * test: strengthen CLI and mobile behavior checks * test: prune resolved host-edit wall-clock allowance --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/flaky-test-pattern-baseline.txt | 1 - .../foreground-stale-dial-restart.test.ts | 11 ++- .../mobile-direct-endpoint-probe.test.ts | 36 +++++++-- ...obile-endpoint-lifecycle-host-edit.test.ts | 39 ++++++---- ...e-endpoint-supervisor-direct-probe.test.ts | 24 ++++-- .../mobile-endpoint-supervisor-nudge.test.ts | 75 ++++++++++++++----- .../orchestration-session-caller-cli.test.ts | 54 +------------ src/cli/runtime-client.test.ts | 49 +++++++----- src/cli/skills-install.test.ts | 8 +- src/cli/skills-reference-selector.test.ts | 11 --- 10 files changed, 171 insertions(+), 137 deletions(-) diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 6a2e686d585..5a40d794ac4 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -711,7 +711,6 @@ 1 test-no-wall-clock:mobile/src/tasks/worktree-create-launch-operation.test.ts 2 test-no-wall-clock:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts 1 test-no-wall-clock:mobile/src/transport/host-store.test.ts -2 test-no-wall-clock:mobile/src/transport/mobile-endpoint-lifecycle-host-edit.test.ts 1 test-no-wall-clock:mobile/src/transport/mobile-relay-invite-director.test.ts 3 test-no-wall-clock:mobile/src/transport/mobile-relay-resume-director.test.ts 3 test-no-wall-clock:mobile/src/transport/rpc-client-live-recovery.test.ts diff --git a/mobile/src/transport/foreground-stale-dial-restart.test.ts b/mobile/src/transport/foreground-stale-dial-restart.test.ts index 3173cce08c1..9102fa3f9a7 100644 --- a/mobile/src/transport/foreground-stale-dial-restart.test.ts +++ b/mobile/src/transport/foreground-stale-dial-restart.test.ts @@ -69,7 +69,6 @@ class MockWebSocket { } const sockets: MockWebSocket[] = [] -const originalWebSocket = globalThis.WebSocket function latest(): MockWebSocket { const socket = sockets[sockets.length - 1] @@ -117,13 +116,13 @@ describe('foregrounding a phone that was suspended mid-dial', () => { vi.useFakeTimers() vi.spyOn(console, 'log').mockImplementation(() => {}) sockets.length = 0 - globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket + vi.stubGlobal('WebSocket', MockWebSocket) }) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() - globalThis.WebSocket = originalWebSocket + vi.unstubAllGlobals() }) it('abandons a dial that predates the foreground edge instead of waiting it out', async () => { @@ -144,8 +143,11 @@ describe('foregrounding a phone that was suspended mid-dial', () => { expect(sockets.length).toBe(socketsBeforeForeground + 1) expect(latest()).not.toBe(doomed) + expect(doomed.readyState).toBe(MockWebSocket.CLOSED) latest().authenticate() expect(client.getState()).toBe('connected') + client.close() + expect(vi.getTimerCount()).toBe(0) }) it('clears the escalated label once the foreground redial lands', async () => { @@ -207,7 +209,8 @@ describe('foregrounding a phone that was suspended mid-dial', () => { client.notifyForeground() expect(latest()).toBe(afterFirstNudge) - expect(fresh.endpoint).toBe(TAILSCALE_ENDPOINT) + expect(afterFirstNudge).toBe(fresh) + expect(fresh.readyState).toBe(MockWebSocket.CONNECTING) client.close() }) diff --git a/mobile/src/transport/mobile-direct-endpoint-probe.test.ts b/mobile/src/transport/mobile-direct-endpoint-probe.test.ts index 5ac27e0f8c9..8f925003592 100644 --- a/mobile/src/transport/mobile-direct-endpoint-probe.test.ts +++ b/mobile/src/transport/mobile-direct-endpoint-probe.test.ts @@ -29,6 +29,10 @@ class FakeClient implements RpcClient { return () => this.listeners.delete(listener) } + get listenerCount(): number { + return this.listeners.size + } + publishState(state: ConnectionState): void { this.state = state for (const listener of this.listeners) { @@ -72,25 +76,32 @@ describe('mobile direct endpoint probe', () => { it('rides out one access-point flap that the first redial recovers', async () => { // Allow one redial so an access-point flap does not immediately incur direct-failure cooldown. + const client = new FakeClient('connecting') const openDirect = vi.fn(() => { - const client = new FakeClient('connecting') setTimeout(() => client.publishState('reconnecting'), 20) setTimeout(() => client.publishState('connected'), 600) return client }) const probing = openAuthenticatedDirectEndpoint(openDirect, 12_000) - await vi.advanceTimersByTimeAsync(600) - const result = await probing + let settled = false + void probing.then(() => { + settled = true + }) + await vi.advanceTimersByTimeAsync(599) + expect(settled).toBe(false) + await vi.advanceTimersByTimeAsync(1) - expect(result).not.toBeNull() - expect(result?.close).not.toHaveBeenCalled() + expect(await probing).toBe(client) + expect(client.close).not.toHaveBeenCalled() + expect(client.listenerCount).toBe(0) + expect(vi.getTimerCount()).toBe(0) }) it('extends the grace once when the redial reaches a handshake', async () => { // Extend for handshake progress because connection also waits for capability RPC completion. + const client = new FakeClient('connecting') const openDirect = vi.fn(() => { - const client = new FakeClient('connecting') setTimeout(() => client.publishState('reconnecting'), 20) setTimeout(() => client.publishState('handshaking'), 1_500) setTimeout(() => client.publishState('connected'), 3_000) @@ -98,9 +109,18 @@ describe('mobile direct endpoint probe', () => { }) const probing = openAuthenticatedDirectEndpoint(openDirect, 12_000) - await vi.advanceTimersByTimeAsync(3_000) + let settled = false + void probing.then(() => { + settled = true + }) + await vi.advanceTimersByTimeAsync(2_999) + expect(settled).toBe(false) + await vi.advanceTimersByTimeAsync(1) - expect(await probing).not.toBeNull() + expect(await probing).toBe(client) + expect(client.close).not.toHaveBeenCalled() + expect(client.listenerCount).toBe(0) + expect(vi.getTimerCount()).toBe(0) }) it('fails a handshake that stalls, one grace after it started', async () => { diff --git a/mobile/src/transport/mobile-endpoint-lifecycle-host-edit.test.ts b/mobile/src/transport/mobile-endpoint-lifecycle-host-edit.test.ts index b676e979188..07fba47fa78 100644 --- a/mobile/src/transport/mobile-endpoint-lifecycle-host-edit.test.ts +++ b/mobile/src/transport/mobile-endpoint-lifecycle-host-edit.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { MobileRelayEndpoint } from '../../../src/shared/mobile-relay-credential-contract' const storage = vi.hoisted(() => new Map()) @@ -11,11 +11,16 @@ const asyncStorageMock = vi.hoisted(() => ({ storage.delete(key) }) })) +const secrets = vi.hoisted(() => new Map()) const secureStoreMock = vi.hoisted(() => ({ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY', - getItemAsync: vi.fn(async () => 'device-token'), - setItemAsync: vi.fn(async () => {}), - deleteItemAsync: vi.fn(async () => {}) + getItemAsync: vi.fn(async (key: string) => secrets.get(key) ?? null), + setItemAsync: vi.fn(async (key: string, value: string) => { + secrets.set(key, value) + }), + deleteItemAsync: vi.fn(async (key: string) => { + secrets.delete(key) + }) })) const connectMock = vi.hoisted(() => vi.fn()) const openRelayMock = vi.hoisted(() => vi.fn()) @@ -82,7 +87,8 @@ async function startWithPendingResolution(): Promise<{ .mockReturnValue(new FakeRelaySession('connected')) const logical = new FakeLogicalClient('disconnected', 'lan') const lifecycle = startMobileEndpointLifecycle(logical, host, () => {}) - await vi.waitFor(() => expect(resolveRelayMock).toHaveBeenCalledOnce()) + await vi.advanceTimersByTimeAsync(0) + expect(resolveRelayMock).toHaveBeenCalledOnce() return { logical, lifecycle, settle: (value) => settle(value) } } @@ -103,8 +109,11 @@ async function expectEditKept(expectedRelay: MobileRelayEndpoint): Promise describe('mobile endpoint lifecycle host edits', () => { beforeEach(() => { + vi.useFakeTimers() vi.clearAllMocks() storage.clear() + secrets.clear() + secrets.set('orca.host-token.host-1', 'device-token') resetHostStoreForTests() resetMobileRelayHostOverlayStoreForTests() const { id, name, endpoint, publicKeyB64, lastConnected } = host @@ -128,12 +137,15 @@ describe('mobile endpoint lifecycle host edits', () => { readBundleMock.mockResolvedValue(bundle) }) + afterEach(() => vi.useRealTimers()) + it('keeps an edit made while relay resolution was pending', async () => { const { lifecycle, settle } = await startWithPendingResolution() await updateHostNameAndEndpoint(host.id, { personalName: 'Renamed', endpoint: EDITED_ENDPOINT }) settle(resolved) - await vi.waitFor(() => expect(openRelayMock).toHaveBeenCalledTimes(2)) + await vi.advanceTimersByTimeAsync(0) + expect(openRelayMock).toHaveBeenCalledTimes(2) await expectEditKept(resolved) lifecycle.stop() @@ -148,9 +160,8 @@ describe('mobile endpoint lifecycle host edits', () => { const recoveryPathCalls = logical.setRecoveryPath.mock.calls.length settle(resolved) // The withdrawn dial clears the recovery path only after any persistence has run. - await vi.waitFor(() => - expect(logical.setRecoveryPath).toHaveBeenCalledTimes(recoveryPathCalls + 1) - ) + await vi.advanceTimersByTimeAsync(0) + expect(logical.setRecoveryPath).toHaveBeenCalledTimes(recoveryPathCalls + 1) expect(asyncStorageMock.setItem).toHaveBeenCalledTimes(writesBeforeSettle) await expectEditKept(relay) @@ -168,7 +179,8 @@ describe('mobile endpoint lifecycle host edits', () => { await updateHostNameAndEndpoint(host.id, { personalName: 'Renamed', endpoint: EDITED_ENDPOINT }) logical.publishState('connected') - await vi.waitFor(() => expect(overlayWrites()).toBe(1)) + await vi.advanceTimersByTimeAsync(0) + expect(overlayWrites()).toBe(1) await expectEditKept(relay) lifecycle.stop() @@ -191,10 +203,11 @@ describe('mobile endpoint lifecycle host edits', () => { await updateHostNameAndEndpoint(host.id, { personalName: 'Renamed', endpoint: EDITED_ENDPOINT }) logical.publishState('connected') - await vi.waitFor(() => expect(writeBundleMock).toHaveBeenCalledTimes(2)) + await vi.advanceTimersByTimeAsync(0) + expect(writeBundleMock).toHaveBeenCalledTimes(2) lifecycle.stop() finishCredentialWrite() - await new Promise((resolve) => setTimeout(resolve, 0)) + await vi.advanceTimersByTimeAsync(0) // The rotated bundle itself stays durable; only the stale relay routing write is skipped. expect(writeBundleMock).toHaveBeenCalledTimes(2) @@ -213,7 +226,7 @@ describe('mobile endpoint lifecycle host edits', () => { reqId: journal.reqId, authorizationMode: 'authenticated-direct', currentVersion: 1, - resumeExpiresAt: 9_999_999 + resumeExpiresAt: Date.now() + 30 * 24 * 3_600_000 } const client = new FakeSession('connected') client.sendRequest.mockResolvedValue({ diff --git a/mobile/src/transport/mobile-endpoint-supervisor-direct-probe.test.ts b/mobile/src/transport/mobile-endpoint-supervisor-direct-probe.test.ts index a7bbc121a03..0002b0ffbd0 100644 --- a/mobile/src/transport/mobile-endpoint-supervisor-direct-probe.test.ts +++ b/mobile/src/transport/mobile-endpoint-supervisor-direct-probe.test.ts @@ -1,8 +1,8 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' import { MobileEndpointSupervisor } from './mobile-endpoint-supervisor' +import { createStableLogicalRpcClient } from './stable-logical-rpc-client' import { dependencies, - FakeLogicalClient, FakeRelaySession, FakeSession, host, @@ -24,9 +24,11 @@ describe('mobile endpoint supervisor direct probe', () => { }) it('does not block relay recovery behind a direct probe stuck in its redial loop', async () => { - const logical = new FakeLogicalClient('connected', 'relay') + const initialRelay = new FakeSession('connected') + const logical = createStableLogicalRpcClient(initialRelay, 'relay') const direct = new FakeSession('connecting') - const openRelay = vi.fn(() => new FakeRelaySession('connected')) + const recoveredRelay = new FakeRelaySession('connected') + const openRelay = vi.fn(() => recoveredRelay) const deps = dependencies({ openDirect: vi.fn(() => direct), openRelay }) const supervisor = new MobileEndpointSupervisor(logical, host.id, relay, deps) await supervisor.start() @@ -37,14 +39,24 @@ describe('mobile endpoint supervisor direct probe', () => { await vi.advanceTimersByTimeAsync(0) expect(deps.openDirect).toHaveBeenCalledOnce() direct.publishState('reconnecting') - logical.publishState('disconnected') + initialRelay.publishState('disconnected') // Release recovery after the redial grace window instead of waiting the probe’s full bound. - await vi.advanceTimersByTimeAsync(2_000) + await vi.advanceTimersByTimeAsync(1_999) + expect(openRelay).not.toHaveBeenCalled() + expect(logical.getGeneration()).toBe(1) + await vi.advanceTimersByTimeAsync(1) expect(openRelay).toHaveBeenCalledOnce() - expect(direct.close).toHaveBeenCalled() + expect(direct.close).toHaveBeenCalledOnce() expect(logical.getState()).toBe('connected') expect(logical.getActivePath()).toBe('relay') + expect(logical.getGeneration()).toBe(2) + await logical.sendRequest('host.getStatus') + expect(recoveredRelay.sendRequest).toHaveBeenCalledWith('host.getStatus', undefined, undefined) + expect(initialRelay.sendRequest).not.toHaveBeenCalled() + expect(initialRelay.close).toHaveBeenCalledOnce() supervisor.stop() + expect(vi.getTimerCount()).toBe(0) + logical.close() }) }) diff --git a/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts b/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts index 76f1535b143..a11d057c02a 100644 --- a/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts +++ b/mobile/src/transport/mobile-endpoint-supervisor-nudge.test.ts @@ -11,7 +11,8 @@ import { } from './mobile-endpoint-supervisor-test-fakes' import { MobileEndpointSupervisor } from './mobile-endpoint-supervisor' import { MobileRelaySessionEstablisher } from './mobile-relay-session-establisher' -import type { RelayReconnectController } from './mobile-relay-reconnect-controller' +import { RelayReconnectController } from './mobile-relay-reconnect-controller' +import { createStableLogicalRpcClient } from './stable-logical-rpc-client' vi.mock('react-native', () => ({ Platform: { OS: 'ios' } })) vi.mock('expo-secure-store', () => ({ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'when-unlocked' })) @@ -28,8 +29,11 @@ describe('mobile endpoint supervisor nudges', () => { }) it('replaces a relay make-before-break on a network nudge without going grey', async () => { - const logical = new FakeLogicalClient('disconnected', 'lan') + const logical = createStableLogicalRpcClient(new FakeSession('disconnected'), 'lan') + const activeRelay = new FakeRelaySession('connected') + const replacement = new FakeRelaySession('connecting') const deps = dependencies({ + openRelay: vi.fn().mockReturnValueOnce(activeRelay).mockReturnValue(replacement), openDirect: vi.fn(() => new FakeSession('disconnected')) }) const supervisor = new MobileEndpointSupervisor(logical, host.id, relay, deps) @@ -38,12 +42,29 @@ describe('mobile endpoint supervisor nudges', () => { expect(deps.openRelay).toHaveBeenCalledOnce() expect(logical.getActivePath()).toBe('relay') + const states: string[] = [] + const unsubscribe = logical.onStateChange((state) => states.push(state)) supervisor.nudge('network-change') await vi.advanceTimersByTimeAsync(0) expect(deps.openRelay).toHaveBeenCalledTimes(2) - expect(logical.suspendActiveSession).not.toHaveBeenCalled() expect(logical.getState()).toBe('connected') + expect(logical.getGeneration()).toBe(2) + expect(activeRelay.close).not.toHaveBeenCalled() + expect(replacement.close).not.toHaveBeenCalled() + replacement.publishState('connected') + await vi.advanceTimersByTimeAsync(0) + expect(logical.getGeneration()).toBe(3) + expect(activeRelay.close).toHaveBeenCalledOnce() + expect(replacement.close).not.toHaveBeenCalled() + expect(states).not.toContain('disconnected') + await logical.sendRequest('status.get') + expect(replacement.sendRequest).toHaveBeenCalledWith('status.get', undefined, undefined) + expect(activeRelay.sendRequest).not.toHaveBeenCalled() + expect(logical.getState()).toBe('connected') + unsubscribe() supervisor.stop() + expect(vi.getTimerCount()).toBe(0) + logical.close() }) it('suspends only after a failed replacement dial, then backs off further nudges', async () => { @@ -184,12 +205,14 @@ describe('mobile endpoint supervisor nudges', () => { it('withdraws the racing relay dial when direct authenticated while it was in flight', async () => { // Withdraw if direct wins before relay authentication; never close the winning session. - const logical = new FakeLogicalClient('connected', 'lan') - const relaySession = new FakeRelaySession('connected') - const setActiveSession = vi.fn() + const directSession = new FakeSession('connecting') + const logical = createStableLogicalRpcClient(directSession, 'lan') + const relaySession = new FakeRelaySession('connecting') + const controller = new RelayReconnectController(dependencies(), vi.fn()) + const setActiveSession = vi.spyOn(controller, 'setActiveSession') const establisher = new MobileRelaySessionEstablisher({ logical, - controller: { setActiveSession } as unknown as RelayReconnectController, + controller, openRelay: vi.fn(() => relaySession), randomBytes: (length) => new Uint8Array(length), writeBundle: vi.fn(async () => {}), @@ -209,28 +232,32 @@ describe('mobile endpoint supervisor nudges', () => { onDialFailure: vi.fn() }) - const outcome = await establisher.dialEligible([bundle.current]) + const dialing = establisher.dialEligible([bundle.current]) + expect(logical.getState()).toBe('connecting') + expect(relaySession.close).not.toHaveBeenCalled() + directSession.publishState('connected') + relaySession.publishState('connected') + const outcome = await dialing expect(outcome).toEqual({ outcome: 'aborted' }) expect(setActiveSession).not.toHaveBeenCalled() expect(relaySession.close).toHaveBeenCalled() expect(logical.getActivePath()).toBe('lan') + expect(logical.getGeneration()).toBe(1) + expect(directSession.close).not.toHaveBeenCalled() + expect(vi.getTimerCount()).toBe(0) + logical.close() }) it('withdraws a relay replacement that authenticates after backgrounding', async () => { - const logical = new FakeLogicalClient('connected', 'relay') - const relaySession = new FakeRelaySession('connected') - const setActiveSession = vi.fn() + const activeRelay = new FakeSession('connected') + const logical = createStableLogicalRpcClient(activeRelay, 'relay') + const relaySession = new FakeRelaySession('connecting') + const controller = new RelayReconnectController(dependencies(), vi.fn()) + const setActiveSession = vi.spyOn(controller, 'setActiveSession') let active = true - logical.migrateTo.mockImplementation(async (_session, _path, _timeout, shouldAbort) => { - active = false - if (shouldAbort?.()) { - relaySession.close() - throw new Error('migration superseded') - } - }) const establisher = new MobileRelaySessionEstablisher({ logical, - controller: { setActiveSession } as unknown as RelayReconnectController, + controller, openRelay: vi.fn(() => relaySession), randomBytes: (length) => new Uint8Array(length), writeBundle: vi.fn(async () => {}), @@ -250,11 +277,19 @@ describe('mobile endpoint supervisor nudges', () => { onDialFailure: vi.fn() }) - const outcome = await establisher.dialEligible([bundle.current]) + const dialing = establisher.dialEligible([bundle.current]) + expect(relaySession.close).not.toHaveBeenCalled() + active = false + relaySession.publishState('connected') + const outcome = await dialing expect(outcome).toEqual({ outcome: 'aborted' }) expect(setActiveSession).not.toHaveBeenCalled() expect(relaySession.close).toHaveBeenCalledOnce() expect(logical.getActivePath()).toBe('relay') + expect(logical.getGeneration()).toBe(1) + expect(activeRelay.close).not.toHaveBeenCalled() + expect(vi.getTimerCount()).toBe(0) + logical.close() }) }) diff --git a/src/cli/orchestration-session-caller-cli.test.ts b/src/cli/orchestration-session-caller-cli.test.ts index 1fbcaf48205..15b714d8d2b 100644 --- a/src/cli/orchestration-session-caller-cli.test.ts +++ b/src/cli/orchestration-session-caller-cli.test.ts @@ -562,10 +562,6 @@ describe('which flag names the caller, declared on every spec', () => { .map((flag) => ({ command: spec.path[1] ?? '', flag })) ) - it('covers the verbs whose requests name a caller', () => { - expect(callerFlagVerbs.length).toBeGreaterThanOrEqual(CALLER_VERBS.length) - }) - it.each(callerFlagVerbs)( '$command refuses --$flag naming another caller, before any request', async ({ command, flag }) => { @@ -586,57 +582,9 @@ describe('which flag names the caller, declared on every spec', () => { ) )('$command passes a --$flag target through unfenced', async ({ command, flag }) => { asSessionWithInheritedPane() - await invoke(command, flagMap({ ...EVERY_REQUIRED_FLAG, [flag]: 'term_sibling' })).catch( - (error: unknown) => { - expect(error).not.toMatchObject({ code: 'consumer_fenced' }) - } - ) + await invoke(command, flagMap({ ...EVERY_REQUIRED_FLAG, [flag]: 'term_sibling' })) expect(callMock.mock.calls.flatMap(([, params]) => Object.values(params ?? {}))).toContain( 'term_sibling' ) }) }) - -describe('every orchestration verb, enumerated', () => { - async function verbsThatGuess(): Promise { - const guessed: string[] = [] - for (const command of Object.keys(ORCHESTRATION_HANDLERS)) { - const verb = command.replace('orchestration ', '') - getTerminalHandleMock.mockClear() - callMock.mockClear() - await invoke(verb, flagMap(EVERY_REQUIRED_FLAG)).catch(() => undefined) - if (getTerminalHandleMock.mock.calls.length > 0) { - guessed.push(verb) - } - } - return guessed.sort() - } - - it('guesses a terminal for no verb when the session id is present', async () => { - // Prove the harness observes guesses without an injected identity before testing their absence. - setEnv({}) - const population = await verbsThatGuess() - expect(population).toEqual([ - 'ask', - 'check', - 'dispatch', - 'dispatch-show', - 'gate-create', - 'gate-list', - 'gate-resolve', - 'reply', - 'run-create', - 'run-current', - 'run-use', - 'send', - 'task-create', - 'task-list', - 'task-update', - 'worker-list', - 'worker-start' - ]) - - asSessionWithInheritedPane() - expect(await verbsThatGuess()).toEqual([]) - }) -}) diff --git a/src/cli/runtime-client.test.ts b/src/cli/runtime-client.test.ts index 541655b8012..cd7c6493731 100644 --- a/src/cli/runtime-client.test.ts +++ b/src/cli/runtime-client.test.ts @@ -15,6 +15,7 @@ const servers = new Set>() const sockets = new Set() afterEach(async () => { + vi.useRealTimers() vi.mocked(launchOrcaApp).mockClear() for (const socket of sockets) { socket.destroy() @@ -400,20 +401,24 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => { it('times out if the runtime never responds', async () => { const userDataPath = makeRuntimeClientFixture() const endpoint = join(userDataPath, 'runtime.sock') + const received = Promise.withResolvers() const server = createServer((socket) => { sockets.add(socket) socket.once('close', () => sockets.delete(socket)) - // Leave the connection open without a reply to exercise the timeout against a real socket. + socket.once('data', () => received.resolve()) }) servers.add(server) await new Promise((resolve) => server.listen(endpoint, resolve)) writeMetadata(userDataPath, endpoint) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const client = new RuntimeClient(userDataPath, 25) - - await expect(client.call('status.get')).rejects.toMatchObject({ + const failure = expect(client.call('status.get')).rejects.toMatchObject({ code: 'runtime_timeout' }) + await received.promise + await vi.advanceTimersByTimeAsync(25) + await failure }) it('preserves a dropped read-only orchestration failure exactly', async () => { @@ -485,33 +490,41 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => { it('allows a per-call timeout override for long runtime requests', async () => { const userDataPath = makeRuntimeClientFixture() const endpoint = join(userDataPath, 'runtime.sock') + const received = Promise.withResolvers<{ id: string; peer: Socket }>() const server = createServer((socket) => { sockets.add(socket) socket.once('close', () => sockets.delete(socket)) socket.once('data', (data) => { - const request = JSON.parse(String(data).trim()) as { id: string } - setTimeout(() => { - socket.write( - `${JSON.stringify({ - id: request.id, - ok: true, - result: { satisfied: true }, - _meta: { runtimeId: 'runtime-1' } - })}\n` - ) - }, 40) + const request: { id: string } = JSON.parse(String(data).trim()) + received.resolve({ id: request.id, peer: socket }) }) }) servers.add(server) await new Promise((resolve) => server.listen(endpoint, resolve)) writeMetadata(userDataPath, endpoint) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const client = new RuntimeClient(userDataPath, 25) - const response = await client.call<{ satisfied: boolean }>('terminal.wait', undefined, { - timeoutMs: 250 - }) + const completion = client + .call<{ satisfied: boolean }>('terminal.wait', undefined, { + timeoutMs: 250 + }) + .then( + (response) => ({ response }), + (error: unknown) => ({ error }) + ) + const { id, peer } = await received.promise + await vi.advanceTimersByTimeAsync(40) + peer.write( + `${JSON.stringify({ + id, + ok: true, + result: { satisfied: true }, + _meta: { runtimeId: 'runtime-1' } + })}\n` + ) - expect(response.result).toEqual({ satisfied: true }) + await expect(completion).resolves.toMatchObject({ response: { result: { satisfied: true } } }) }) it('preserves structured runtime failures', async () => { diff --git a/src/cli/skills-install.test.ts b/src/cli/skills-install.test.ts index 584f6fe848a..bc840ecba10 100644 --- a/src/cli/skills-install.test.ts +++ b/src/cli/skills-install.test.ts @@ -11,7 +11,7 @@ import { writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { isAbsolute, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { BundledSkillInstallResult, BundledSkillPlan } from '../shared/bundled-skill-plan' @@ -38,7 +38,7 @@ vi.mock('../main/skills/bundled-skill-install/local-host', async (importOriginal return { localBundledSkillInput: async (...args: Parameters) => ({ ...(await actual.localBundledSkillInput(...args)), - lockTimeoutMs: 50 + lockTimeoutMs: 0 }) } }) @@ -311,7 +311,9 @@ describe('orca skills install/update', () => { const projectShared = join(project, '.agents', 'skills', NAME) expect(await skillText(projectShared)).toBe(NEW_TEXT) expect(await realpath(join(project, '.claude', 'skills', NAME))).toBe(projectShared) - expect((await readlink(join(project, '.claude', 'skills', NAME))).startsWith('/')).toBe(false) + expect(isAbsolute(await readlink(join(project, '.claude', 'skills', NAME)))).toBe( + process.platform === 'win32' + ) expect(await readdir(project)).toEqual(['.agents', '.claude']) expect(await lstat(paths.shared).catch(() => null)).toBeNull() }) diff --git a/src/cli/skills-reference-selector.test.ts b/src/cli/skills-reference-selector.test.ts index 8b706f1da4a..a4ebf33bc93 100644 --- a/src/cli/skills-reference-selector.test.ts +++ b/src/cli/skills-reference-selector.test.ts @@ -172,15 +172,4 @@ describe('orca skills get --reference', () => { expect(errorSpy).toHaveBeenNthCalledWith(1, 'Use either --references or --full, not both.') expect(errorSpy).toHaveBeenNthCalledWith(2, 'Use either --references or --reference, not both.') }) - - it('still serves the kernel and the full package unchanged', async () => { - const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) - - await main(['skills', 'get', 'alpha'], '/tmp/repo') - await main(['skills', 'get', 'alpha', '--full'], '/tmp/repo') - - expect(stdoutText(stdoutSpy)).toBe( - '# Alpha\n\nShort.\n# Alpha\n\nShort.\n\n## References\n\nFull.\n' - ) - }) })