Files
orca/src/cli/index-test-harness.ts
T
Neil 4cc7e7859a fix(cli): route --host runtime:<id> to that server instead of answering locally (#15364)
* fix(cli): route --host runtime:<id> to that server instead of answering locally

`--host` was only ever a local filter over whatever runtime the CLI happened
to connect to, so `--host runtime:<id>` silently answered for (and mutated)
the local machine. A real environment id and a made-up one were
indistinguishable: both returned ok:true with an empty list and the local
runtimeId in _meta, and `project setup-clone --host runtime:<id>` cloned into
the caller's own machine.

Resolve the flag before the client is built: unparseable host ids and runtime
ids that no paired environment owns are rejected, and a known runtime id
selects that environment as the connection (conflicting with --pairing-code or
a different --environment is an error). Once routed, a host filter also
accepts the runtime's own `local`-stamped rows, since both spellings name the
machine we are now talking to.

* fix(cli): close --host routing gaps found in review

- Conflict-check an ambient ORCA_ENVIRONMENT, not just the --environment flag.
  `ORCA_ENVIRONMENT=staging orca ... --host runtime:<prod-id>` silently routed
  to prod while the flag spelling errored. An ambient pairing code still loses
  to the explicit flag, because it cannot be resolved to an id to compare.
- Attach the known environment ids to the unknown-id error as `error.data`, so
  a --json consumer can retry without parsing prose, and say outright that
  runtime:<id> matches ids only and never environment names.
- Fix four command examples that documented `--host runtime:gpu`. `gpu` is an
  environment name, so every one of them would now be rejected; use an id.
- Cover the routed connection on `worktree create` and `automations create`
  (the mutating paths), the `--environment X --host local` filter-only case,
  and assert error.code/error.data rather than only substrings.

* test(cli): pin execution-host-flag to the deferred error-class import

index.ts now loads execution-host-flag.ts on every invocation, making it the
sixth module on the --help path. It imports RuntimeClientError from
./runtime/types today, but nothing enforced that; switching it to the barrel
would silently drag zod/ws/tweetnacl back onto --help, which is exactly what
this guard exists to prevent. Verified the assertion fails when the import is
flipped to the barrel.
2026-08-18 14:12:24 -07:00

203 lines
6.5 KiB
TypeScript

import { afterEach, beforeEach, vi, type Mock } from 'vitest'
export type RuntimeClientModuleMocks = {
callMock: Mock
runtimeClientConstructorMock: Mock
serveOrcaAppMock: Mock
getDefaultUserDataPathMock: Mock
}
export type WorktreeAwarenessMocks = {
callMock: Mock
serveOrcaAppMock: Mock
getDefaultUserDataPathMock: Mock
addEnvironmentFromPairingCodeMock: Mock
listEnvironmentsMock: Mock
spawnMock: Mock
}
export async function createRuntimeClientModuleMock(mocks: RuntimeClientModuleMocks) {
// Why: re-export the REAL error classes rather than redefining them. format.ts
// narrows with `instanceof` against ./runtime/types, so a look-alike class
// here would make every CLI error fall through to the generic `runtime_error`
// shape — mirroring the barrel keeps the mock faithful to production.
const { RuntimeClientError, RuntimeRpcFailureError } = await import('./runtime/types.js')
class RuntimeClient {
readonly isRemote: boolean
call = mocks.callMock
getCliStatus = vi.fn()
openOrca = vi.fn()
constructor(
_userDataPath?: string,
_requestTimeoutMs?: number,
remotePairingCode?: string | null,
environmentSelector?: string | null
) {
mocks.runtimeClientConstructorMock(remotePairingCode, environmentSelector)
const effectivePairingCode =
remotePairingCode === undefined
? (process.env.ORCA_PAIRING_CODE ?? process.env.ORCA_REMOTE_PAIRING)
: remotePairingCode
const effectiveEnvironment =
environmentSelector === undefined ? process.env.ORCA_ENVIRONMENT : environmentSelector
if (effectivePairingCode && effectiveEnvironment) {
throw new RuntimeClientError(
'invalid_argument',
'Use either --pairing-code or --environment, not both.'
)
}
this.isRemote = Boolean(effectivePairingCode || effectiveEnvironment)
}
}
return {
RuntimeClient,
RuntimeClientError,
RuntimeRpcFailureError,
serveOrcaApp: mocks.serveOrcaAppMock,
getDefaultUserDataPath: mocks.getDefaultUserDataPathMock
}
}
export async function createChildProcessModuleMock(spawnMock: Mock) {
const { EventEmitter } = await import('node:events')
return {
spawn: spawnMock.mockImplementation(() => {
const child = Object.assign(new EventEmitter(), {
stdout: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stderr: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stdin: {
write: vi.fn(),
end: vi.fn()
},
kill: vi.fn()
})
process.nextTick(() => {
child.emit('exit', 0, null)
child.emit('close', 0, null)
})
return child
})
}
}
/** Registers a paired environment so `--host runtime:<id>` routes there instead of being rejected. */
export function pairRuntimeEnvironment(listEnvironmentsMock: Mock, id: string, name = id): void {
listEnvironmentsMock.mockReturnValue([
{
id,
name,
createdAt: 1,
updatedAt: 1,
lastUsedAt: null,
runtimeId: null,
endpoints: [
{
id: `ws-${id}`,
kind: 'websocket',
label: 'WebSocket',
endpoint: 'ws://127.0.0.1:6768',
deviceToken: 'token',
publicKeyB64: 'pk'
}
],
preferredEndpointId: `ws-${id}`
}
])
}
/** Installs the env-var save/restore + mock-reset hooks shared by the CLI worktree-awareness suites. */
export function useWorktreeAwarenessEnvironment(mocks: WorktreeAwarenessMocks): void {
const originalTerminalHandle = process.env.ORCA_TERMINAL_HANDLE
const originalUserDataPath = process.env.ORCA_USER_DATA_PATH
const originalDevCliInvocation = process.env.ORCA_DEV_CLI_INVOCATION
const originalPairingCode = process.env.ORCA_PAIRING_CODE
const originalRemotePairing = process.env.ORCA_REMOTE_PAIRING
const originalEnvironment = process.env.ORCA_ENVIRONMENT
const originalWorkspaceId = process.env.ORCA_WORKSPACE_ID
const originalWorktreeId = process.env.ORCA_WORKTREE_ID
beforeEach(() => {
mocks.callMock.mockReset()
delete process.env.ORCA_TERMINAL_HANDLE
delete process.env.ORCA_USER_DATA_PATH
delete process.env.ORCA_DEV_CLI_INVOCATION
delete process.env.ORCA_WORKSPACE_ID
delete process.env.ORCA_WORKTREE_ID
// Isolate the pane key so claude-teams tests that set it don't leak a
// senderPaneKey into later orchestration.send assertions.
delete process.env.ORCA_PANE_KEY
mocks.serveOrcaAppMock.mockReset()
mocks.getDefaultUserDataPathMock.mockClear()
mocks.addEnvironmentFromPairingCodeMock.mockReset()
mocks.listEnvironmentsMock.mockReset()
mocks.spawnMock.mockClear()
mocks.addEnvironmentFromPairingCodeMock.mockReturnValue({
id: 'env-1',
name: 'desk',
createdAt: 100,
updatedAt: 100,
lastUsedAt: null,
runtimeId: null,
endpoints: [
{
id: 'ws-env-1',
kind: 'websocket',
label: 'WebSocket',
endpoint: 'ws://127.0.0.1:6768',
deviceToken: 'token',
publicKeyB64: 'pk'
}
],
preferredEndpointId: 'ws-env-1'
})
mocks.listEnvironmentsMock.mockReturnValue([])
})
afterEach(() => {
vi.restoreAllMocks()
if (originalTerminalHandle === undefined) {
delete process.env.ORCA_TERMINAL_HANDLE
} else {
process.env.ORCA_TERMINAL_HANDLE = originalTerminalHandle
}
if (originalUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = originalUserDataPath
}
if (originalDevCliInvocation === undefined) {
delete process.env.ORCA_DEV_CLI_INVOCATION
} else {
process.env.ORCA_DEV_CLI_INVOCATION = originalDevCliInvocation
}
if (originalPairingCode === undefined) {
delete process.env.ORCA_PAIRING_CODE
} else {
process.env.ORCA_PAIRING_CODE = originalPairingCode
}
if (originalRemotePairing === undefined) {
delete process.env.ORCA_REMOTE_PAIRING
} else {
process.env.ORCA_REMOTE_PAIRING = originalRemotePairing
}
if (originalEnvironment === undefined) {
delete process.env.ORCA_ENVIRONMENT
} else {
process.env.ORCA_ENVIRONMENT = originalEnvironment
}
if (originalWorkspaceId === undefined) {
delete process.env.ORCA_WORKSPACE_ID
} else {
process.env.ORCA_WORKSPACE_ID = originalWorkspaceId
}
if (originalWorktreeId === undefined) {
delete process.env.ORCA_WORKTREE_ID
} else {
process.env.ORCA_WORKTREE_ID = originalWorktreeId
}
})
}