/* eslint-disable max-lines -- Why: this integration-style RPC test keeps the request/response contract together so regressions in the external CLI surface are easier to spot. */ import { existsSync, mkdirSync, mkdtempSync } from 'node:fs' import { rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createConnection, type Socket } from 'node:net' import { EventEmitter } from 'node:events' import { describe, expect, it, vi } from 'vitest' import WebSocket from 'ws' import Database from '../sqlite/sync-database' import { OrcaRuntimeService } from './orca-runtime' import { OrchestrationDb } from './orchestration/db' import * as runtimeMetadataModule from './runtime-metadata' import { readRuntimeMetadata, writeRuntimeMetadata } from './runtime-metadata' import { createRuntimeTransportMetadata, OrcaRuntimeRpcServer } from './runtime-rpc' import { parsePairingCode } from '../../shared/pairing' import { subscribeRemoteRuntimeRequest } from '../../shared/remote-runtime-client' import { TerminalStreamOpcode, decodeTerminalStreamFrame, decodeTerminalStreamText, encodeTerminalStreamFrame, encodeTerminalStreamJson, encodeTerminalStreamText } from '../../shared/terminal-stream-protocol' import { decrypt, deriveSharedKey, encrypt, generateKeyPair } from './rpc/e2ee-crypto' import { WebSocketTransport } from './rpc/ws-transport' import { DeviceRegistry } from './device-registry' import { DEVICE_REGISTRY_FILENAME, E2EE_KEYPAIR_FILENAME } from './mobile-pairing-files' import { ORCHESTRATION_CONTRACT_VERSION } from '../../shared/protocol-version' vi.mock('../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue([ { path: '/tmp/worktree-a', head: 'abc', branch: 'feature/foo', isBare: false, isMainWorktree: false } ]), listWorktreesStrict: vi.fn().mockResolvedValue([]) })) async function sendRequest( endpoint: string, request: Record ): Promise> { return await new Promise((resolve, reject) => { const socket = createConnection(endpoint) let buffer = '' socket.setEncoding('utf8') socket.once('error', reject) socket.on('data', (chunk: string) => { buffer += chunk const newlineIndex = buffer.indexOf('\n') if (newlineIndex === -1) { return } const message = buffer.slice(0, newlineIndex) socket.end() resolve(JSON.parse(message) as Record) }) socket.on('connect', () => { socket.write(`${JSON.stringify(withCurrentOrchestrationContract(request))}\n`) }) }) } // Why: long-poll keepalive tests need every frame, not just the first, because // we need to count `_keepalive` frames before the terminal success/failure. // Also exposes the socket so tests can close it mid-wait to exercise the // long-poll counter decrement path. type FramedSession = { socket: Socket frames: Record[] done: Promise } function openFramedSession(endpoint: string, request: Record): FramedSession { const frames: Record[] = [] const socket = createConnection(endpoint) let buffer = '' socket.setEncoding('utf8') const done = new Promise((resolve, reject) => { socket.once('error', (err) => { // Why: ECONNRESET is expected when we deliberately destroy the socket // mid-wait to probe the counter decrement; surface other errors. if ((err as NodeJS.ErrnoException).code === 'ECONNRESET') { resolve() return } reject(err) }) socket.on('close', () => resolve()) socket.on('data', (chunk: string) => { buffer += chunk let newlineIndex = buffer.indexOf('\n') while (newlineIndex !== -1) { const raw = buffer.slice(0, newlineIndex).trim() buffer = buffer.slice(newlineIndex + 1) if (raw) { const frame = JSON.parse(raw) as Record frames.push(frame) // Why: the server leaves the socket open after writing the terminal // frame (short RPCs expect the client to close); close the client // side so `done` resolves once we've captured the response. if (frame._keepalive !== true) { socket.end() } } newlineIndex = buffer.indexOf('\n') } }) socket.on('connect', () => { socket.write(`${JSON.stringify(withCurrentOrchestrationContract(request))}\n`) }) }) return { socket, frames, done } } function withCurrentOrchestrationContract( request: Record ): Record { return typeof request.method === 'string' && request.method.startsWith('orchestration.') ? { ...request, orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION } : request } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } async function waitFor(predicate: () => boolean, timeoutMs = 2_000): Promise { const start = Date.now() while (!predicate()) { if (Date.now() - start > timeoutMs) { throw new Error('timed out waiting for condition') } await sleep(20) } } function seedSupervisedAskWorkers(db: OrchestrationDb, workerHandles: string[]): void { const run = db.createRun({ objective: 'Exercise ask admission', coordinatorHandle: 'term_coord', coordinatorPaneKey: 'tab_coord:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' }) for (const workerHandle of workerHandles) { const task = db.createTask({ spec: 'Wait for coordinator input', runId: run.id }) db.createDispatchContext(task.id, workerHandle) } } function connectWs(endpoint: string): Promise { return new Promise((resolve, reject) => { const ws = new WebSocket(endpoint) ws.once('open', () => resolve(ws)) ws.once('error', reject) }) } function nextWsMessage(ws: WebSocket): Promise { return new Promise((resolve) => { ws.once('message', (data) => { resolve(typeof data === 'string' ? data : data.toString('utf-8')) }) }) } function waitForWsClose(ws: WebSocket): Promise { return new Promise((resolve) => { if (ws.readyState === ws.CLOSED) { resolve() return } ws.once('close', () => resolve()) }) } type AuthenticatedMobileWs = { ws: WebSocket sharedKey: Uint8Array } async function authenticateMobileWsSession(pairingUrl: string): Promise { const parsed = parsePairingCode(pairingUrl) expect(parsed).toBeTruthy() const ws = await connectWs(parsed!.endpoint) const mobileKeys = generateKeyPair() const serverPublicKey = Uint8Array.from(Buffer.from(parsed!.publicKeyB64, 'base64')) const sharedKey = deriveSharedKey(mobileKeys.secretKey, serverPublicKey) ws.send( JSON.stringify({ type: 'e2ee_hello', publicKeyB64: Buffer.from(mobileKeys.publicKey).toString('base64') }) ) expect(JSON.parse(await nextWsMessage(ws))).toEqual({ type: 'e2ee_ready' }) ws.send( encrypt(JSON.stringify({ type: 'e2ee_auth', deviceToken: parsed!.deviceToken }), sharedKey) ) expect(JSON.parse(decrypt(await nextWsMessage(ws), sharedKey)!)).toEqual({ type: 'e2ee_authenticated' }) return { ws, sharedKey } } async function authenticateMobileWs(pairingUrl: string): Promise { return (await authenticateMobileWsSession(pairingUrl)).ws } function sendEncryptedWsRequest( session: AuthenticatedMobileWs, request: Record ): void { session.ws.send(encrypt(JSON.stringify(request), session.sharedKey)) } function createEncryptedWsResponseReader(session: AuthenticatedMobileWs): { next: ( id: string, predicate?: (response: Record) => boolean ) => Promise> dispose: () => void } { type Waiter = { id: string predicate: (response: Record) => boolean resolve: (response: Record) => void } const queue: Record[] = [] const waiters: Waiter[] = [] const takeQueued = ( id: string, predicate: (response: Record) => boolean ): Record | null => { const index = queue.findIndex((response) => response.id === id && predicate(response)) if (index === -1) { return null } const [response] = queue.splice(index, 1) return response ?? null } const onMessage = (data: WebSocket.RawData): void => { const decrypted = decrypt( typeof data === 'string' ? data : data.toString('utf-8'), session.sharedKey ) expect(decrypted).toBeTruthy() const response = JSON.parse(decrypted!) as Record const waiterIndex = waiters.findIndex( (waiter) => response.id === waiter.id && waiter.predicate(response) ) if (waiterIndex === -1) { queue.push(response) return } const [waiter] = waiters.splice(waiterIndex, 1) waiter?.resolve(response) } session.ws.on('message', onMessage) return { next: (id: string, predicate: (response: Record) => boolean = () => true) => { const queued = takeQueued(id, predicate) if (queued) { return Promise.resolve(queued) } return new Promise>((resolve) => { waiters.push({ id, predicate, resolve }) }) }, dispose: () => { session.ws.off('message', onMessage) waiters.length = 0 queue.length = 0 } } } class FakeWebSocket extends EventEmitter { readonly OPEN = 1 readyState = this.OPEN } describe('OrcaRuntimeRpcServer', () => { const makeStore = (overrides?: { isUnread?: boolean }) => ({ getRepo: (id: string) => makeStore(overrides) .getRepos() .find((repo) => repo.id === id), getRepos: () => [ { id: 'repo-1', path: '/tmp/repo', displayName: 'repo', badgeColor: 'blue', addedAt: 1 } ], addRepo: () => {}, updateRepo: (id: string, updates: Record) => ({ ...makeStore(overrides).getRepo(id), ...updates }) as never, getAllWorktreeMeta: () => ({ 'repo-1::/tmp/worktree-a': { displayName: 'foo', comment: '', linkedIssue: 123, linkedPR: null, linkedLinearIssue: null, isArchived: false, isUnread: overrides?.isUnread ?? false, isPinned: false, sortOrder: 0, lastActivityAt: 0 } }), getWorktreeMeta: (worktreeId: string) => worktreeId === 'repo-1::/tmp/worktree-a' ? (makeStore(overrides).getAllWorktreeMeta()[worktreeId] as never) : undefined, setWorktreeMeta: (_worktreeId: string, meta: Record) => ({ ...makeStore(overrides).getAllWorktreeMeta()['repo-1::/tmp/worktree-a'], ...meta }) as never, removeWorktreeMeta: () => {}, getSettings: () => ({ workspaceDir: '/tmp/workspaces', nestWorkspaces: false, branchPrefix: 'none', branchPrefixCustom: '' }) }) it('writes runtime metadata with transport details when started', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) expect(metadata?.runtimeId).toBe(runtime.getRuntimeId()) expect(metadata?.authToken).toBeTruthy() expect(metadata?.transports?.[0]?.endpoint).toBeTruthy() expect(metadata?.transports).toEqual(server['transports']) await server.stop() expect(readRuntimeMetadata(userDataPath)).toMatchObject({ runtimeId: runtime.getRuntimeId() }) }) it('reclaims runtime metadata clobbered by a second instance that has since died', async () => { // Why: #7848 — a launch that slips past the single-instance lock republishes // orca-runtime.json with its own pid, so the CLI reports stale_bootstrap // against this still-serving runtime once that instance exits. const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const published = readRuntimeMetadata(userDataPath) writeRuntimeMetadata(userDataPath, { runtimeId: 'rt_second_instance', pid: 99999999, transports: [{ kind: 'unix', endpoint: join(userDataPath, 'o-99999999-rt2.sock') }], authToken: 'second-instance-token', startedAt: 1 }) server.checkRuntimeMetadataOwnership() expect(readRuntimeMetadata(userDataPath)).toEqual(published) await server.stop() }) it('leaves runtime metadata owned by a live sibling runtime untouched', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) // Why: a synthetic owned pid frees the always-alive process.pid to stand in for // the sibling — Windows never assigns pid 1, so hardcoding it there reads as dead. const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, pid: 4242 }) await server.start() writeRuntimeMetadata(userDataPath, { runtimeId: 'rt_live_sibling', pid: process.pid, transports: [{ kind: 'unix', endpoint: join(userDataPath, `o-${process.pid}-rt2.sock`) }], authToken: 'sibling-token', startedAt: 1 }) server.checkRuntimeMetadataOwnership() expect(readRuntimeMetadata(userDataPath)).toMatchObject({ runtimeId: 'rt_live_sibling' }) await server.stop() }) it('stops reclaiming runtime metadata after the server is stopped', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath }) await server.start() const watch = server['metadataOwnershipWatch'] if (!watch) { throw new Error('start() must arm the metadata ownership watch') } // Why: the republish guard alone would keep this test green, so assert the timer teardown itself. const watchStop = vi.spyOn(watch, 'stop') await server.stop() writeRuntimeMetadata(userDataPath, { runtimeId: 'rt_second_instance', pid: 99999999, transports: [], authToken: 'second-instance-token', startedAt: 1 }) server.checkRuntimeMetadataOwnership() expect(watchStop).toHaveBeenCalledTimes(1) expect(server['metadataOwnershipWatch']).toBeNull() expect(readRuntimeMetadata(userDataPath)).toMatchObject({ runtimeId: 'rt_second_instance' }) }) it('creates a pairing offer for the active WebSocket transport', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0, webClientRoot: userDataPath }) await server.start() const offer = server.createPairingOffer({ address: '100.64.1.20', name: 'CLI test' }) expect(offer.available).toBe(true) if (offer.available) { expect(offer.endpoint).toContain('100.64.1.20') const parsed = parsePairingCode(offer.pairingUrl) expect(parsed?.endpoint).toBe(offer.endpoint) expect(parsed?.deviceToken).toBeTruthy() expect(parsed?.publicKeyB64).toBeTruthy() expect(parsed?.scope).toBe('runtime') expect(server.getDeviceRegistry()?.getDevice(offer.deviceId)?.scope).toBe('runtime') } await server.stop() }) it('reports why pairing is unavailable before the WebSocket listener is ready', () => { const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath: mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')), enableWebSocket: true, wsPort: 0 }) expect(server.createPairingOffer({ name: 'Early test' })).toMatchObject({ available: false, reason: 'websocket_unavailable', guidance: expect.any(String) }) }) it('reports an E2EE identity initialization failure after the local transport starts', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) mkdirSync(join(userDataPath, E2EE_KEYPAIR_FILENAME)) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) try { await server.start() expect(server.createPairingOffer({ name: 'E2EE failure test' })).toMatchObject({ available: false, reason: 'e2ee_key_unavailable', guidance: expect.any(String) }) } finally { errorSpy.mockRestore() await server.stop() } }) it('reports a registry persistence failure without retaining a ghost credential', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() mkdirSync(join(userDataPath, DEVICE_REGISTRY_FILENAME)) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) try { expect(server.createPairingOffer({ name: 'Registry failure test' })).toMatchObject({ available: false, reason: 'device_registry_unavailable', guidance: expect.any(String) }) expect(server.getDeviceRegistry()?.listDevices()).toHaveLength(0) } finally { errorSpy.mockRestore() await server.stop() } }) it('rejects wildcard advertised addresses before minting a device credential', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { expect(server.getDeviceRegistry()?.listDevices()).toHaveLength(0) expect(server.createPairingOffer({ address: '0.0.0.0', name: 'Invalid test' })).toMatchObject( { available: false, reason: 'invalid_advertised_endpoint', guidance: expect.any(String) } ) expect(server.getDeviceRegistry()?.listDevices()).toHaveLength(0) } finally { await server.stop() } }) it('includes a web client URL when the web bundle is served by the runtime', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0, webClientRoot: userDataPath }) await server.start() try { const offer = server.createPairingOffer({ address: '100.64.1.20', name: 'Web test' }) expect(offer.available).toBe(true) if (offer.available) { expect(offer.webClientUrl).toBeTruthy() const url = new URL(offer.webClientUrl!) expect(url.protocol).toBe('http:') expect(url.hostname).toBe('100.64.1.20') expect(url.pathname).toBe('/web-index.html') expect(url.search).toBe('') expect(url.hash).toBe(`#pairing=${encodeURIComponent(offer.pairingUrl)}`) } } finally { await server.stop() } }) it('preserves proxy path prefixes in web client URLs', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0, webClientRoot: userDataPath }) await server.start() try { const offer = server.createPairingOffer({ address: 'wss://runtime.example.com/orca', name: 'Proxy test' }) expect(offer.available).toBe(true) if (offer.available) { expect(offer.webClientUrl).toContain('https://runtime.example.com/orca/web-index.html') } } finally { await server.stop() } }) it('formats pairing-address overrides for IPv6 and host-port tunnel endpoints', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0, webClientRoot: userDataPath }) await server.start() try { const ipv6 = server.createPairingOffer({ address: '::1', name: 'IPv6 test' }) expect(ipv6.available).toBe(true) if (ipv6.available) { expect(ipv6.endpoint).toMatch(/^ws:\/\/\[::1\]:\d+$/) expect(parsePairingCode(ipv6.pairingUrl)?.endpoint).toBe(ipv6.endpoint) } const tunnel = server.createPairingOffer({ address: 'tunnel.example.com:443', name: 'Tunnel test' }) expect(tunnel.available).toBe(true) if (tunnel.available) { expect(tunnel.endpoint).toBe('ws://tunnel.example.com:443') } const fullUrl = server.createPairingOffer({ address: 'wss://runtime.example.com/orca', name: 'Full URL test' }) expect(fullUrl.available).toBe(true) if (fullUrl.available) { expect(fullUrl.endpoint).toBe('wss://runtime.example.com/orca') } } finally { await server.stop() } }) it('creates mobile-scoped pairing offers for headless mobile pairing', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0, webClientRoot: userDataPath }) await server.start() try { const offer = server.createPairingOffer({ address: '100.64.1.20', name: 'Mobile test', scope: 'mobile' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } expect(server.getDeviceRegistry()?.getDevice(offer.deviceId)?.scope).toBe('mobile') expect(offer.webClientUrl).toBeNull() const parsed = parsePairingCode(offer.pairingUrl) expect(parsed?.endpoint).toBe(offer.endpoint) expect(parsed?.endpoint).toContain('100.64.1.20') } finally { await server.stop() } }) it('adds only the exact optional relay object to GUI mobile pairing offers', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const relay = { v: 1 as const, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 as const } server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay, binding: { relayHostId: relay.relayHostId, relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', name: 'Mobile test' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } const parsed = parsePairingCode(offer.pairingUrl) expect(parsed).toEqual( expect.objectContaining({ endpoint: offer.endpoint, scope: 'mobile', relay }) ) expect(parsed).not.toHaveProperty('endpoints') expect(offer.connectionMode).toBe('automatic') expect(server.getDeviceRegistry()?.getDevice(offer.deviceId)?.relayBinding).toEqual({ relayHostId: relay.relayHostId, relayDeviceId: offer.deviceId, ownerIdentityKey: 'user\0profile\0org' }) } finally { await server.stop() } }) it('queues the old Relay binding when a stable provider changes accounts', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let relayHostId = 'AbCdEf0123_-xyZ9' let ownerIdentityKey = 'user-a\0profile-a\0org' const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId, inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId, relayDeviceId, ownerIdentityKey } }), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(first.available).toBe(true) if (!first.available) { throw new Error('WebSocket pairing unavailable') } const firstBinding = server.getDeviceRegistry()?.getDevice(first.deviceId)?.relayBinding expect(firstBinding).toBeTruthy() if (!firstBinding) { throw new Error('Relay binding unavailable') } relayHostId = 'ZyXwVu9876_-abcD' ownerIdentityKey = 'user-b\0profile-b\0org' const second = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(second.available).toBe(true) if (!second.available) { throw new Error('WebSocket pairing unavailable') } expect(second.deviceId).toBe(first.deviceId) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() expect(onDeviceRevokeQueued).toHaveBeenCalledWith(expect.objectContaining(firstBinding)) expect(server.getDeviceRegistry()?.getDevice(second.deviceId)?.relayBinding).toEqual({ relayHostId, relayDeviceId: second.deviceId, ownerIdentityKey }) } finally { await server.stop() } }) it('refuses a silent LAN QR when relay invite minting fails under Anywhere', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) server.setMobileRelayPairingProvider({ createPairingRelay: vi.fn().mockRejectedValue(new Error('relay offline')), onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' }) // Why: Anywhere must not ship a scannable local-only code under the Relay label. expect(offer.available).toBe(false) if (offer.available) { throw new Error('expected relay mint failure') } expect(offer.reason).toBe('relay_mint_failed') expect(offer.relayFailure).toMatchObject({ code: 'relay_mint_failed', stage: 'create_pairing_relay' }) expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).toBeNull() } finally { await server.stop() } }) it('reports a missing Relay provider without creating a fallback QR', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(offer).toMatchObject({ available: false, reason: 'relay_mint_failed', relayFailure: { code: 'relay_provider_unavailable', stage: 'provider_missing', message: 'Orca Relay is not available on this desktop' } }) expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).toBeNull() } finally { await server.stop() } }) it('preserves an existing Relay QR when a same-mode remint fails', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const createPairingRelay = vi .fn() .mockImplementationOnce(async (relayDeviceId: string) => ({ relay: { v: 1 as const, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 as const }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } })) .mockRejectedValueOnce(new Error('relay offline')) server.setMobileRelayPairingProvider({ createPairingRelay, onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(first.available).toBe(true) if (!first.available) { throw new Error('WebSocket pairing unavailable') } const second = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(second.available).toBe(false) expect(server.getDeviceRegistry()?.getDevice(first.deviceId)?.relayBinding).toBeTruthy() expect(server.getDeviceRegistry()?.getPendingDevice('mobile')?.deviceId).toBe(first.deviceId) } finally { await server.stop() } }) it('coalesces concurrent mobile Relay mints for the shared pending credential', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveFirst: (() => void) | undefined const firstMint = new Promise((resolve) => { resolveFirst = resolve }) const createPairingRelay = vi.fn(async (relayDeviceId: string) => { await firstMint return { relay: { v: 1 as const, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 as const }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }) server.setMobileRelayPairingProvider({ createPairingRelay, onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = server.createMobilePairingOffer({ address: '100.64.1.20', rotate: true }) const second = server.createMobilePairingOffer({ address: '100.64.1.20', rotate: true }) await vi.waitFor(() => expect(createPairingRelay).toHaveBeenCalledTimes(1)) resolveFirst?.() const [firstOffer, secondOffer] = await Promise.all([first, second]) expect(firstOffer.available).toBe(true) expect(secondOffer.available).toBe(true) if (!firstOffer.available || !secondOffer.available) { throw new Error('WebSocket pairing unavailable') } expect(secondOffer.deviceId).toBe(firstOffer.deviceId) expect(secondOffer.pairingUrl).toBe(firstOffer.pairingUrl) expect(createPairingRelay).toHaveBeenCalledTimes(1) } finally { await server.stop() } }) it('supersedes an older concurrent Relay rotation for a different address', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveFirst: (() => void) | undefined const firstMint = new Promise((resolve) => { resolveFirst = resolve }) const createPairingRelay = vi.fn(async (relayDeviceId: string) => { if (createPairingRelay.mock.calls.length === 1) { await firstMint } return { relay: { v: 1 as const, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 as const }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay, onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = server.createMobilePairingOffer({ address: '100.64.1.20', rotate: true }) await vi.waitFor(() => expect(createPairingRelay).toHaveBeenCalledOnce()) const second = server.createMobilePairingOffer({ address: '100.64.1.21', rotate: true }) resolveFirst?.() await expect(first).resolves.toMatchObject({ available: false, relayFailure: { code: 'relay_request_superseded' } }) const secondOffer = await second expect(secondOffer.available).toBe(true) if (!secondOffer.available) { throw new Error('WebSocket pairing unavailable') } expect(secondOffer.endpoint).toContain('100.64.1.21') expect(createPairingRelay).toHaveBeenCalledTimes(2) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() } finally { await server.stop() } }) it('supersedes an older concurrent Relay mint for a different address without rotate', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveFirst: (() => void) | undefined const firstMint = new Promise((resolve) => { resolveFirst = resolve }) const createPairingRelay = vi.fn(async (relayDeviceId: string) => { if (createPairingRelay.mock.calls.length === 1) { await firstMint } return { relay: { v: 1 as const, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 as const }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay, onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = server.createMobilePairingOffer({ address: '100.64.1.20' }) await vi.waitFor(() => expect(createPairingRelay).toHaveBeenCalledOnce()) const second = server.createMobilePairingOffer({ address: '100.64.1.21' }) resolveFirst?.() await expect(first).resolves.toMatchObject({ available: false, relayFailure: { code: 'relay_request_superseded' } }) const secondOffer = await second expect(secondOffer.available).toBe(true) if (!secondOffer.available) { throw new Error('WebSocket pairing unavailable') } expect(secondOffer.endpoint).toContain('100.64.1.21') expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() } finally { await server.stop() } }) it('lets LAN supersede a pending Relay mint without waiting for it', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveRelay: (() => void) | undefined const relayGate = new Promise((resolve) => { resolveRelay = resolve }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => { await relayGate return { relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }, onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const relayOffer = server.createMobilePairingOffer({ address: '100.64.1.20' }) await vi.waitFor(() => expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).not.toBeNull() ) const localOffer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(localOffer.available).toBe(true) if (!localOffer.available) { throw new Error('LAN pairing unavailable') } expect(localOffer.connectionMode).toBe('local-only') resolveRelay?.() const staleRelayOffer = await relayOffer expect(staleRelayOffer.available).toBe(false) expect(server.getDeviceRegistry()?.getPendingDevice('mobile')?.deviceId).toBe( localOffer.deviceId ) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() } finally { await server.stop() } }) it('revokes a Relay invite when binding persistence throws', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const registry = server.getDeviceRegistry() if (!registry) { throw new Error('Device registry unavailable') } vi.spyOn(registry, 'setRelayBinding').mockImplementation(() => { throw new Error('disk full') }) const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(offer.available).toBe(false) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() expect(registry.getPendingDevice('mobile')).toBeNull() } finally { await server.stop() } }) it('revokes a Relay result from a provider replaced during minting', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveRelay: (() => void) | undefined const relayGate = new Promise((resolve) => { resolveRelay = resolve }) server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => { await relayGate return { relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }, onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offerPromise = server.createMobilePairingOffer({ address: '100.64.1.20' }) await vi.waitFor(() => expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).not.toBeNull() ) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: vi.fn(), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) resolveRelay?.() await expect(offerPromise).resolves.toMatchObject({ available: false }) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).toBeNull() } finally { await server.stop() } }) it('retains a minted Relay binding on the device when cleanup cannot be queued', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) let resolveRelay: (() => void) | undefined const relayGate = new Promise((resolve) => { resolveRelay = resolve }) server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => { await relayGate return { relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } } }, onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const registry = server.getDeviceRegistry() if (!registry) { throw new Error('Device registry unavailable') } const offerPromise = server.createMobilePairingOffer({ address: '100.64.1.20' }) await vi.waitFor(() => expect(registry.getPendingDevice('mobile')).not.toBeNull()) const deviceId = registry.getPendingDevice('mobile')?.deviceId vi.spyOn(server.getRelayRevokeOutbox(), 'enqueue').mockImplementation(() => { throw new Error('disk full') }) // Why: swapping the provider supersedes the in-flight mint. server.setMobileRelayPairingProvider({ createPairingRelay: vi.fn(), onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) resolveRelay?.() await expect(offerPromise).resolves.toMatchObject({ available: false }) expect(registry.getDevice(deviceId ?? '')?.relayBinding).toMatchObject({ relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId: deviceId }) } finally { await server.stop() } }) it('queues cloud cleanup when a minted Relay binding cannot be persisted', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async () => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId: 'wrong-device', ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(offer.available).toBe(false) expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() expect(server.getDeviceRegistry()?.getPendingDevice('mobile')).toBeNull() } finally { await server.stop() } }) it('persists local-only pairing and never mints or later binds Relay', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const createPairingRelay = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay, onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } expect(parsePairingCode(offer.pairingUrl)).not.toHaveProperty('relay') expect(createPairingRelay).not.toHaveBeenCalled() expect(offer.connectionMode).toBe('local-only') expect(server.getDeviceRegistry()?.getMobilePairingConnectionMode(offer.deviceId)).toBe( 'local-only' ) expect( server.setMobileRelayBinding(offer.deviceId, { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId: offer.deviceId, ownerIdentityKey: 'user\0profile\0org' }) ).toBe(false) } finally { await server.stop() } }) it('normalizes untrusted pairing modes to automatic at the runtime boundary', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const offer = await server.createMobilePairingOffer({ connectionMode: 'renderer-controlled-value' as never }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } expect(offer.connectionMode).toBe('automatic') expect(server.getDeviceRegistry()?.getMobilePairingConnectionMode(offer.deviceId)).toBe( 'automatic' ) } finally { await server.stop() } }) it('revokes and rotates a pending Relay code when switching it to local-only', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const anywhere = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(anywhere.available).toBe(true) if (!anywhere.available) { throw new Error('WebSocket pairing unavailable') } const local = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(local.available).toBe(true) if (!local.available) { throw new Error('WebSocket pairing unavailable') } expect(local.deviceId).not.toBe(anywhere.deviceId) expect(server.getDeviceRegistry()?.getDevice(anywhere.deviceId)).toBeNull() expect(onDeviceRevokeQueued).toHaveBeenCalledOnce() expect(parsePairingCode(local.pairingUrl)).not.toHaveProperty('relay') } finally { await server.stop() } }) it('rotates a pending local-only code when switching it back to Anywhere', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued: vi.fn(), getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const local = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(local.available).toBe(true) if (!local.available) { throw new Error('WebSocket pairing unavailable') } const anywhere = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(anywhere.available).toBe(true) if (!anywhere.available) { throw new Error('WebSocket pairing unavailable') } // Why: a QR displayed under the local-only pledge must not become an // anywhere-capable credential; the policy switch mints a fresh token. expect(anywhere.deviceId).not.toBe(local.deviceId) expect(server.getDeviceRegistry()?.getDevice(local.deviceId)).toBeNull() expect(anywhere.connectionMode).toBe('automatic') expect(parsePairingCode(anywhere.pairingUrl)).toHaveProperty('relay') } finally { await server.stop() } }) it('reuses the pending token when the requested mode is unchanged', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const onDeviceRevokeQueued = vi.fn() server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(first.available).toBe(true) if (!first.available) { throw new Error('WebSocket pairing unavailable') } // Why: a same-mode remint (e.g. two windows converging after a // preference sync) must not race rotations off each other's token. const second = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(second.available).toBe(true) if (!second.available) { throw new Error('WebSocket pairing unavailable') } expect(second.deviceId).toBe(first.deviceId) expect(onDeviceRevokeQueued).not.toHaveBeenCalled() } finally { await server.stop() } }) it('records cloud cleanup before rotating or deleting the local mobile credential', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const registryPresence: boolean[] = [] server.setMobileRelayPairingProvider({ createPairingRelay: async (relayDeviceId) => ({ relay: { v: 1, directorUrl: 'https://relay.example.com', cellUrl: 'https://cell.example.com', assignmentEpoch: 7, relayHostId: 'AbCdEf0123_-xyZ9', inviteToken: 'A'.repeat(43), inviteExpiresAt: Date.now() + 60_000, e2eeFraming: 2 }, binding: { relayHostId: 'AbCdEf0123_-xyZ9', relayDeviceId, ownerIdentityKey: 'user\0profile\0org' } }), onDeviceRevokeQueued: (item) => { registryPresence.push(server.getDeviceRegistry()?.getDevice(item.relayDeviceId) !== null) }, getEndpoints: vi.fn(), provisionRelay: vi.fn() }) await server.start() try { const first = await server.createMobilePairingOffer({ address: '100.64.1.20' }) expect(first.available).toBe(true) if (!first.available) { throw new Error('WebSocket pairing unavailable') } const second = await server.createMobilePairingOffer({ address: '100.64.1.20', rotate: true }) expect(second.available).toBe(true) if (!second.available) { throw new Error('WebSocket pairing unavailable') } expect(server.getDeviceRegistry()?.getDevice(first.deviceId)).toBeNull() await expect(server.revokeMobileDevice(second.deviceId)).resolves.toBe(true) expect(server.getDeviceRegistry()?.getDevice(second.deviceId)).toBeNull() expect(registryPresence).toEqual([true, true]) } finally { await server.stop() } }) it('binds pairing RPC providers to the immutable authenticated socket context', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) const getEndpoints = vi.fn().mockResolvedValue({ v: 1, relay: null }) const provisionRelay = vi.fn().mockResolvedValue({ v: 1, reqId: 'install-1', authorizationMode: 'authenticated-direct', currentVersion: 1, resumeExpiresAt: Date.now() + 60_000 }) server.setMobileRelayPairingProvider({ createPairingRelay: vi.fn(), onDeviceRevokeQueued: vi.fn(), getEndpoints, provisionRelay }) await server.start() try { const offer = server.createPairingOffer({ address: '127.0.0.1', scope: 'mobile' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } const session = await authenticateMobileWsSession(offer.pairingUrl) const responses = createEncryptedWsResponseReader(session) sendEncryptedWsRequest(session, { id: 'endpoints-1', method: 'pairing.getEndpoints', params: { installReqId: 'status-1' } }) await expect(responses.next('endpoints-1')).resolves.toMatchObject({ ok: true, result: { v: 1, relay: null } }) sendEncryptedWsRequest(session, { id: 'provision-1', method: 'pairing.provisionRelay', params: { reqId: 'install-1', newResumeTokenHash: 'A'.repeat(43) } }) await expect(responses.next('provision-1')).resolves.toMatchObject({ ok: true }) const [endpointContext, endpointParams] = getEndpoints.mock.calls[0]! expect(endpointContext).toEqual({ deviceId: offer.deviceId, connectionId: expect.any(String), transport: { transport: 'direct' } }) expect(endpointParams).toEqual({ installReqId: 'status-1' }) expect(provisionRelay).toHaveBeenCalledWith(endpointContext, { reqId: 'install-1', newResumeTokenHash: 'A'.repeat(43) }) responses.dispose() session.ws.close() await waitForWsClose(session.ws) } finally { await server.stop() } }) it('cleans up pre-auth E2EE WebSocket state when the socket closes', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'mobile-test', scope: 'mobile' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } const parsed = parsePairingCode(offer.pairingUrl)! const ws = await connectWs(parsed.endpoint) const mobileKeys = generateKeyPair() ws.send( JSON.stringify({ type: 'e2ee_hello', publicKeyB64: Buffer.from(mobileKeys.publicKey).toString('base64') }) ) expect(JSON.parse(await nextWsMessage(ws))).toEqual({ type: 'e2ee_ready' }) expect(server['mobileSocketWiring']?.channelCount).toBe(1) expect(server['mobileSocketWiring']?.connectionCount).toBe(1) ws.close() await waitForWsClose(ws) await waitFor( () => server['mobileSocketWiring']?.channelCount === 0 && server['mobileSocketWiring']?.connectionCount === 0 ) } finally { await server.stop() } }) it('terminates active WebSockets for a revoked mobile device', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) const disconnectSpy = vi.spyOn(runtime, 'onClientDisconnected') await server.start() try { const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'mobile-test', scope: 'mobile' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } const first = await authenticateMobileWs(offer.pairingUrl) const second = await authenticateMobileWs(offer.pairingUrl) await expect(server.revokeMobileDevice(offer.deviceId)).resolves.toBe(true) await Promise.all([waitForWsClose(first), waitForWsClose(second)]) await waitFor( () => server['mobileSocketWiring']?.channelCount === 0 && server['mobileSocketWiring']?.connectionCount === 0 ) expect(disconnectSpy).toHaveBeenCalledTimes(1) } finally { disconnectSpy.mockRestore() await server.stop() } }, 15_000) it('does not revoke runtime-scoped devices through mobile revocation', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'runtime-test', scope: 'runtime' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } await expect(server.revokeMobileDevice(offer.deviceId)).resolves.toBe(false) expect(server.getDeviceRegistry()?.getDevice(offer.deviceId)?.scope).toBe('runtime') } finally { await server.stop() } }) it('terminates active WebSockets for a revoked runtime access grant', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'runtime-test', scope: 'runtime' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } const first = await authenticateMobileWs(offer.pairingUrl) const second = await authenticateMobileWs(offer.pairingUrl) expect(server.revokeRuntimeAccess(offer.deviceId)).toBe(true) await Promise.all([waitForWsClose(first), waitForWsClose(second)]) await waitFor( () => server['mobileSocketWiring']?.channelCount === 0 && server['mobileSocketWiring']?.connectionCount === 0 ) expect(server.getDeviceRegistry()?.getDevice(offer.deviceId)).toBeNull() } finally { await server.stop() } }, 15_000) it('rotates unused runtime pairing links without revoking already-used grants', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const first = server.createPairingOffer({ address: '127.0.0.1', name: 'runtime-test', rotate: true, scope: 'runtime' }) const second = server.createPairingOffer({ address: '127.0.0.1', name: 'runtime-test', rotate: true, scope: 'runtime' }) expect(first.available).toBe(true) expect(second.available).toBe(true) if (!first.available || !second.available) { throw new Error('WebSocket pairing unavailable') } expect(first.deviceId).not.toBe(second.deviceId) expect(parsePairingCode(first.pairingUrl)?.deviceToken).not.toBe( parsePairingCode(second.pairingUrl)?.deviceToken ) expect(server.getDeviceRegistry()?.getDevice(first.deviceId)).toBeNull() server.getDeviceRegistry()?.updateLastSeen(second.deviceId) const third = server.createPairingOffer({ address: '127.0.0.1', name: 'runtime-test', rotate: true, scope: 'runtime' }) expect(third.available).toBe(true) if (!third.available) { throw new Error('WebSocket pairing unavailable') } expect(server.getDeviceRegistry()?.getDevice(second.deviceId)).not.toBeNull() expect(server.getDeviceRegistry()?.getDevice(third.deviceId)).not.toBeNull() } finally { await server.stop() } }, 15_000) it('caps WebSocket long-polls and aborts them when the socket closes', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false, longPollCap: 1 }) const device = server['deviceRegistry'] ?? null expect(device).toBeNull() server['deviceRegistry'] = new DeviceRegistry(userDataPath) const entry = server['deviceRegistry']!.addDevice('runtime-test', 'runtime') const ws = new FakeWebSocket() server['mobileSocketWiring'] = { getConnectionId: () => 'conn-test' } as unknown as NonNullable<(typeof server)['mobileSocketWiring']> const replies: Record[] = [] try { const first = server['handleWebSocketMessage']( JSON.stringify( withCurrentOrchestrationContract({ id: 'req_wait', method: 'orchestration.check', deviceToken: entry.token, params: { terminal: 'term_wait', wait: true, timeoutMs: 10_000 } }) ), (response) => replies.push(JSON.parse(response) as Record), () => {}, undefined, ws as unknown as WebSocket ) await waitFor(() => server['activeLongPolls'] === 1) await server['handleWebSocketMessage']( JSON.stringify( withCurrentOrchestrationContract({ id: 'req_busy', method: 'orchestration.check', deviceToken: entry.token, params: { terminal: 'term_busy', wait: true, timeoutMs: 10_000 } }) ), (response) => replies.push(JSON.parse(response) as Record), () => {}, undefined, ws as unknown as WebSocket ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_busy', ok: false, error: expect.objectContaining({ code: 'runtime_busy' }) }) ) expect(server['activeLongPolls']).toBe(1) ws.readyState = 3 ws.emit('close') await first expect(server['activeLongPolls']).toBe(0) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_wait', ok: true })) } finally { db.close() await server.stop() } }) it('applies the ask sub-cap on the WebSocket path and releases both counters on close', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) seedSupervisedAskWorkers(db, ['term_w0', 'term_w1', 'term_w2']) // Why: cap 4 → ask sub-cap 2, so the third ask must be shed while waits keep the other half. const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false, longPollCap: 4 }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) // Why: 'runtime' scope, not 'mobile' — orchestration.ask is absent from the mobile allowlist. const entry = server['deviceRegistry']!.addDevice('runtime-test', 'runtime') const ws = new FakeWebSocket() server['mobileSocketWiring'] = { getConnectionId: () => 'conn-test' } as unknown as NonNullable<(typeof server)['mobileSocketWiring']> const replies: Record[] = [] const push = (response: string): void => { replies.push(JSON.parse(response) as Record) } const dispatch = (id: string, method: string, params: unknown): Promise => server['handleWebSocketMessage']( JSON.stringify( withCurrentOrchestrationContract({ id, method, deviceToken: entry.token, params }) ), push, () => {}, undefined, ws as unknown as WebSocket ) try { const asks = [0, 1].map((i) => dispatch(`req_ask_${i}`, 'orchestration.ask', { from: `term_w${i}`, to: 'term_coord', question: 'proceed?', timeoutMs: 10_000 }) ) // Why: gate on the pre-existing total so a missing sub-cap fails on the shed below, not here. await waitFor(() => server['activeLongPolls'] === 2) await dispatch('req_ask_overflow', 'orchestration.ask', { from: 'term_w2', to: 'term_coord', question: 'proceed?', timeoutMs: 10_000 }) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_ask_overflow', ok: false, error: expect.objectContaining({ code: 'runtime_busy', message: 'orchestration.ask capacity reached; retry with backoff' }) }) ) // Shedding the ask must not burn a slot from the reserved half. expect(server['activeLongPolls']).toBe(2) expect(server['activeAskLongPolls']).toBe(2) const wait = dispatch('req_check_wait', 'orchestration.check', { terminal: 'term_other', wait: true, timeoutMs: 10_000 }) await waitFor(() => server['activeLongPolls'] === 3) expect(server['activeAskLongPolls']).toBe(2) ws.readyState = 3 ws.emit('close') await Promise.all([...asks, wait]) expect(server['activeLongPolls']).toBe(0) expect(server['activeAskLongPolls']).toBe(0) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_ask_0', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_check_wait', ok: true })) } finally { db.close() await server.stop() } }) it('shares one socket close listener across concurrent WebSocket dispatches', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const entry = server['deviceRegistry']!.addDevice('runtime-test', 'runtime') const ws = new FakeWebSocket() server['mobileSocketWiring'] = { getConnectionId: () => 'conn-test' } as unknown as NonNullable<(typeof server)['mobileSocketWiring']> let activeDispatches = 0 ;( server as unknown as { dispatcher: { dispatchStreaming: ( request: unknown, reply: unknown, context: { signal?: AbortSignal } ) => Promise } } ).dispatcher = { dispatchStreaming: vi.fn( async ( _request: unknown, _reply: unknown, context: { signal?: AbortSignal } ): Promise => { activeDispatches += 1 await new Promise((resolve) => { context.signal?.addEventListener( 'abort', () => { activeDispatches -= 1 resolve() }, { once: true } ) }) } ) } as never const pending = Array.from({ length: 12 }, (_entry, index) => server['handleWebSocketMessage']( JSON.stringify({ id: `req_${index}`, method: 'status.get', deviceToken: entry.token }), () => {}, () => {}, undefined, ws as unknown as WebSocket ) ) await waitFor(() => activeDispatches === 12) expect(ws.listenerCount('close')).toBe(1) expect(ws.listenerCount('error')).toBe(1) ws.readyState = 3 ws.emit('close') await Promise.all(pending) expect(activeDispatches).toBe(0) expect(ws.listenerCount('close')).toBe(0) expect(ws.listenerCount('error')).toBe(0) }) it('limits mobile-scoped WebSocket tokens to the mobile RPC surface', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const pushRuntimeGit = vi.fn().mockResolvedValue({ ok: true }) const selectClaudeAccount = vi.fn().mockResolvedValue({ ok: true }) const selectCodexAccount = vi.fn().mockResolvedValue({ ok: true }) const expectedCodexResetScope = { target: { runtime: 'host' as const, wslDistro: null }, accountId: 'codex-account', accountRevision: 42, offerRevision: 'v1:offer' } const consumeCodexRateLimitResetCredit = vi.fn().mockResolvedValue({ outcome: 'reset', scope: expectedCodexResetScope, snapshot: { claude: null, codex: null } }) const removeClaudeAccount = vi.fn().mockResolvedValue({ ok: true }) const readTerminal = vi.fn().mockResolvedValue({ tail: ['ok'] }) const getRuntimeGitStatus = vi .fn() .mockResolvedValue({ entries: [], conflictOperation: 'unknown' }) const getRuntimeGitUpstreamStatus = vi .fn() .mockResolvedValue({ hasUpstream: true, ahead: 1, behind: 0 }) const rebaseRuntimeGitFromBase = vi.fn().mockResolvedValue({ ok: true }) const abortRuntimeGitMerge = vi.fn().mockResolvedValue({ ok: true }) const abortRuntimeGitRebase = vi.fn().mockResolvedValue({ ok: true }) const bulkStageRuntimeGitPaths = vi.fn().mockResolvedValue({ ok: true }) const bulkUnstageRuntimeGitPaths = vi.fn().mockResolvedValue({ ok: true }) const getRuntimeGitDiff = vi.fn().mockResolvedValue({ kind: 'text', originalContent: 'before\n', modifiedContent: 'after\n', originalIsBinary: false, modifiedIsBinary: false }) const openMobileDiff = vi.fn().mockResolvedValue({ worktree: 'wt-1', relativePath: 'docs/readme.md', kind: 'markdown', opened: true }) const browserTabCreate = vi.fn().mockResolvedValue({ page: 'page-1' }) const browserSetViewport = vi.fn().mockResolvedValue({ ok: true }) const browserDialogAccept = vi.fn().mockResolvedValue({ ok: true }) const browserDialogDismiss = vi.fn().mockResolvedValue({ ok: true }) const listGitHubProjects = vi.fn().mockResolvedValue({ ok: true, projects: [] }) const listGitHubLabelsBySlug = vi.fn().mockResolvedValue({ ok: true, labels: ['bug'] }) const listGitHubAssignableUsersBySlug = vi .fn() .mockResolvedValue({ ok: true, users: [{ login: 'alex' }] }) const listGitHubIssueTypesBySlug = vi.fn().mockResolvedValue({ ok: true, types: [{ id: 'type-1', name: 'Bug', color: 'RED', description: null }] }) const updateGitHubProjectItemField = vi.fn().mockResolvedValue({ ok: true }) const clearGitHubProjectItemField = vi.fn().mockResolvedValue({ ok: true }) const updateGitHubIssueBySlug = vi.fn().mockResolvedValue({ ok: true }) const updateGitHubIssueTypeBySlug = vi.fn().mockResolvedValue({ ok: true }) const updateGitHubPullRequestBySlug = vi.fn().mockResolvedValue({ ok: true }) const updateRepoIssue = vi.fn().mockResolvedValue({ ok: true }) const listRepoLabels = vi.fn().mockResolvedValue(['bug']) const listRepoAssignableUsers = vi.fn().mockResolvedValue([{ login: 'alex' }]) const addRepoIssueComment = vi.fn().mockResolvedValue({ ok: true, comment: { id: 2 } }) const addRepoPRReviewComment = vi.fn().mockResolvedValue({ ok: true, comment: { id: 3 } }) const addRepoPRReviewCommentReply = vi.fn().mockResolvedValue({ ok: true, comment: { id: 4 } }) const getRepoPRFileContents = vi.fn().mockResolvedValue({ original: 'before', modified: 'after', originalIsBinary: false, modifiedIsBinary: false }) const rerunRepoPRChecks = vi.fn().mockResolvedValue({ ok: true, count: 1 }) const resolveRepoReviewThread = vi.fn().mockResolvedValue(true) const setRepoPRFileViewed = vi.fn().mockResolvedValue(true) const requestRepoPRReviewers = vi.fn().mockResolvedValue({ ok: true }) const mergeRepoPR = vi.fn().mockResolvedValue({ ok: true }) const addGitLabRepoIssueComment = vi.fn().mockResolvedValue({ ok: true }) const addGitLabRepoMRComment = vi.fn().mockResolvedValue({ ok: true }) const resolveGitLabRepoMRDiscussion = vi.fn().mockResolvedValue({ ok: true }) const mergeGitLabRepoMR = vi.fn().mockResolvedValue({ ok: true }) const addGitHubIssueCommentBySlug = vi.fn().mockResolvedValue({ ok: true, comment: { id: 1, author: 'me', body: 'done', createdAt: '2026-01-01T00:00:00Z', url: '' } }) const updateGitHubIssueCommentBySlug = vi.fn().mockResolvedValue({ ok: true }) const deleteGitHubIssueCommentBySlug = vi.fn().mockResolvedValue({ ok: true }) const linearSearchIssues = vi.fn().mockResolvedValue([]) const linearSelectWorkspace = vi.fn().mockReturnValue({ connected: true, selectedWorkspaceId: 'workspace-1' }) const linearTeamLabels = vi.fn().mockResolvedValue([{ id: 'label-1', name: 'bug' }]) const linearTeamMembers = vi.fn().mockResolvedValue([{ id: 'member-1', displayName: 'Alex' }]) const linearAddIssueComment = vi.fn().mockResolvedValue({ ok: true, id: 'comment-1' }) const runtime = { getRuntimeId: () => 'test-runtime', getStatus: vi.fn().mockResolvedValue({ graphStatus: 'ok' }), pushRuntimeGit, selectClaudeAccount, selectCodexAccount, consumeCodexRateLimitResetCredit, removeClaudeAccount, readTerminal, getRuntimeGitStatus, getRuntimeGitUpstreamStatus, rebaseRuntimeGitFromBase, abortRuntimeGitMerge, abortRuntimeGitRebase, bulkStageRuntimeGitPaths, bulkUnstageRuntimeGitPaths, getRuntimeGitDiff, openMobileDiff, browserTabCreate, browserSetViewport, browserDialogAccept, browserDialogDismiss, listGitHubProjects, listGitHubLabelsBySlug, listGitHubAssignableUsersBySlug, listGitHubIssueTypesBySlug, updateGitHubProjectItemField, clearGitHubProjectItemField, updateGitHubIssueBySlug, updateGitHubIssueTypeBySlug, updateGitHubPullRequestBySlug, updateRepoIssue, listRepoLabels, listRepoAssignableUsers, addRepoIssueComment, addRepoPRReviewComment, addRepoPRReviewCommentReply, getRepoPRFileContents, rerunRepoPRChecks, resolveRepoReviewThread, setRepoPRFileViewed, requestRepoPRReviewers, mergeRepoPR, addGitLabRepoIssueComment, addGitLabRepoMRComment, resolveGitLabRepoMRDiscussion, mergeGitLabRepoMR, addGitHubIssueCommentBySlug, updateGitHubIssueCommentBySlug, deleteGitHubIssueCommentBySlug, linearSearchIssues, linearSelectWorkspace, linearTeamLabels, linearTeamMembers, linearAddIssueComment, getClientSettings: vi.fn(() => ({ defaultTuiAgent: 'codex', agentCmdOverrides: {} })), updateClientSettings: vi.fn(() => ({ defaultTaskSource: 'linear' })) } as unknown as OrcaRuntimeService const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const mobile = server['deviceRegistry']!.addDevice('phone', 'mobile') const replies: Record[] = [] await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_forbidden', // files.delete is a real registered RPC intentionally kept off the // mobile allowlist — mobile clients must never delete host files. method: 'files.delete', deviceToken: mobile.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_allowed', method: 'status.get', deviceToken: mobile.token }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_settings_get', method: 'settings.get', deviceToken: mobile.token }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_settings_update', method: 'settings.update', deviceToken: mobile.token, params: { defaultTaskSource: 'linear' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_projects', method: 'github.project.listAccessible', deviceToken: mobile.token, params: {} }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_issue_types', method: 'github.project.listIssueTypesBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_labels', method: 'github.project.listLabelsBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_assignees', method: 'github.project.listAssignableUsersBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', seedLogins: ['alex'] } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_update_issue', method: 'github.project.updateIssueBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', number: 123, updates: { title: 'New title' } } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_update_issue_type', method: 'github.project.updateIssueTypeBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', number: 123, issueTypeId: 'type-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_update_field', method: 'github.project.updateItemField', deviceToken: mobile.token, params: { projectId: 'project-1', itemId: 'item-1', fieldId: 'field-1', value: { kind: 'text', text: 'Ready' } } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_clear_field', method: 'github.project.clearItemField', deviceToken: mobile.token, params: { projectId: 'project-1', itemId: 'item-1', fieldId: 'field-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_update_pr', method: 'github.project.updatePullRequestBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', number: 456, updates: { state: 'closed' } } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_add_comment', method: 'github.project.addIssueCommentBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', number: 123, body: 'done' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_update_comment', method: 'github.project.updateIssueCommentBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', commentId: 101, body: 'edited' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_project_delete_comment', method: 'github.project.deleteIssueCommentBySlug', deviceToken: mobile.token, params: { owner: 'stablyai', repo: 'orca', commentId: 101 } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_update_issue', method: 'github.updateIssue', deviceToken: mobile.token, params: { repo: 'id:repo-1', number: 123, updates: { title: 'New title', addLabels: ['bug'] } } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_labels', method: 'github.listLabels', deviceToken: mobile.token, params: { repo: 'id:repo-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_assignees', method: 'github.listAssignableUsers', deviceToken: mobile.token, params: { repo: 'id:repo-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_add_comment', method: 'github.addIssueComment', deviceToken: mobile.token, params: { repo: 'id:repo-1', number: 123, body: 'done' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_add_review_comment', method: 'github.addPRReviewComment', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, commitId: 'abc123', path: 'src/app.ts', line: 10, body: 'please fix' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_reply_review_comment', method: 'github.addPRReviewCommentReply', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, commentId: 99, body: 'fixed', threadId: 'thread-1', path: 'src/app.ts', line: 10 } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_pr_file_contents', method: 'github.prFileContents', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, path: 'src/app.ts', status: 'modified', headSha: 'abc123', baseSha: 'def456' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_rerun_checks', method: 'github.rerunPRChecks', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, headSha: 'abc123', failedOnly: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_resolve_thread', method: 'github.resolveReviewThread', deviceToken: mobile.token, params: { repo: 'id:repo-1', threadId: 'thread-1', resolve: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_file_viewed', method: 'github.setPRFileViewed', deviceToken: mobile.token, params: { repo: 'id:repo-1', pullRequestId: 'PR_kw', path: 'src/app.ts', viewed: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_request_reviewers', method: 'github.requestPRReviewers', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, reviewers: ['alex'] } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_github_merge_pr', method: 'github.mergePR', deviceToken: mobile.token, params: { repo: 'id:repo-1', prNumber: 456, method: 'squash' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_gitlab_add_issue_comment', method: 'gitlab.addIssueComment', deviceToken: mobile.token, params: { repo: 'id:repo-1', number: 123, body: 'done' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_gitlab_add_mr_comment', method: 'gitlab.addMRComment', deviceToken: mobile.token, params: { repo: 'id:repo-1', iid: 456, body: 'ship it' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_gitlab_resolve_mr_discussion', method: 'gitlab.resolveMRDiscussion', deviceToken: mobile.token, params: { repo: 'id:repo-1', iid: 456, discussionId: 'discussion-1', resolved: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_gitlab_merge_mr', method: 'gitlab.mergeMR', deviceToken: mobile.token, params: { repo: 'id:repo-1', iid: 456, method: 'merge' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_linear_search', method: 'linear.searchIssues', deviceToken: mobile.token, params: { query: 'auth', limit: 10, workspaceId: 'workspace-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_linear_select_workspace', method: 'linear.selectWorkspace', deviceToken: mobile.token, params: { workspaceId: 'workspace-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_linear_team_labels', method: 'linear.teamLabels', deviceToken: mobile.token, params: { teamId: 'team-1', workspaceId: 'workspace-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_linear_team_members', method: 'linear.teamMembers', deviceToken: mobile.token, params: { teamId: 'team-1', workspaceId: 'workspace-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_linear_add_comment', method: 'linear.addIssueComment', deviceToken: mobile.token, params: { issueId: 'issue-1', workspaceId: 'workspace-1', body: 'done' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_status', method: 'git.status', deviceToken: mobile.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_push', method: 'git.push', deviceToken: mobile.token, params: { worktree: 'id:wt-1', publish: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_upstream', method: 'git.upstreamStatus', deviceToken: mobile.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_rebase_from_base', method: 'git.rebaseFromBase', deviceToken: mobile.token, params: { worktree: 'id:wt-1', baseRef: 'origin/main' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_bulk_stage', method: 'git.bulkStage', deviceToken: mobile.token, params: { worktree: 'id:wt-1', filePaths: ['a.ts', 'b.ts'] } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_abort_merge', method: 'git.abortMerge', deviceToken: mobile.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_abort_rebase', method: 'git.abortRebase', deviceToken: mobile.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_bulk_unstage', method: 'git.bulkUnstage', deviceToken: mobile.token, params: { worktree: 'id:wt-1', filePaths: ['c.ts'] } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_select_claude', method: 'accounts.selectClaude', deviceToken: mobile.token, params: { accountId: 'claude-account' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_select_codex', method: 'accounts.selectCodex', deviceToken: mobile.token, params: { accountId: null } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_consume_codex_reset', method: 'accounts.consumeCodexResetCredit', deviceToken: mobile.token, params: { idempotencyKey: '11111111-1111-4111-8111-111111111111', expectedScope: expectedCodexResetScope } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_remove_claude', method: 'accounts.removeClaude', deviceToken: mobile.token, params: { accountId: 'claude-account' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_terminal_read', method: 'terminal.read', deviceToken: mobile.token, params: { terminal: 'term-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_files_open_diff', method: 'files.openDiff', deviceToken: mobile.token, params: { worktree: 'id:wt-1', relativePath: 'docs/readme.md', staged: true } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_git_diff', method: 'git.diff', deviceToken: mobile.token, params: { worktree: 'id:wt-1', filePath: 'docs/readme.md', staged: false } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_browser_tab_create', method: 'browser.tabCreate', deviceToken: mobile.token, params: { worktree: 'id:wt-1', url: 'about:blank' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_browser_viewport', method: 'browser.viewport', deviceToken: mobile.token, params: { worktree: 'id:wt-1', page: 'page-1', width: 390, height: 844 } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_browser_certificate_proceed', method: 'browser.certificate.proceed', deviceToken: mobile.token, params: { worktree: 'id:wt-1', page: 'page-1', challengeId: 'challenge-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_browser_dialog_accept', method: 'browser.dialogAccept', deviceToken: mobile.token, params: { worktree: 'id:wt-1', page: 'page-1', text: 'ok' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_browser_dialog_dismiss', method: 'browser.dialogDismiss', deviceToken: mobile.token, params: { worktree: 'id:wt-1', page: 'page-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_forbidden', ok: false, error: expect.objectContaining({ code: 'forbidden' }) }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_allowed', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_settings_get', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_settings_update', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_projects', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_issue_types', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_project_labels', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_assignees', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_update_issue', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_update_issue_type', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_update_field', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_clear_field', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_update_pr', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_add_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_update_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_project_delete_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_update_issue', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_labels', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_assignees', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_add_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_add_review_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_reply_review_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_pr_file_contents', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_rerun_checks', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_resolve_thread', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_file_viewed', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_github_request_reviewers', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_merge_pr', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_gitlab_add_issue_comment', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_gitlab_add_mr_comment', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_gitlab_merge_mr', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_linear_search', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_linear_select_workspace', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_linear_team_labels', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_linear_team_members', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_linear_add_comment', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_status', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_push', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_upstream', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_git_rebase_from_base', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_bulk_stage', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_abort_merge', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_git_abort_rebase', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_git_bulk_unstage', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_select_claude', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_select_codex', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_consume_codex_reset', ok: true }) ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_terminal_read', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_files_open_diff', ok: true })) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_diff', ok: true })) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_browser_tab_create', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_browser_viewport', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_browser_certificate_proceed', ok: false, error: expect.objectContaining({ code: 'forbidden' }) }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_browser_dialog_accept', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_browser_dialog_dismiss', ok: true }) ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_remove_claude', ok: false, error: expect.objectContaining({ code: 'forbidden' }) }) ) expect(selectClaudeAccount).toHaveBeenCalledWith('claude-account') expect(selectCodexAccount).toHaveBeenCalledWith(null) expect(consumeCodexRateLimitResetCredit).toHaveBeenCalledWith( '11111111-1111-4111-8111-111111111111', expectedCodexResetScope ) expect(readTerminal).toHaveBeenCalledWith('term-1', { cursor: undefined }) expect(getRuntimeGitStatus).toHaveBeenCalledWith('id:wt-1') expect(pushRuntimeGit).toHaveBeenCalledWith('id:wt-1', true, undefined, undefined) expect(getRuntimeGitUpstreamStatus).toHaveBeenCalledWith('id:wt-1') expect(bulkStageRuntimeGitPaths).toHaveBeenCalledWith('id:wt-1', ['a.ts', 'b.ts']) expect(abortRuntimeGitMerge).toHaveBeenCalledWith('id:wt-1') expect(abortRuntimeGitRebase).toHaveBeenCalledWith('id:wt-1') expect(bulkUnstageRuntimeGitPaths).toHaveBeenCalledWith('id:wt-1', ['c.ts']) expect(openMobileDiff).toHaveBeenCalledWith('id:wt-1', 'docs/readme.md', true) expect(getRuntimeGitDiff).toHaveBeenCalledWith('id:wt-1', 'docs/readme.md', false, undefined) expect(browserTabCreate).toHaveBeenCalledWith({ worktree: 'id:wt-1', url: 'about:blank' }) expect(browserSetViewport).toHaveBeenCalledWith({ worktree: 'id:wt-1', page: 'page-1', width: 390, height: 844 }) expect(browserDialogAccept).toHaveBeenCalledWith({ worktree: 'id:wt-1', page: 'page-1', text: 'ok' }) expect(browserDialogDismiss).toHaveBeenCalledWith({ worktree: 'id:wt-1', page: 'page-1' }) expect(listGitHubIssueTypesBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca' }) expect(listGitHubLabelsBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca' }) expect(listGitHubAssignableUsersBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', seedLogins: ['alex'] }) expect(updateGitHubIssueBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', number: 123, updates: { title: 'New title' } }) expect(updateGitHubIssueTypeBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', number: 123, issueTypeId: 'type-1' }) expect(updateGitHubPullRequestBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', number: 456, updates: { state: 'closed' } }) expect(addGitHubIssueCommentBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', number: 123, body: 'done' }) expect(updateGitHubIssueCommentBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', commentId: 101, body: 'edited' }) expect(deleteGitHubIssueCommentBySlug).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', commentId: 101 }) expect(updateRepoIssue).toHaveBeenCalledWith('id:repo-1', 123, { title: 'New title', addLabels: ['bug'] }) expect(listRepoLabels).toHaveBeenCalledWith('id:repo-1') expect(listRepoAssignableUsers).toHaveBeenCalledWith('id:repo-1') expect(addRepoIssueComment).toHaveBeenCalledWith('id:repo-1', 123, 'done', null) expect(addRepoPRReviewComment).toHaveBeenCalledWith('id:repo-1', { prNumber: 456, commitId: 'abc123', path: 'src/app.ts', line: 10, startLine: undefined, body: 'please fix', prRepo: null }) expect(addRepoPRReviewCommentReply).toHaveBeenCalledWith('id:repo-1', { prNumber: 456, commentId: 99, body: 'fixed', threadId: 'thread-1', path: 'src/app.ts', line: 10, prRepo: null }) expect(getRepoPRFileContents).toHaveBeenCalledWith('id:repo-1', { prNumber: 456, path: 'src/app.ts', oldPath: undefined, status: 'modified', headSha: 'abc123', baseSha: 'def456', prRepo: null }) expect(rerunRepoPRChecks).toHaveBeenCalledWith('id:repo-1', 456, { headSha: 'abc123', failedOnly: true, prRepo: null }) expect(resolveRepoReviewThread).toHaveBeenCalledWith('id:repo-1', 'thread-1', true, null) expect(setRepoPRFileViewed).toHaveBeenCalledWith('id:repo-1', { pullRequestId: 'PR_kw', path: 'src/app.ts', viewed: true, prRepo: null }) expect(requestRepoPRReviewers).toHaveBeenCalledWith('id:repo-1', 456, ['alex'], null) expect(mergeRepoPR).toHaveBeenCalledWith('id:repo-1', 456, 'squash', null) expect(addGitLabRepoIssueComment).toHaveBeenCalledWith('id:repo-1', 123, 'done', undefined) expect(addGitLabRepoMRComment).toHaveBeenCalledWith('id:repo-1', 456, 'ship it', undefined) expect(resolveGitLabRepoMRDiscussion).toHaveBeenCalledWith( 'id:repo-1', 456, 'discussion-1', true, undefined ) expect(mergeGitLabRepoMR).toHaveBeenCalledWith('id:repo-1', 456, 'merge', undefined) expect(updateGitHubProjectItemField).toHaveBeenCalledWith({ projectId: 'project-1', itemId: 'item-1', fieldId: 'field-1', value: { kind: 'text', text: 'Ready' } }) expect(clearGitHubProjectItemField).toHaveBeenCalledWith({ projectId: 'project-1', itemId: 'item-1', fieldId: 'field-1' }) expect(linearSearchIssues).toHaveBeenCalledWith('auth', 10, 'workspace-1') expect(linearSelectWorkspace).toHaveBeenCalledWith('workspace-1') expect(linearTeamLabels).toHaveBeenCalledWith('team-1', 'workspace-1') expect(linearTeamMembers).toHaveBeenCalledWith('team-1', 'workspace-1') expect(linearAddIssueComment).toHaveBeenCalledWith('issue-1', 'done', 'workspace-1') expect(removeClaudeAccount).not.toHaveBeenCalled() }) it('rejects WebSocket requests whose request token differs from the authenticated channel token', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = { getRuntimeId: () => 'test-runtime', getStatus: vi.fn().mockResolvedValue({ graphStatus: 'ok' }) } as unknown as OrcaRuntimeService const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const channelDevice = server['deviceRegistry']!.addDevice('phone', 'mobile') const requestDevice = server['deviceRegistry']!.addDevice('cli', 'runtime') const replies: Record[] = [] await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_mismatch', method: 'status.get', deviceToken: requestDevice.token }), (response) => replies.push(JSON.parse(response) as Record), () => {}, undefined, undefined, channelDevice.token ) expect(replies).toContainEqual( expect.objectContaining({ id: 'req_mismatch', ok: false, error: expect.objectContaining({ code: 'unauthorized' }) }) ) }) it('rejects unpaired terminal creates before runtime dispatch', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const createMobileSessionTerminal = vi.fn() const runtime = { getRuntimeId: () => 'test-runtime', createMobileSessionTerminal } as unknown as OrcaRuntimeService const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const replies: Record[] = [] const send = async (id: string, deviceToken?: string): Promise => { await server['handleWebSocketMessage']( JSON.stringify({ id, method: 'session.tabs.createTerminal', ...(deviceToken ? { deviceToken } : {}), params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) } await send('req_missing') await send('req_invalid', 'invalid-token') expect(replies).toEqual([ expect.objectContaining({ id: 'req_missing', error: expect.objectContaining({ code: 'unauthorized' }), ok: false }), expect.objectContaining({ id: 'req_invalid', error: expect.objectContaining({ code: 'unauthorized' }), ok: false }) ]) expect(createMobileSessionTerminal).not.toHaveBeenCalled() }) it('allows runtime-scoped WebSocket tokens to use the full RPC surface', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const pushRuntimeGit = vi.fn().mockResolvedValue({ ok: true }) const runtime = { getRuntimeId: () => 'test-runtime', pushRuntimeGit } as unknown as OrcaRuntimeService const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const runtimeDevice = server['deviceRegistry']!.addDevice('cli', 'runtime') const replies: Record[] = [] await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_push', method: 'git.push', deviceToken: runtimeDevice.token, params: { worktree: 'id:wt-1' } }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) expect(replies).toContainEqual(expect.objectContaining({ id: 'req_push', ok: true })) expect(pushRuntimeGit).toHaveBeenCalledWith('id:wt-1', undefined, undefined, undefined) }) it('leaves the last published metadata in place when a runtime stops', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, pid: 1001 }) await server.start() const metadata = readRuntimeMetadata(userDataPath) expect(metadata?.pid).toBe(1001) await server.stop() expect(readRuntimeMetadata(userDataPath)).toMatchObject({ pid: 1001, runtimeId: runtime.getRuntimeId() }) }) it('closes the socket if metadata publication fails during startup', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) const writeMetadataSpy = vi .spyOn(runtimeMetadataModule, 'writeRuntimeMetadata') .mockImplementationOnce(() => { throw new Error('write failed') }) const endpoint = createRuntimeTransportMetadata( userDataPath, process.pid, process.platform, runtime.getRuntimeId() ).endpoint await expect(server.start()).rejects.toThrow('write failed') expect(readRuntimeMetadata(userDataPath)).toBeNull() expect(existsSync(endpoint)).toBe(false) expect(server['transports']).toEqual([]) expect(server['activeTransports']).toEqual([]) writeMetadataSpy.mockRestore() }) it('serves status.get for authenticated callers', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_1', authToken: metadata!.authToken, method: 'status.get' }) expect(response).toMatchObject({ id: 'req_1', ok: true, _meta: { runtimeId: runtime.getRuntimeId() } }) expect((response.result as { graphStatus: string }).graphStatus).toBe('unavailable') await server.stop() }) it('stamps the authenticated device scope onto status.get for WebSocket clients', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false }) server['deviceRegistry'] = new DeviceRegistry(userDataPath) const mobile = server['deviceRegistry']!.addDevice('phone', 'mobile') const runtimeDevice = server['deviceRegistry']!.addDevice('browser', 'runtime') const sendStatus = async (token: string): Promise> => { const replies: Record[] = [] await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_status', method: 'status.get', deviceToken: token }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) return replies[0]! } const mobileReply = await sendStatus(mobile.token) expect(mobileReply).toMatchObject({ id: 'req_status', ok: true }) // Why: the mobile-scope web client reads this to refuse the full app. expect((mobileReply.result as { deviceScope?: string }).deviceScope).toBe('mobile') const runtimeReply = await sendStatus(runtimeDevice.token) expect((runtimeReply.result as { deviceScope?: string }).deviceScope).toBe('runtime') // Other methods stay unmodified — only status.get carries the scope. const replies: Record[] = [] await server['handleWebSocketMessage']( JSON.stringify({ id: 'req_forbidden', method: 'files.delete', deviceToken: mobile.token }), (response) => replies.push(JSON.parse(response) as Record), () => {} ) expect(replies[0]).toMatchObject({ id: 'req_forbidden', ok: false, error: { code: 'forbidden' } }) }) it('rejects requests with the wrong auth token', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_1', authToken: 'wrong', method: 'status.get' }) expect(response).toMatchObject({ id: 'req_1', ok: false, error: { code: 'unauthorized' } }) await server.stop() }) it('rejects malformed requests before dispatch', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { authToken: metadata!.authToken, method: 'status.get' }) expect(response).toMatchObject({ id: 'unknown', ok: false, error: { code: 'bad_request' } }) await server.stop() }) it('serves terminal.list and terminal.show for live runtime terminals', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore() as never) const writes: string[] = [] runtime.setPtyController({ write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Claude', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'hello\n', 123) await server.start() const metadata = readRuntimeMetadata(userDataPath) const listResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_list', authToken: metadata!.authToken, method: 'terminal.list', params: { worktree: 'id:repo-1::/tmp/worktree-a' } }) expect(listResponse).toMatchObject({ id: 'req_list', ok: true, result: { terminals: [expect.objectContaining({ ptyId: 'pty-1' })] } }) const handle = ( ( listResponse.result as { terminals: { handle: string }[] totalCount: number truncated: boolean } ).terminals[0] ?? { handle: '' } ).handle expect(handle).toBeTruthy() const showResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_show', authToken: metadata!.authToken, method: 'terminal.show', params: { terminal: handle } }) expect(showResponse).toMatchObject({ id: 'req_show', ok: true }) const readResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_read', authToken: metadata!.authToken, method: 'terminal.read', params: { terminal: handle } }) expect(readResponse).toMatchObject({ id: 'req_read', ok: true }) const sendResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_send', authToken: metadata!.authToken, method: 'terminal.send', params: { terminal: handle, text: 'continue', enter: true } }) expect(sendResponse).toMatchObject({ id: 'req_send', ok: true }) expect(writes).toEqual(['continue', '\r']) const waitPromise = sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_wait', authToken: metadata!.authToken, method: 'terminal.wait', params: { terminal: handle, for: 'exit', timeoutMs: 1000 } }) runtime.onPtyExit('pty-1', 9) const waitResponse = await waitPromise expect(waitResponse).toMatchObject({ id: 'req_wait', ok: true, result: { wait: { handle, condition: 'exit', satisfied: true, status: 'exited', exitCode: 9 } } }) await server.stop() }) it('serves terminal.list with visual split-group and pane nesting', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore() as never) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) const worktreeId = 'repo-1::/tmp/worktree-a' const leftLeaf = '11111111-1111-4111-8111-111111111111' const topLeaf = '22222222-2222-4222-8222-222222222222' const bottomLeaf = '33333333-3333-4333-8333-333333333333' runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-left', worktreeId, title: 'Left', activeLeafId: leftLeaf, layout: { type: 'leaf', leafId: leftLeaf } }, { tabId: 'tab-right', worktreeId, title: 'Right', activeLeafId: bottomLeaf, layout: { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: topLeaf }, second: { type: 'leaf', leafId: bottomLeaf } } } ], leaves: [ { tabId: 'tab-left', worktreeId, leafId: leftLeaf, paneRuntimeId: 1, ptyId: 'pty-left', title: 'Left' }, { tabId: 'tab-right', worktreeId, leafId: topLeaf, paneRuntimeId: 1, ptyId: 'pty-top', title: 'Right top' }, { tabId: 'tab-right', worktreeId, leafId: bottomLeaf, paneRuntimeId: 2, ptyId: 'pty-bottom', title: 'Right bottom' } ], mobileSessionTabs: [ { worktree: worktreeId, publicationEpoch: 'test', snapshotVersion: 1, activeGroupId: 'group-right', activeTabId: `tab-right::${bottomLeaf}`, activeTabType: 'terminal', tabGroups: [ { id: 'group-left', activeTabId: 'tab-left', tabOrder: ['tab-left'] }, { id: 'group-right', activeTabId: 'tab-right', tabOrder: ['tab-right'] } ], tabGroupLayout: { type: 'split', direction: 'horizontal', first: { type: 'leaf', groupId: 'group-left' }, second: { type: 'leaf', groupId: 'group-right' } }, tabs: [ { type: 'terminal', id: `tab-left::${leftLeaf}`, title: 'Left', parentTabId: 'tab-left', leafId: leftLeaf, ptyId: 'pty-left', parentLayout: { root: { type: 'leaf', leafId: leftLeaf }, activeLeafId: leftLeaf, expandedLeafId: null, ptyIdsByLeafId: { [leftLeaf]: 'pty-left' } }, isActive: false }, { type: 'terminal', id: `tab-right::${topLeaf}`, title: 'Right top', parentTabId: 'tab-right', leafId: topLeaf, ptyId: 'pty-top', parentLayout: { root: { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: topLeaf }, second: { type: 'leaf', leafId: bottomLeaf } }, activeLeafId: bottomLeaf, expandedLeafId: null, ptyIdsByLeafId: { [topLeaf]: 'pty-top', [bottomLeaf]: 'pty-bottom' } }, isActive: false }, { type: 'terminal', id: `tab-right::${bottomLeaf}`, title: 'Right bottom', parentTabId: 'tab-right', leafId: bottomLeaf, ptyId: 'pty-bottom', parentLayout: { root: { type: 'split', direction: 'vertical', first: { type: 'leaf', leafId: topLeaf }, second: { type: 'leaf', leafId: bottomLeaf } }, activeLeafId: bottomLeaf, expandedLeafId: null, ptyIdsByLeafId: { [topLeaf]: 'pty-top', [bottomLeaf]: 'pty-bottom' } }, isActive: true } ] } ] }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const listResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_list_layout', authToken: metadata!.authToken, method: 'terminal.list', params: { worktree: `id:${worktreeId}` } }) const result = listResponse.result as { visualLayouts?: unknown[] terminals: { handle: string; tabId: string; leafId: string }[] } const handleByLeaf = new Map( result.terminals.map((terminal) => [terminal.leafId, terminal.handle]) ) expect(listResponse).toMatchObject({ id: 'req_list_layout', ok: true }) expect(result.visualLayouts).toMatchObject([ { worktreeId, worktreePath: '/tmp/worktree-a', root: { type: 'split', direction: 'horizontal', first: { type: 'group', groupId: 'group-left', tabs: [ { tabId: 'tab-left', panes: { type: 'terminal', handle: handleByLeaf.get(leftLeaf), leafId: leftLeaf } } ] }, second: { type: 'group', groupId: 'group-right', tabs: [ { tabId: 'tab-right', panes: { type: 'pane-split', direction: 'vertical', first: { type: 'terminal', handle: handleByLeaf.get(topLeaf), leafId: topLeaf }, second: { type: 'terminal', handle: handleByLeaf.get(bottomLeaf), leafId: bottomLeaf, active: true } } } ] } } } ]) const resolvePaneResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_resolve_pane', authToken: metadata!.authToken, method: 'terminal.resolvePane', params: { paneKey: `tab-right:${bottomLeaf}`, worktreeId } }) expect(resolvePaneResponse).toMatchObject({ id: 'req_resolve_pane', ok: true, result: { terminal: { handle: handleByLeaf.get(bottomLeaf), tabId: 'tab-right', leafId: bottomLeaf, ptyId: 'pty-bottom', worktreeId } } }) const wrongOwnerResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_resolve_pane_wrong_owner', authToken: metadata!.authToken, method: 'terminal.resolvePane', params: { paneKey: `tab-right:${bottomLeaf}`, worktreeId: 'other-worktree' } }) expect(wrongOwnerResponse).toMatchObject({ id: 'req_resolve_pane_wrong_owner', ok: false, error: { message: 'terminal_not_found' } }) } finally { await server.stop() } }) it('mirrors laptop-created remote runtime terminals into phone session tabs over RPC', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore() as never) const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) runtime.setPtyController({ spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null }) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint const authToken = metadata!.authToken const leafId = '11111111-1111-4111-8111-111111111111' const createResponse = await sendRequest(endpoint, { id: 'laptop_create', authToken, method: 'terminal.create', params: { worktree: 'id:repo-1::/tmp/worktree-a', command: "claude 'work on the issue'", terminalColorQueryReplies: { foreground: '#ffffff', background: '#282c34' }, tabId: 'laptop-tab', leafId, presentation: 'background' } }) expect(createResponse).toMatchObject({ id: 'laptop_create', ok: true, result: { terminal: { worktreeId: 'repo-1::/tmp/worktree-a', surface: 'background' } } }) expect( (createResponse.result as { terminal?: { warning?: string } } | undefined)?.terminal?.warning ).toBeUndefined() expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ terminalColorQueryReplies: { foreground: '#ffffff', background: '#282c34' } }) ) runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 456) runtime.onPtyData('laptop-created-pty', 'Claude is working...\r\n', 456) const listResponse = await sendRequest(endpoint, { id: 'phone_list', authToken, method: 'session.tabs.list', params: { worktree: 'id:repo-1::/tmp/worktree-a' } }) const terminal = ( createResponse.result as { terminal: { handle: string } } ).terminal expect(listResponse).toMatchObject({ id: 'phone_list', ok: true, result: { tabs: [ { type: 'terminal', id: `laptop-tab::${leafId}`, parentTabId: 'laptop-tab', leafId, status: 'ready', terminal: terminal.handle, agentStatus: { state: 'working', paneKey: `laptop-tab:${leafId}`, terminalHandle: terminal.handle } } ] } }) const readResponse = await sendRequest(endpoint, { id: 'phone_read', authToken, method: 'terminal.read', params: { terminal: terminal.handle } }) expect(readResponse).toMatchObject({ id: 'phone_read', ok: true, result: { terminal: { tail: ['Claude is working...'] } } }) await server.stop() }) it('streams laptop-created runtime terminals to a paired phone WebSocket client', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const writes: string[] = [] const runtime = new OrcaRuntimeService(makeStore() as never) const spawn = vi.fn().mockResolvedValue({ id: 'paired-laptop-pty' }) runtime.setPtyController({ spawn, write: (_ptyId, data) => { writes.push(data) return true }, kill: () => true, getForegroundProcess: async () => null }) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() const phoneOffer = server.createPairingOffer({ address: '127.0.0.1', name: 'phone', scope: 'mobile' }) expect(phoneOffer.available).toBe(true) if (!phoneOffer.available) { throw new Error('WebSocket pairing unavailable') } expect(parsePairingCode(phoneOffer.pairingUrl)?.scope).toBe('mobile') const phone = await authenticateMobileWsSession(phoneOffer.pairingUrl) const phoneResponses = createEncryptedWsResponseReader(phone) const metadata = readRuntimeMetadata(userDataPath) const laptopEndpoint = metadata!.transports[0]!.endpoint const laptopAuthToken = metadata!.authToken const worktree = 'id:repo-1::/tmp/worktree-a' const leafId = '11111111-1111-4111-8111-111111111111' try { sendEncryptedWsRequest(phone, { id: 'phone_subscribe_tabs', method: 'session.tabs.subscribe', params: { worktree } }) await expect( phoneResponses.next('phone_subscribe_tabs', (response) => { const result = response.result as { type?: string; tabs?: unknown[] } | undefined return result?.type === 'snapshot' && result.tabs?.length === 0 }) ).resolves.toMatchObject({ ok: true, streaming: true }) const blockedUpdate = phoneResponses.next('phone_subscribe_tabs', (response) => { const result = response.result as { type?: string; tabs?: unknown[] } | undefined const tab = result?.tabs?.[0] as { agentStatus?: { state?: string } } | undefined return result?.type === 'updated' && tab?.agentStatus?.state === 'blocked' }) const createResponse = await sendRequest(laptopEndpoint, { id: 'laptop_create', authToken: laptopAuthToken, method: 'terminal.create', params: { worktree, command: "claude 'work on the issue'", tabId: 'laptop-tab', leafId, activate: true } }) const terminal = ( createResponse.result as { terminal: { handle: string } } ).terminal runtime.onPtyData('paired-laptop-pty', '\x1b]0;Claude waiting for permission\x07', 456) runtime.onPtyData('paired-laptop-pty', 'Need approval\r\n', 457) await expect(blockedUpdate).resolves.toMatchObject({ ok: true, streaming: true, result: { type: 'updated', tabs: [ { type: 'terminal', id: `laptop-tab::${leafId}`, parentTabId: 'laptop-tab', leafId, status: 'ready', terminal: terminal.handle, agentStatus: { state: 'blocked', paneKey: `laptop-tab:${leafId}`, terminalHandle: terminal.handle } } ] } }) sendEncryptedWsRequest(phone, { id: 'phone_read', method: 'terminal.read', params: { terminal: terminal.handle } }) await expect(phoneResponses.next('phone_read')).resolves.toMatchObject({ ok: true, result: { terminal: { tail: ['Need approval'] } } }) sendEncryptedWsRequest(phone, { id: 'phone_send', method: 'terminal.send', params: { terminal: terminal.handle, text: 'approved' } }) await expect(phoneResponses.next('phone_send')).resolves.toMatchObject({ ok: true, result: { send: { accepted: true } } }) expect(writes).toEqual(['approved']) } finally { phoneResponses.dispose() phone.ws.close() await server.stop() } }) it('authorizes a mobile artifact tap after first-connect backfill even once the raw window scrolls', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore() as never) runtime.setPtyController({ spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }), write: () => true, kill: () => true, getCwd: async () => '/tmp/worktree-a', getForegroundProcess: async () => null }) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) // Real artifact under the temp root so the grant path stats it. const artifactPath = join(tmpdir(), `orca-artifact-${process.pid}-${Date.now()}.json`) await writeFile(artifactPath, '{"ok":true}') runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Agent', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) // Path printed before any mobile client exists: tracking is inactive, so // only the retained raw window knows it at connect time. runtime.onPtyData('pty-1', `wrote ${artifactPath}\n`, 100) await server.start() const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'phone', scope: 'mobile' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } // Full direct E2EE authentication drives MobileSocketWiring.onReady (the // relay transport attaches through the same wiring), which must backfill // candidates from the raw window without any direct activation call. const phone = await authenticateMobileWsSession(offer.pairingUrl) const phoneResponses = createEncryptedWsResponseReader(phone) try { // Post-connect pathless output scrolls the artifact out of the raw // 64KiB window; only the connect-time backfilled candidate can answer. runtime.onPtyData('pty-1', 'x'.repeat(70 * 1024), 200) sendEncryptedWsRequest(phone, { id: 'phone_terminals', method: 'terminal.list', params: { worktree: 'id:repo-1::/tmp/worktree-a' } }) const listResponse = await phoneResponses.next('phone_terminals') const handle = (listResponse.result as { terminals: { handle: string }[] }).terminals[0]! .handle expect(handle).toBeTruthy() sendEncryptedWsRequest(phone, { id: 'phone_tap', method: 'files.resolveTerminalPath', params: { worktree: 'id:repo-1::/tmp/worktree-a', pathText: artifactPath, terminal: handle } }) await expect(phoneResponses.next('phone_tap')).resolves.toMatchObject({ ok: true, result: { exists: true, isDirectory: false, openTarget: { kind: 'absolute-file', provider: 'local', grantId: expect.any(String) } } }) } finally { phoneResponses.dispose() phone.ws.close() await server.stop() await rm(artifactPath, { force: true }) } }) it('completes remote E2EE authentication against a runtime proxy without activateRecentPtyPathCandidateTracking', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) // Why: a remote-host runtime proxy only implements RPC-forwarded methods; // activation is a local-host concern, so the proxy legitimately lacks // activateRecentPtyPathCandidateTracking and onReady must not throw. const runtimeProxy = { getRuntimeId: () => 'proxy-runtime-test', getStartedAt: () => 1, getStatus: () => ({ graphStatus: 'unavailable' }), cleanupSubscriptionsForConnection: () => {}, cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {} } as unknown as OrcaRuntimeService expect( (runtimeProxy as { activateRecentPtyPathCandidateTracking?: unknown }) .activateRecentPtyPathCandidateTracking ).toBeUndefined() const server = new OrcaRuntimeRpcServer({ runtime: runtimeProxy, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() const offer = server.createPairingOffer({ address: '127.0.0.1', name: 'remote', scope: 'runtime' }) expect(offer.available).toBe(true) if (!offer.available) { throw new Error('WebSocket pairing unavailable') } // Real E2EE pairing + authentication drives MobileSocketWiring.onReady // before e2ee_authenticated is sent; a throwing onReady never authenticates. const session = await authenticateMobileWsSession(offer.pairingUrl) const responses = createEncryptedWsResponseReader(session) try { sendEncryptedWsRequest(session, { id: 'proxy_status', method: 'status.get' }) await expect(responses.next('proxy_status')).resolves.toMatchObject({ id: 'proxy_status', ok: true, result: { graphStatus: 'unavailable' } }) } finally { responses.dispose() session.ws.close() await server.stop() } }) it('keeps active runtime multiplex streams responsive while a background stream is ACK-limited over WebSocket', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const writes: { terminal: string; text: string }[] = [] const runtime = new OrcaRuntimeService(makeStore() as never) const spawn = vi .fn() .mockResolvedValueOnce({ id: 'multiplex-background-pty' }) .mockResolvedValueOnce({ id: 'multiplex-active-pty' }) runtime.setPtyController({ spawn, write: (ptyId, data) => { writes.push({ terminal: ptyId, text: data }) return true }, kill: () => true, getForegroundProcess: async () => null }) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() const phoneOffer = server.createPairingOffer({ address: '127.0.0.1', name: 'phone', scope: 'mobile' }) expect(phoneOffer.available).toBe(true) if (!phoneOffer.available) { throw new Error('WebSocket pairing unavailable') } const pairing = parsePairingCode(phoneOffer.pairingUrl) expect(pairing).toBeTruthy() if (!pairing) { throw new Error('Pairing URL did not parse') } const metadata = readRuntimeMetadata(userDataPath) const laptopEndpoint = metadata!.transports[0]!.endpoint const laptopAuthToken = metadata!.authToken const worktree = 'id:repo-1::/tmp/worktree-a' const backgroundLeafId = '11111111-1111-4111-8111-111111111111' const activeLeafId = '22222222-2222-4222-8222-222222222222' const backgroundCreateResponse = await sendRequest(laptopEndpoint, { id: 'laptop_create_background', authToken: laptopAuthToken, method: 'terminal.create', params: { worktree, command: 'background', tabId: 'multiplex-background-tab', leafId: backgroundLeafId } }) const activeCreateResponse = await sendRequest(laptopEndpoint, { id: 'laptop_create_active', authToken: laptopAuthToken, method: 'terminal.create', params: { worktree, command: 'active', tabId: 'multiplex-active-tab', leafId: activeLeafId, activate: true } }) const backgroundTerminal = (backgroundCreateResponse.result as { terminal: { handle: string } }) .terminal const activeTerminal = (activeCreateResponse.result as { terminal: { handle: string } }) .terminal const responses: Record[] = [] const binaryFrames: Uint8Array[] = [] const onError = vi.fn() const subscription = await subscribeRemoteRuntimeRequest( pairing, 'terminal.multiplex', {}, 15_000, { onResponse: (response) => responses.push(response as Record), onBinary: (bytes) => binaryFrames.push(bytes), onError } ) try { await vi.waitFor(() => expect( responses.some( (response) => (response.result as { type?: string } | undefined)?.type === 'ready' ) ).toBe(true) ) subscription.sendBinary( encodeTerminalStreamFrame({ seq: 1, opcode: TerminalStreamOpcode.Subscribe, streamId: 0, payload: encodeTerminalStreamJson({ streamId: 21, terminal: backgroundTerminal.handle, client: { id: 'desktop-background', type: 'desktop' }, capabilities: { ackOutput: 1 } }) }) ) subscription.sendBinary( encodeTerminalStreamFrame({ seq: 2, opcode: TerminalStreamOpcode.Subscribe, streamId: 0, payload: encodeTerminalStreamJson({ streamId: 22, terminal: activeTerminal.handle, client: { id: 'desktop-active', type: 'desktop' }, capabilities: { ackOutput: 1 } }) }) ) await vi.waitFor(() => { const subscribedStreamIds = responses .map((response) => response.result as { type?: string; streamId?: number } | undefined) .filter((result) => result?.type === 'subscribed') .map((result) => result?.streamId) expect(subscribedStreamIds).toEqual(expect.arrayContaining([21, 22])) }) binaryFrames.splice(0) const backgroundOutput = 'B'.repeat(700 * 1024) runtime.onPtyData('multiplex-background-pty', backgroundOutput, 1) await vi.waitFor(() => { const backgroundFrames = binaryFrames .map((frame) => decodeTerminalStreamFrame(frame)) .filter((frame) => frame?.opcode === TerminalStreamOpcode.Output && frame.streamId === 21) const backgroundBytes = backgroundFrames.reduce( (total, frame) => total + (frame?.payload.byteLength ?? 0), 0 ) expect(backgroundBytes).toBeGreaterThan(0) expect(backgroundBytes).toBeLessThan(backgroundOutput.length) }) const frameCountBeforeActive = binaryFrames.length runtime.onPtyData('multiplex-active-pty', 'ACTIVE_MULTIPLEX_READY\r\n', 2) await vi.waitFor(() => { const activeOutput = binaryFrames .slice(frameCountBeforeActive) .map((frame) => decodeTerminalStreamFrame(frame)) .filter((frame) => frame?.opcode === TerminalStreamOpcode.Output && frame.streamId === 22) .map((frame) => (frame ? decodeTerminalStreamText(frame.payload) : '')) .join('') expect(activeOutput).toContain('ACTIVE_MULTIPLEX_READY') }) subscription.sendBinary( encodeTerminalStreamFrame({ seq: 3, opcode: TerminalStreamOpcode.Input, streamId: 22, payload: encodeTerminalStreamText('still interactive\r') }) ) await vi.waitFor(() => expect(writes).toContainEqual({ terminal: 'multiplex-active-pty', text: 'still interactive\r' }) ) const backgroundBytesBeforeAck = binaryFrames .map((frame) => decodeTerminalStreamFrame(frame)) .filter((frame) => frame?.opcode === TerminalStreamOpcode.Output && frame.streamId === 21) .reduce((total, frame) => total + (frame?.payload.byteLength ?? 0), 0) subscription.sendBinary( encodeTerminalStreamFrame({ seq: 4, opcode: TerminalStreamOpcode.Ack, streamId: 21, payload: encodeTerminalStreamJson({ bytes: backgroundBytesBeforeAck }) }) ) await vi.waitFor(() => { const backgroundBytesAfterAck = binaryFrames .map((frame) => decodeTerminalStreamFrame(frame)) .filter((frame) => frame?.opcode === TerminalStreamOpcode.Output && frame.streamId === 21) .reduce((total, frame) => total + (frame?.payload.byteLength ?? 0), 0) expect(backgroundBytesAfterAck).toBeGreaterThan(backgroundBytesBeforeAck) }) expect(onError).not.toHaveBeenCalled() } finally { subscription.close() await server.stop() } }) it('serves worktree.ps from the runtime summary builder', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore({ isUnread: true }) as never) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Claude', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'hello\n', 555) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_ps', authToken: metadata!.authToken, method: 'worktree.ps' }) expect(response).toMatchObject({ id: 'req_ps', ok: true, result: { worktrees: [ { worktreeId: 'repo-1::/tmp/worktree-a', repoId: 'repo-1', repo: 'repo', path: '/tmp/worktree-a', branch: 'feature/foo', linkedIssue: 123, sortOrder: 0, unread: true, liveTerminalCount: 1, hasAttachedPty: true, lastOutputAt: 555, preview: 'hello' } ], totalCount: 1, truncated: false } }) await server.stop() }) it('bounds worktree.list responses with limit metadata', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService(makeStore({ isUnread: true }) as never) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_worktrees', authToken: metadata!.authToken, method: 'worktree.list', params: { limit: 1 } }) expect(response).toMatchObject({ id: 'req_worktrees', ok: true, result: { totalCount: 1, truncated: false } }) await server.stop() }) it('rejects oversized RPC frames instead of buffering them indefinitely', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() const metadata = readRuntimeMetadata(userDataPath) const response = await new Promise>((resolve, reject) => { const socket = createConnection(metadata!.transports[0]!.endpoint) let buffer = '' socket.setEncoding('utf8') socket.once('error', reject) socket.on('data', (chunk: string) => { buffer += chunk const newlineIndex = buffer.indexOf('\n') if (newlineIndex === -1) { return } socket.end() resolve(JSON.parse(buffer.slice(0, newlineIndex)) as Record) }) socket.on('connect', () => { socket.write(`${'x'.repeat(1024 * 1024 + 1)}\n`) }) }) expect(response).toMatchObject({ ok: false, error: { code: 'request_too_large' } }) await server.stop() }) // Why: §6 tests for the transport keepalive + long-poll counter path in §3.1. // Exercise the real socket (not a mock) so we catch buffer/flush regressions // that a unit-level test would miss. describe('long-poll transport (§3.1)', () => { it('emits keepalive frames while a check --wait handler blocks', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) // Why: 50ms keepalive lets us collect ≥3 frames within a 300ms wait // window without slowing the suite. const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 50 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const session = openFramedSession(metadata!.transports[0]!.endpoint, { id: 'req_wait', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_nobody', wait: true, timeoutMs: 300 } }) await session.done const keepalives = session.frames.filter((f) => f._keepalive === true) const terminals = session.frames.filter((f) => f.ok !== undefined) expect(terminals).toHaveLength(1) expect(terminals[0]).toMatchObject({ id: 'req_wait', ok: true }) // Why: 300ms wait with 50ms keepalive → expect roughly 5 keepalives; // assert ≥3 to tolerate scheduler jitter without flaking. expect(keepalives.length).toBeGreaterThanOrEqual(3) } finally { db.close() await server.stop() } }) it('emits keepalive frames while orchestration.ask blocks for a reply', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const askerPaneKey = 'tab_asker:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => handle === 'term_asker' ? askerPaneKey : null ) const run = db.createRun({ objective: 'Keepalive test', coordinatorHandle: 'term_nobody', coordinatorPaneKey: 'tab_coord:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' }) const task = db.createTask({ spec: 'Wait for an answer', runId: run.id }) db.createDispatchContext(task.id, 'term_asker', askerPaneKey) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 50 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) // Why: no reply is ever sent, so ask blocks the full window on the same // hold-the-socket path check --wait uses. Without ask in the long-poll // set the 30s idle timer would tear this down before it keepalives. const session = openFramedSession(metadata!.transports[0]!.endpoint, { id: 'req_ask', authToken: metadata!.authToken, method: 'orchestration.ask', params: { to: 'term_nobody', from: 'term_asker', question: 'ping?', timeoutMs: 300 } }) await session.done const keepalives = session.frames.filter((f) => f._keepalive === true) const terminals = session.frames.filter((f) => f.ok !== undefined) expect(terminals).toHaveLength(1) expect(terminals[0]).toMatchObject({ id: 'req_ask', ok: true, result: { timedOut: true } }) expect(keepalives.length).toBeGreaterThanOrEqual(3) } finally { db.close() await server.stop() } }) it('emits keepalive frames while terminal.wait blocks and returns its structured timeout', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 30 }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) runtime.onPtyData('pty-1', 'Starting MCP servers...\n', 123) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const listResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_list', authToken: metadata!.authToken, method: 'terminal.list' }) const handle = ( listResponse.result as { terminals: { handle: string }[] } ).terminals[0]!.handle const session = openFramedSession(metadata!.transports[0]!.endpoint, { id: 'req_terminal_wait', authToken: metadata!.authToken, method: 'terminal.wait', params: { terminal: handle, for: 'tui-idle', timeoutMs: 150 } }) await session.done const keepalives = session.frames.filter((f) => f._keepalive === true) const terminalFrames = session.frames.filter((f) => f.ok !== undefined) expect(keepalives.length).toBeGreaterThanOrEqual(2) expect(terminalFrames).toHaveLength(1) expect(terminalFrames[0]).toMatchObject({ id: 'req_terminal_wait', ok: false, error: { code: 'timeout' } }) } finally { await server.stop() } }) it('releases terminal.wait long-poll slot when the client closes mid-wait', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 1 }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const listResponse = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_list', authToken: metadata!.authToken, method: 'terminal.list' }) const handle = ( listResponse.result as { terminals: { handle: string }[] } ).terminals[0]!.handle const endpoint = metadata!.transports[0]!.endpoint const session = openFramedSession(endpoint, { id: 'req_terminal_wait', authToken: metadata!.authToken, method: 'terminal.wait', params: { terminal: handle, for: 'exit', timeoutMs: 10_000 } }) await waitFor(() => server['activeLongPolls'] === 1) session.socket.destroy() await session.done await waitFor(() => server['activeLongPolls'] === 0) const admitted = openFramedSession(endpoint, { id: 'req_terminal_wait_2', authToken: metadata!.authToken, method: 'terminal.wait', params: { terminal: handle, for: 'tui-idle', timeoutMs: 50 } }) await admitted.done expect(admitted.frames.find((f) => f.ok !== undefined)).toMatchObject({ id: 'req_terminal_wait_2', ok: false, error: { code: 'timeout' } }) } finally { await server.stop() } }) it('releases long-poll slot when client closes mid-wait', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 2 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint // Fill the cap with two long waits (10s each — we'll kill them). const a = openFramedSession(endpoint, { id: 'req_a', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_a', wait: true, timeoutMs: 10_000 } }) const b = openFramedSession(endpoint, { id: 'req_b', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_b', wait: true, timeoutMs: 10_000 } }) // Let the two waits land in the handler and increment the counter. await sleep(100) expect(server['activeLongPolls']).toBe(2) // Kill one client mid-wait; counter must drop to 1. a.socket.destroy() await a.done // Give Node one tick to fire the close event on the server socket. await sleep(50) expect(server['activeLongPolls']).toBe(1) // The freed slot must admit a new long-poll immediately. const c = openFramedSession(endpoint, { id: 'req_c', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_c', wait: true, timeoutMs: 100 } }) await c.done const cTerminal = c.frames.find((f) => f.ok !== undefined) expect(cTerminal).toMatchObject({ ok: true, id: 'req_c' }) b.socket.destroy() await b.done } finally { db.close() await server.stop() } }) it('destroys active Unix socket connections when the runtime stops', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 1 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint const session = openFramedSession(endpoint, { id: 'req_stop', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_stop', wait: true, timeoutMs: 10_000 } }) await waitFor(() => server['activeLongPolls'] === 1) const stopResult = await Promise.race([ server.stop().then(() => 'stopped'), sleep(500).then(() => 'timeout') ]) expect(stopResult).toBe('stopped') await session.done await waitFor(() => server['activeLongPolls'] === 0) expect(session.socket.destroyed).toBe(true) } finally { db.close() await server.stop() } }) it('responds runtime_busy once the long-poll cap is saturated', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 1 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint const a = openFramedSession(endpoint, { id: 'req_a', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_a', wait: true, timeoutMs: 5_000 } }) await sleep(100) expect(server['activeLongPolls']).toBe(1) // Second long-poll overflows the cap → runtime_busy. const overflow = await sendRequest(endpoint, { id: 'req_overflow', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_b', wait: true, timeoutMs: 5_000 } }) expect(overflow).toMatchObject({ id: 'req_overflow', ok: false, error: { code: 'runtime_busy' } }) // The failing request must not have counted against the cap. expect(server['activeLongPolls']).toBe(1) // Short RPCs still succeed even when the long-poll cap is full. const short = await sendRequest(endpoint, { id: 'req_short', authToken: metadata!.authToken, method: 'status.get' }) expect(short).toMatchObject({ id: 'req_short', ok: true }) a.socket.destroy() await a.done } finally { db.close() await server.stop() } }) it('reserves long-poll headroom for terminal.wait when orchestration.ask floods', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) seedSupervisedAskWorkers(db, ['term_w0', 'term_w1', 'term_w2', 'term_w3']) // Why: cap 4 → ask sub-cap 2, so 4 concurrent asks can only take half the budget. const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 4 }) runtime.attachWindow(1) runtime.syncWindowGraph(1, { tabs: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', title: 'Terminal 1', activeLeafId: 'pane:1', layout: null } ], leaves: [ { tabId: 'tab-1', worktreeId: 'repo-1::/tmp/worktree-a', leafId: 'pane:1', paneRuntimeId: 1, ptyId: 'pty-1' } ] }) await server.start() const asks: ReturnType[] = [] try { const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint const listResponse = await sendRequest(endpoint, { id: 'req_list', authToken: metadata!.authToken, method: 'terminal.list' }) const handle = (listResponse.result as { terminals: { handle: string }[] }).terminals[0]! .handle // Four workers block in ask; distinct `from` handles so no reply wakes another. for (let i = 0; i < 4; i++) { asks.push( openFramedSession(endpoint, { id: `req_ask_${i}`, authToken: metadata!.authToken, method: 'orchestration.ask', params: { from: `term_w${i}`, to: 'term_coord', question: 'proceed?', timeoutMs: 10_000 } }) ) } // Let every ask reach the admission fence before probing the reserved half. await waitFor(() => server['activeLongPolls'] >= 2) await sleep(100) // The reserved half still admits a terminal.wait from any other client. const admitted = openFramedSession(endpoint, { id: 'req_terminal_wait', authToken: metadata!.authToken, method: 'terminal.wait', params: { terminal: handle, for: 'tui-idle', timeoutMs: 50 } }) await admitted.done expect(admitted.frames.find((f) => f.ok !== undefined)).toMatchObject({ id: 'req_terminal_wait', ok: false, error: { code: 'timeout' } }) // …and a check --wait too, which shares the same reserved class. const check = openFramedSession(endpoint, { id: 'req_check_wait', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_other', wait: true, timeoutMs: 100 } }) await check.done expect(check.frames.find((f) => f.ok !== undefined)).toMatchObject({ id: 'req_check_wait', ok: true }) // Overflow asks are shed, not queued: the sub-cap holds at half the budget. expect(server['activeAskLongPolls']).toBe(2) const shed = asks .map((a) => a.frames.find((f) => f.ok !== undefined)) .filter((f) => f !== undefined) expect(shed).toHaveLength(2) expect(shed[0]).toMatchObject({ ok: false, error: { code: 'runtime_busy' } }) } finally { for (const ask of asks) { ask.socket.destroy() } await Promise.all(asks.map((ask) => ask.done)) db.close() await server.stop() } }) it('keeps the full cap available to terminal.wait and check --wait', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 1000, longPollCap: 4 }) await server.start() const waits: ReturnType[] = [] try { const metadata = readRuntimeMetadata(userDataPath) const endpoint = metadata!.transports[0]!.endpoint // The ask sub-cap must not narrow the budget for the reserved class. for (let i = 0; i < 4; i++) { waits.push( openFramedSession(endpoint, { id: `req_wait_${i}`, authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: `term_${i}`, wait: true, timeoutMs: 10_000 } }) ) } await waitFor(() => server['activeLongPolls'] === 4) expect(server['activeAskLongPolls']).toBe(0) const overflow = await sendRequest(endpoint, { id: 'req_overflow', authToken: metadata!.authToken, method: 'orchestration.check', params: { terminal: 'term_overflow', wait: true, timeoutMs: 5_000 } }) expect(overflow).toMatchObject({ ok: false, error: { code: 'runtime_busy' } }) } finally { for (const wait of waits) { wait.socket.destroy() } await Promise.all(waits.map((wait) => wait.done)) db.close() await server.stop() } }) it('does not emit keepalive frames for short RPCs', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() // Why: a 10ms interval means any frame in the first ~100ms of a short // RPC would show up; `status.get` returns in <10ms so no keepalive // should ever fire. Locks in the "keepalive is long-poll-only" invariant // so a future refactor can't silently re-broaden the timer. const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, keepaliveIntervalMs: 10 }) await server.start() try { const metadata = readRuntimeMetadata(userDataPath) const session = openFramedSession(metadata!.transports[0]!.endpoint, { id: 'req_short', authToken: metadata!.authToken, method: 'status.get' }) await session.done const keepalives = session.frames.filter((f) => f._keepalive === true) const terminals = session.frames.filter((f) => f.ok !== undefined) expect(terminals).toHaveLength(1) expect(terminals[0]).toMatchObject({ id: 'req_short', ok: true }) expect(keepalives).toHaveLength(0) } finally { await server.stop() } }) it('returns an internal_error envelope when the dispatcher throws', async () => { // Why: handlers are designed to return error envelopes, never to throw, // but a bug somewhere in the RPC stack (e.g. JSON.stringify choking on // a response with circular refs) must still produce a terminal frame. // Without the `.catch` on handleMessage's promise, a throw would leave // the client hanging until the 30s idle timer and leak the dispatch's // AbortController in the transport's in-flight set. const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const runtime = new OrcaRuntimeService() const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) await server.start() // Force the dispatcher to throw a non-envelope error. const originalDispatch = server['dispatcher'].dispatch.bind(server['dispatcher']) server['dispatcher'].dispatch = vi.fn().mockRejectedValue(new Error('boom')) try { const metadata = readRuntimeMetadata(userDataPath) const response = await sendRequest(metadata!.transports[0]!.endpoint, { id: 'req_throw', authToken: metadata!.authToken, method: 'status.get' }) expect(response).toMatchObject({ id: 'req_throw', ok: false, error: { code: 'internal_error', message: 'boom' } }) } finally { server['dispatcher'].dispatch = originalDispatch await server.stop() } }) }) // Why: §6 test for the idempotent + hard-fail schema migration. A broken // migration must crash startup loudly rather than serve traffic against a // schema missing the delivered_at column. describe('orchestration DB migration (§3.2)', () => { it('is idempotent when delivered_at already exists', () => { // First open creates the column; second open should be a no-op. const db1 = new OrchestrationDb(':memory:') db1.close() // File path reuse is meaningless with :memory:, so use a tmp file. const tmpPath = join(mkdtempSync(join(tmpdir(), 'orca-orch-mig-')), 'orch.sqlite') const a = new OrchestrationDb(tmpPath) a.close() // Second construction must not throw "duplicate column name". expect(() => { const b = new OrchestrationDb(tmpPath) b.close() }).not.toThrow() }) it('hard-fails startup when the migration cannot be applied', () => { // Simulate a migration error by monkey-patching the SQLite wrapper's exec. // If ALTER TABLE throws for any reason (e.g. disk full, permissions), // the constructor must propagate — not swallow and serve half-broken. // // Why the pre-seeded v2 DB: after the schema bundle, fresh DBs are // initialized directly at v3 via createTables() (which already includes // `delivered_at`), so the v2 → v3 ALTER is a no-op for new installs. // To exercise the hard-fail path we need a DB that actually has work // to migrate — a v2-shape file without the delivered_at column — so // the guarded ALTER runs and the stub can fire. const tmpPath = join(mkdtempSync(join(tmpdir(), 'orca-orch-mig-')), 'orch.sqlite') const seed = new Database(tmpPath) seed.exec(` CREATE TABLE messages ( id TEXT NOT NULL, from_handle TEXT NOT NULL, to_handle TEXT NOT NULL, subject TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', type TEXT NOT NULL DEFAULT 'status' CHECK(type IN ( 'status', 'dispatch', 'worker_done', 'merge_ready', 'escalation', 'handoff', 'decision_gate', 'heartbeat' )), priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('normal', 'high', 'urgent')), thread_id TEXT, payload TEXT, read INTEGER NOT NULL DEFAULT 0, sequence INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); `) seed.pragma('user_version = 2') seed.close() const realPrototype = Database.prototype as unknown as { exec: (sql: string) => unknown } const originalExec = realPrototype.exec realPrototype.exec = function (sql: string) { if (sql.includes('ALTER TABLE messages ADD COLUMN delivered_at')) { throw new Error('simulated migration failure') } return originalExec.call(this, sql) } try { expect(() => new OrchestrationDb(tmpPath)).toThrow('simulated migration failure') } finally { realPrototype.exec = originalExec } }) }) }) describe('OrcaRuntimeRpcServer WebSocket bind host (STA-2370)', () => { const wsTransportOf = (server: OrcaRuntimeRpcServer): WebSocketTransport | undefined => (server['activeTransports'] as unknown[]).find( (transport): transport is WebSocketTransport => transport instanceof WebSocketTransport ) it('binds the listener to loopback on a fresh desktop with no paired device', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { expect(server.getDeviceRegistry()?.listDevices()).toHaveLength(0) // Why: the exposure regression — before STA-2370 this bound 0.0.0.0 with zero devices paired. expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') expect(new URL(server.getWebSocketEndpoint()!).hostname).toBe('127.0.0.1') } finally { await server.stop() } }) it('widens the listener to all interfaces when a mobile pairing offer is created', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const loopbackPort = wsTransportOf(server)?.resolvedPort expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(offer.available).toBe(true) expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') expect(new URL(server.getWebSocketEndpoint()!).hostname).toBe('0.0.0.0') // Why: the widen reuses the same port so the QR's already-advertised endpoint stays valid. expect(wsTransportOf(server)?.resolvedPort).toBe(loopbackPort) } finally { await server.stop() } }) it('binds all interfaces at startup when exposeNetworkByDefault is set (orca serve)', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0, exposeNetworkByDefault: true }) await server.start() try { expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') expect(new URL(server.getWebSocketEndpoint()!).hostname).toBe('0.0.0.0') } finally { await server.stop() } }) it('binds all interfaces at startup when a previously-connected device can reconnect', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) // Why: a device that has actually connected (lastSeenAt > 0) may reconnect, so the listener must be // reachable at startup without waiting for a new pairing action. const registry = new DeviceRegistry(userDataPath) const device = registry.getOrCreatePendingDevice('Paired phone', 'mobile') registry.updateLastSeen(device.deviceId) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { expect( server .getDeviceRegistry() ?.listDevices() .some((d) => d.lastSeenAt > 0) ).toBe(true) expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') } finally { await server.stop() } }) it('stays on loopback at startup for a pending device that has never connected', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) // Why: a pending device (offer created but never connected: lastSeenAt === 0) is not a reconnect, so // the listener must stay loopback — this distinguishes the reconnect widen from a blanket any-device // widen (a revert to listDevices().length > 0 would wrongly expose the LAN here). const registry = new DeviceRegistry(userDataPath) registry.getOrCreatePendingDevice('Pending phone', 'mobile') const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { expect(server.getDeviceRegistry()?.listDevices()).toHaveLength(1) expect( server .getDeviceRegistry() ?.listDevices() .every((d) => d.lastSeenAt === 0) ).toBe(true) expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') } finally { await server.stop() } }) it('keeps the same MobileSocketWiring instance across a pairing widen (relay capture stays valid)', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const wiringBeforeWiden = server.getMobileSocketWiring() expect(wiringBeforeWiden).not.toBeNull() expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(offer.available).toBe(true) expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') // Why: DesktopRelayService captures the wiring once at construction and hands it to every relay // broker, so the widen must swap the transport under the SAME wiring — replacing the wiring would // strand relay sockets on a dead object (lost connection IDs, binary handling, revocation targeting). expect(server.getMobileSocketWiring()).toBe(wiringBeforeWiden) // Why: object identity alone would pass even if the post-widen transport were never re-attached to the // captured wiring. Prove the swap functionally — route a revocation through the pre-widen wiring and // assert it reaches the NEW (0.0.0.0) transport, confirming attachTransport ran on the same wiring // after the rebind. A revert that leaves the wiring pointed at the stopped loopback transport fails here. const widenedTransport = wsTransportOf(server) expect(widenedTransport).toBeDefined() const terminateSpy = vi.spyOn(widenedTransport!, 'terminateClientConnections') wiringBeforeWiden!.terminateDeviceConnections('device-token-xyz') expect(terminateSpy).toHaveBeenCalledWith('device-token-xyz') } finally { await server.stop() } }) it('coalesces concurrent pairing widens into a single rebind', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') const widenSpy = vi.spyOn( server as unknown as { widenWebSocketBind: () => Promise }, 'widenWebSocketBind' ) await Promise.all([server.ensureNetworkExposure(), server.ensureNetworkExposure()]) // Why: two racing pairing offers must share one rebind via networkExposurePromise — two competing // stop/start races would fight for the port and could strand the listener on a random one. expect(widenSpy).toHaveBeenCalledTimes(1) expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') } finally { await server.stop() } }) it('reports pairing unavailable but keeps a serving loopback listener when the widen bind fails', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { const loopbackPort = wsTransportOf(server)?.resolvedPort // Why: force the wide bind to throw AFTER the loopback listener is stopped, then let the loopback // recovery bind through — proving no stranded/closed socket and a retry-able state. const target = server as unknown as { startWebSocketTransport: (opts: { host: string }) => Promise wsBoundHost: string | null } const original = target.startWebSocketTransport.bind(server) vi.spyOn(target, 'startWebSocketTransport').mockImplementation(async (opts) => { if (opts.host === '0.0.0.0') { throw new Error('injected wide bind failure') } return original(opts) }) const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) // Why: a failed widen must NOT advertise a LAN endpoint with no LAN listener behind it — the offer is // reported unavailable (STA-2370). A revert that swallows the widen failure would return available:true. expect(offer.available).toBe(false) if (!offer.available) { expect(offer.reason).toBe('network_exposure_failed') } // Why: the listener must keep serving on loopback (same port) rather than being left stranded/closed. expect(wsTransportOf(server)?.resolvedHost).toBe('127.0.0.1') expect(wsTransportOf(server)?.resolvedPort).toBe(loopbackPort) // Why: wsBoundHost stays loopback so a later pairing offer retries the widen. expect(target.wsBoundHost).toBe('127.0.0.1') } finally { await server.stop() errorSpy.mockRestore() } }) it('keeps the widened listener tracked when persisting pairing metadata fails', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() try { // Why: fail metadata publication AFTER the wide bind already succeeded. The live 0.0.0.0 listener must // stay tracked in activeTransports — a revert that runs loopback recovery here orphans a running wide // listener (an untracked 0.0.0.0 socket outside stop(): the exact STA-2370 exposure). const target = server as unknown as { writeMetadata: () => void wsBoundHost: string | null activeTransports: unknown[] } let injected = false const originalWrite = target.writeMetadata.bind(server) vi.spyOn(target, 'writeMetadata').mockImplementation(() => { if (!injected && target.wsBoundHost === '0.0.0.0') { injected = true throw new Error('injected metadata write failure') } return originalWrite() }) const offer = await server.createMobilePairingOffer({ address: '100.64.1.20', connectionMode: 'local-only' }) expect(injected).toBe(true) // Why: only metadata persistence failed; the wide bind succeeded, so pairing is available and the wide // listener is tracked (not orphaned) — bound to all interfaces, exactly one WebSocket transport. expect(offer.available).toBe(true) expect(wsTransportOf(server)?.resolvedHost).toBe('0.0.0.0') expect(target.activeTransports.filter((t) => t instanceof WebSocketTransport)).toHaveLength(1) } finally { await server.stop() errorSpy.mockRestore() } }) it('does not strand a wide listener when stop() races an in-flight widen', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() const target = server as unknown as { startWebSocketTransport: (opts: { host: string }) => Promise<{ transport: WebSocketTransport; endpoint: string }> activeTransports: unknown[] } const original = target.startWebSocketTransport.bind(server) let releaseWideStart: () => void = () => {} const wideStartGate = new Promise((resolve) => { releaseWideStart = resolve }) let wideStopSpy: ReturnType | null = null vi.spyOn(target, 'startWebSocketTransport').mockImplementation(async (opts) => { if (opts.host === '0.0.0.0') { // Why: hold the widen mid-flight (loopback already stopped) so stop() must race the rebind. await wideStartGate const result = await original(opts) wideStopSpy = vi.spyOn(result.transport, 'stop') return result } return original(opts) }) // Why: begin a pairing widen but do not await it, then start shutdown while it is still in-flight. const widen = server.ensureNetworkExposure() const stopping = server.stop() releaseWideStart() await Promise.all([widen.catch(() => {}), stopping]) try { // Why: STA-2370 — stop() must fence and await the in-flight widen so the freshly-bound wide listener is // snapshotted and stopped, never written back into the cleared arrays as a live 0.0.0.0 leak. A revert // (no fence) leaves the wide transport in activeTransports after stop() and never calls its stop(). expect(target.activeTransports).toHaveLength(0) expect(wideStopSpy).not.toBeNull() expect(wideStopSpy).toHaveBeenCalled() } finally { errorSpy.mockRestore() } }) it('refuses to widen a pairing offer that arrives after the server has stopped', async () => { const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) const server = new OrcaRuntimeRpcServer({ runtime: new OrcaRuntimeService(), userDataPath, enableWebSocket: true, wsPort: 0 }) await server.start() const widenSpy = vi.spyOn( server as unknown as { widenWebSocketBind: () => Promise }, 'widenWebSocketBind' ) await server.stop() // Why: STA-2370 — the `stopping` fence must reject a widen that arrives on its own AFTER shutdown, not // only one already in-flight during stop() (covered above via the pending-exposure await). Reverting the // `stopping` guard alone still passes the race test, but here ensureNetworkExposure would re-enter // widenWebSocketBind and re-open a 0.0.0.0 listener on a server that is supposed to be down. await server.ensureNetworkExposure() expect(widenSpy).not.toHaveBeenCalled() }) })