Files
orca/src/main/runtime/runtime-rpc.test.ts
T
Jinjing 9eede0084d fix(relay): refuse silent fallback when pairing invite fails (#11528)
* fix(relay): refuse silent fallback when pairing invite fails

When Orca Relay pairing fails, don't silently degrade to a LAN-only QR under the Relay label. Instead, surface structured failure information so the UI can clearly inform the user and offer recovery options.

* fix issues
2026-07-30 02:13:47 -07:00

5443 lines
181 KiB
TypeScript

/* 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 { 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<string, unknown>
): Promise<Record<string, unknown>> {
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<string, unknown>)
})
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<string, unknown>[]
done: Promise<void>
}
function openFramedSession(endpoint: string, request: Record<string, unknown>): FramedSession {
const frames: Record<string, unknown>[] = []
const socket = createConnection(endpoint)
let buffer = ''
socket.setEncoding('utf8')
const done = new Promise<void>((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<string, unknown>
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<string, unknown>
): Record<string, unknown> {
return typeof request.method === 'string' && request.method.startsWith('orchestration.')
? { ...request, orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION }
: request
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function waitFor(predicate: () => boolean, timeoutMs = 2_000): Promise<void> {
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<WebSocket> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(endpoint)
ws.once('open', () => resolve(ws))
ws.once('error', reject)
})
}
function nextWsMessage(ws: WebSocket): Promise<string> {
return new Promise((resolve) => {
ws.once('message', (data) => {
resolve(typeof data === 'string' ? data : data.toString('utf-8'))
})
})
}
function waitForWsClose(ws: WebSocket): Promise<void> {
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<AuthenticatedMobileWs> {
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<WebSocket> {
return (await authenticateMobileWsSession(pairingUrl)).ws
}
function sendEncryptedWsRequest(
session: AuthenticatedMobileWs,
request: Record<string, unknown>
): void {
session.ws.send(encrypt(JSON.stringify(request), session.sharedKey))
}
function createEncryptedWsResponseReader(session: AuthenticatedMobileWs): {
next: (
id: string,
predicate?: (response: Record<string, unknown>) => boolean
) => Promise<Record<string, unknown>>
dispose: () => void
} {
type Waiter = {
id: string
predicate: (response: Record<string, unknown>) => boolean
resolve: (response: Record<string, unknown>) => void
}
const queue: Record<string, unknown>[] = []
const waiters: Waiter[] = []
const takeQueued = (
id: string,
predicate: (response: Record<string, unknown>) => boolean
): Record<string, unknown> | 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<string, unknown>
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<string, unknown>) => boolean = () => true) => {
const queued = takeQueued(id, predicate)
if (queued) {
return Promise.resolve(queued)
}
return new Promise<Record<string, unknown>>((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<string, unknown>) =>
({
...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<string, unknown>) =>
({
...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<void>((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<void>((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<void>((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<void>((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<void>((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<void>((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<string, unknown>[] = []
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<string, unknown>),
() => {},
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<string, unknown>),
() => {},
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<string, unknown>[] = []
const push = (response: string): void => {
replies.push(JSON.parse(response) as Record<string, unknown>)
}
const dispatch = (id: string, method: string, params: unknown): Promise<void> =>
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<void>
}
}
).dispatcher = {
dispatchStreaming: vi.fn(
async (
_request: unknown,
_reply: unknown,
context: { signal?: AbortSignal }
): Promise<void> => {
activeDispatches += 1
await new Promise<void>((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<string, unknown>[] = []
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<string, unknown>),
() => {}
)
await server['handleWebSocketMessage'](
JSON.stringify({
id: 'req_allowed',
method: 'status.get',
deviceToken: mobile.token
}),
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
() => {}
)
await server['handleWebSocketMessage'](
JSON.stringify({
id: 'req_settings_get',
method: 'settings.get',
deviceToken: mobile.token
}),
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>),
() => {}
)
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<string, unknown>[] = []
await server['handleWebSocketMessage'](
JSON.stringify({
id: 'req_mismatch',
method: 'status.get',
deviceToken: requestDevice.token
}),
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
() => {},
undefined,
undefined,
channelDevice.token
)
expect(replies).toContainEqual(
expect.objectContaining({
id: 'req_mismatch',
ok: false,
error: expect.objectContaining({ code: 'unauthorized' })
})
)
})
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<string, unknown>[] = []
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<string, unknown>),
() => {}
)
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<Record<string, unknown>> => {
const replies: Record<string, unknown>[] = []
await server['handleWebSocketMessage'](
JSON.stringify({ id: 'req_status', method: 'status.get', deviceToken: token }),
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
() => {}
)
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<string, unknown>[] = []
await server['handleWebSocketMessage'](
JSON.stringify({ id: 'req_forbidden', method: 'files.delete', deviceToken: mobile.token }),
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
() => {}
)
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<string, unknown>[] = []
const binaryFrames: Uint8Array<ArrayBufferLike>[] = []
const onError = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
pairing,
'terminal.multiplex',
{},
15_000,
{
onResponse: (response) => responses.push(response as Record<string, unknown>),
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<Record<string, unknown>>((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<string, unknown>)
})
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<typeof openFramedSession>[] = []
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<typeof openFramedSession>[] = []
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
}
})
})
})