mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<string, string>())
|
||||
@@ -11,11 +11,16 @@ const asyncStorageMock = vi.hoisted(() => ({
|
||||
storage.delete(key)
|
||||
})
|
||||
}))
|
||||
const secrets = vi.hoisted(() => new Map<string, string>())
|
||||
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<void>
|
||||
|
||||
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({
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string[]> {
|
||||
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([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ const servers = new Set<ReturnType<typeof createServer>>()
|
||||
const sockets = new Set<Socket>()
|
||||
|
||||
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<void>()
|
||||
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<void>((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<void>((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 () => {
|
||||
|
||||
@@ -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<typeof actual.localBundledSkillInput>) => ({
|
||||
...(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()
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user