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:
OrcaWin
2026-10-25 05:16:25 -07:00
committed by GitHub
co-authored by OrcaWin
parent 0044d2c0b0
commit 7d18e19907
10 changed files with 171 additions and 137 deletions
-1
View File
@@ -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([])
})
})
+31 -18
View File
@@ -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 () => {
+5 -3
View File
@@ -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()
})
-11
View File
@@ -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'
)
})
})