mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
* fix(orchestration): authorize worker reports without the dispatch capability Worker lifecycle reports and questions no longer depend on the per-dispatch capability token that lives only in the agent's conversation. The host now: - ignores capability_hash/capability_revoked_at for authorization on every row and checks the exact worker process instead (ask gains that check); - refuses a report whose calling terminal is provably another orchestration party (a Run coordinator or another Dispatch's worker), treating env that names no live pane here as absent; - applies one worker-state rule locally and remotely: a stop in flight refuses, while stop_unknown and start_unknown accept and settle. Minting and printing the flag are unchanged, so an older host and older preambles keep working. * fix(orchestration): stop minting the dispatch capability Dispatches no longer mint a per-Dispatch token, and preambles, the bundled skill guide and the ask resume hint stop printing --dispatch-capability. The consumer-generation bump and delivery fence that minting carried stay, now as setDispatchConsumer. Readers that inferred meaning from capability_hash read what they meant instead: worker-show's injected stage comes from the attached consumer, and a failed start copies custody identity only when no authority was ever attached. The CLI keeps accepting and forwarding the flag for older hosts. Cancelling a Task is recorded as failed with a reason; task-list now shows that reason and the guide and task-update notes document the recipe. * fix(orchestration): name the fenced party without implying which Dispatch it owns * refactor(orchestration): one worker report rule, fence only a different party - One module owns the unproven/settleable worker states and the refusal rule; local send records it, ask and remote throw it. A stale process is worker_identity_changed on every path. - The caller fence passes the worker's own terminal when its --from handle went stale. - Document the shared-tmux-server limit; drop the dead dispatch_capability_invalid rejection member; tests assert dispatch state, not the capability column. * refactor(orchestration): drop setDispatchConsumer and the dead capability retention - dispatch --inject no longer re-points the row createDispatchContext just wrote; worker-show reports every worker-less Dispatch as context_only, since Orca keeps no record of the paste. Tests re-point through a fixture. - failWorkerStart always records when the lifecycle closed; nothing authorizes on it. - Restore the ask resume hint's echo of a passed --dispatch-capability: an old host checks it before --resume. - Move the cancellation convention to #23983. * test(orchestration): drop capability-era assertions other tests already cover * refactor(orchestration): drop the host-side capability field and no-op test fixtures - RpcRequest and the SSH bridge stop carrying orchestrationCapability; the CLI's wire field stays for older hosts. - Fixtures pass identity to createRootDispatch instead of re-pointing to the same values; drop absence checks for a flag that can no longer be produced. * test(orchestration): cover a current process whose terminal moved to another pane * chore(orchestration): finish the capability cleanup in test stubs and skill wording * test(orchestration): drop needless response casts; mark the db stub cast safe
789 lines
24 KiB
TypeScript
789 lines
24 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('electron', () => ({
|
|
app: {
|
|
isPackaged: false,
|
|
getAppPath: () => '/host/app'
|
|
}
|
|
}))
|
|
vi.mock('../persistence', () => ({
|
|
getCanonicalUserDataPath: () => '/host/user-data'
|
|
}))
|
|
|
|
import { OrchestrationDb } from '../runtime/orchestration/db'
|
|
import { OrcaRuntimeService } from '../runtime/orca-runtime'
|
|
import type { HostCliPassthroughOptions } from './ssh-remote-cli-host-passthrough'
|
|
import { runRemoteOrcaCli } from './ssh-remote-orca-cli'
|
|
import { createRootDispatch } from '../runtime/orchestration/db/root-dispatch-test-fixture'
|
|
|
|
// Why: pointing the passthrough at a missing CLI entry forces the legacy
|
|
// in-process fallback, which is what these dispatch tests exercise.
|
|
const LEGACY_FALLBACK_OPTIONS: HostCliPassthroughOptions = {
|
|
execPath: '/host/electron',
|
|
cliEntryPath: '/host/app/out/cli/index.js',
|
|
userDataPath: '/host/user-data',
|
|
entryExists: () => false
|
|
}
|
|
type FakeChild = EventEmitter & {
|
|
stdout: EventEmitter
|
|
stderr: EventEmitter
|
|
stdin: { end: ReturnType<typeof vi.fn>; on: ReturnType<typeof vi.fn> }
|
|
kill: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
function createFakeChild(): FakeChild {
|
|
const child = new EventEmitter() as FakeChild
|
|
child.stdout = new EventEmitter()
|
|
child.stderr = new EventEmitter()
|
|
child.stdin = { end: vi.fn(), on: vi.fn() }
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
describe('runRemoteOrcaCli', () => {
|
|
function createRuntime() {
|
|
const messages: {
|
|
id: string
|
|
from_handle: string
|
|
to_handle: string
|
|
subject: string
|
|
body?: string
|
|
read_at: string | null
|
|
}[] = []
|
|
let nextMessage = 1
|
|
const db = {
|
|
insertMessage: vi.fn(
|
|
(message: { from: string; to: string; subject: string; body?: string }) => {
|
|
const row = {
|
|
id: `msg_${nextMessage++}`,
|
|
from_handle: message.from,
|
|
to_handle: message.to,
|
|
subject: message.subject,
|
|
body: message.body,
|
|
read_at: null
|
|
}
|
|
messages.push(row)
|
|
return row
|
|
}
|
|
),
|
|
getUnreadMessages: vi.fn((handle: string) =>
|
|
messages.filter((message) => message.to_handle === handle && message.read_at === null)
|
|
),
|
|
getAllMessagesForHandle: vi.fn((handle: string) =>
|
|
messages.filter((message) => message.to_handle === handle)
|
|
),
|
|
markAsRead: vi.fn((ids: string[]) => {
|
|
for (const message of messages) {
|
|
if (ids.includes(message.id)) {
|
|
message.read_at = new Date(0).toISOString()
|
|
}
|
|
}
|
|
}),
|
|
getLegacyAdoption: vi.fn(() => undefined),
|
|
getActiveDispatchForIdentity: vi.fn(() => undefined),
|
|
getActiveDispatchMailboxOwners: vi.fn(() => []),
|
|
getCurrentRunForPane: vi.fn(() => undefined),
|
|
getCurrentRunForCoordinator: vi.fn(() => undefined),
|
|
getRunMailboxOwnerIdsForHandle: vi.fn(() => []),
|
|
findActiveRemoteAttachmentForPane: vi.fn(() => undefined)
|
|
}
|
|
const runtime = {
|
|
getRuntimeId: () => 'runtime-test',
|
|
getStatus: () => ({
|
|
runtimeId: 'runtime-test',
|
|
rendererGraphEpoch: 1,
|
|
graphStatus: 'ready',
|
|
authoritativeWindowId: 1,
|
|
liveTabCount: 1,
|
|
liveLeafCount: 1
|
|
}),
|
|
getOrchestrationDb: () => db,
|
|
getTerminalPaneKey: () => null,
|
|
getLiveTerminalPaneKey: (handle: string) =>
|
|
handle === 'term_windows' ? 'tab_windows:leaf_windows' : null,
|
|
deliverPendingMessagesForHandle: vi.fn(),
|
|
notifyMessageArrived: vi.fn(),
|
|
linearIssueContext: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issue: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
title: 'Fix thing',
|
|
url: 'https://linear.app/acme/issue/ENG-123',
|
|
labels: []
|
|
},
|
|
meta: {
|
|
requested: {
|
|
current: true,
|
|
include: {
|
|
comments: true,
|
|
children: true,
|
|
attachments: true,
|
|
relations: true,
|
|
activity: true
|
|
},
|
|
depth: 2
|
|
},
|
|
resolved: {
|
|
id: 'issue-1',
|
|
identifier: 'ENG-123',
|
|
workspaceId: 'workspace-1',
|
|
workspaceName: 'Acme'
|
|
},
|
|
partial: false,
|
|
includeErrors: [],
|
|
sections: {}
|
|
}
|
|
})),
|
|
linearSearchForAgents: vi.fn(async (request: unknown) => ({
|
|
request,
|
|
issues: [],
|
|
meta: { query: 'auth bug', limit: 5, returned: 0, limitReached: false }
|
|
}))
|
|
} as unknown as OrcaRuntimeService
|
|
return { runtime, db }
|
|
}
|
|
|
|
it.each([
|
|
{ argv: ['terminal', 'list'], includeVisualLayouts: true },
|
|
{ argv: ['terminal', 'list', '--json'], includeVisualLayouts: false },
|
|
{
|
|
argv: ['terminal', 'list', '--json', '--include-visual-layouts'],
|
|
includeVisualLayouts: true
|
|
},
|
|
{
|
|
argv: ['--include-visual-layouts', 'terminal', 'list', '--json'],
|
|
includeVisualLayouts: true
|
|
}
|
|
])(
|
|
'requests terminal layouts according to the legacy SSH output mode',
|
|
async ({ argv, includeVisualLayouts }) => {
|
|
const runtime = new OrcaRuntimeService()
|
|
const listTerminals = vi.spyOn(runtime, 'listTerminals').mockResolvedValue({
|
|
terminals: [],
|
|
totalCount: 0,
|
|
truncated: false
|
|
})
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv, cwd: '/home/alice/repo', env: {} },
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(listTerminals).toHaveBeenCalledWith(undefined, undefined, {
|
|
handles: undefined,
|
|
requireFreshPtyLiveness: undefined,
|
|
includeVisualLayouts
|
|
})
|
|
}
|
|
)
|
|
|
|
// Why: `orca terminal create --shell` gates on these; without them an SSH pane was told the
|
|
// host was too old, when the accurate refusal is that SSH cannot apply the shell.
|
|
it('reports the execution host capabilities through the legacy status fallback', async () => {
|
|
const runtime = new OrcaRuntimeService()
|
|
vi.spyOn(runtime, 'getStatus').mockReturnValue({
|
|
runtimeId: 'runtime-test',
|
|
rendererGraphEpoch: 1,
|
|
graphStatus: 'ready',
|
|
authoritativeWindowId: 1,
|
|
liveTabCount: 0,
|
|
liveLeafCount: 0,
|
|
capabilities: ['terminal.create-shell-selection.v1']
|
|
})
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['status', '--json'], cwd: '/home/alice/repo', env: {} },
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode, result.stdout).toBe(0)
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
ok: true,
|
|
result: {
|
|
target: { kind: 'environment', environment: 'ssh' },
|
|
runtime: { reachable: true, capabilities: ['terminal.create-shell-selection.v1'] }
|
|
}
|
|
})
|
|
})
|
|
|
|
it('uses the remote ORCA_TERMINAL_HANDLE as orchestration sender identity', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['orchestration', 'send', '--to', 'term_windows', '--subject', 'ping', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode, result.stdout).toBe(0)
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
ok: true,
|
|
result: {
|
|
warnings: [
|
|
{
|
|
code: 'legacy_terminal_recipient',
|
|
recipient: 'term_windows'
|
|
}
|
|
]
|
|
}
|
|
})
|
|
expect(db.getUnreadMessages('term_windows')[0]?.from_handle).toBe('term_ssh')
|
|
})
|
|
|
|
it('does not trust caller-supplied remote pane identity in the legacy fallback', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['orchestration', 'send', '--to', 'term_windows', '--subject', 'ping', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_PANE_KEY: 'tab_ssh:leaf_ssh'
|
|
}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode, result.stdout).toBe(0)
|
|
expect(db.insertMessage).toHaveBeenCalledWith(
|
|
expect.objectContaining({ senderPaneKey: undefined })
|
|
)
|
|
})
|
|
|
|
it('returns a non-zero status for lifecycle rejection through the legacy fallback', async () => {
|
|
const db = new OrchestrationDb(':memory:')
|
|
const runtime = new OrcaRuntimeService()
|
|
runtime.setOrchestrationDb(db)
|
|
vi.spyOn(runtime, 'deliverPendingMessagesForHandle').mockImplementation(() => {})
|
|
vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
|
|
const run = db.createRun({
|
|
objective: 'Remote lifecycle rejection',
|
|
coordinatorHandle: 'term_coord',
|
|
coordinatorPaneKey: 'tab_coord:leaf_coord'
|
|
})
|
|
const task = db.createTask({ spec: 'remote work', runId: run.id })
|
|
const dispatch = createRootDispatch(db, task.id, 'term_ssh', 'tab_owner:leaf_owner')
|
|
vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue('tab_foreign:leaf_foreign')
|
|
|
|
try {
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--from',
|
|
'term_ssh',
|
|
'--to',
|
|
'term_coord',
|
|
'--subject',
|
|
'Done',
|
|
'--type',
|
|
'worker_done',
|
|
'--payload',
|
|
JSON.stringify({
|
|
taskId: task.id,
|
|
dispatchId: dispatch.id,
|
|
outcome: 'succeeded'
|
|
}),
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: { ORCA_PANE_KEY: 'tab_foreign:leaf_foreign' }
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
ok: true,
|
|
result: {
|
|
message: { type: 'worker_done', subject: 'Rejected worker_done: Done' },
|
|
lifecycle: { action: 'rejected', code: 'sender_not_assignee' }
|
|
}
|
|
})
|
|
expect(db.getTask(task.id)?.status).toBe('dispatched')
|
|
} finally {
|
|
db.close()
|
|
}
|
|
})
|
|
|
|
it('preserves structured lifecycle payload flags through the legacy fallback', async () => {
|
|
const db = new OrchestrationDb(':memory:')
|
|
const runtime = new OrcaRuntimeService()
|
|
runtime.setOrchestrationDb(db)
|
|
vi.spyOn(runtime, 'deliverPendingMessagesForHandle').mockImplementation(() => {})
|
|
vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
|
|
const run = db.createRun({
|
|
objective: 'Remote lifecycle success',
|
|
coordinatorHandle: 'term_coord',
|
|
coordinatorPaneKey: 'tab_coord:leaf_coord'
|
|
})
|
|
const task = db.createTask({ spec: 'remote work', runId: run.id })
|
|
const dispatch = createRootDispatch(db, task.id, 'term_ssh', 'tab_owner:leaf_owner')
|
|
vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue('tab_owner:leaf_owner')
|
|
|
|
try {
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--to',
|
|
'term_coord',
|
|
'--subject',
|
|
'Done',
|
|
'--type',
|
|
'worker_done',
|
|
'--task-id',
|
|
task.id,
|
|
'--dispatch-id',
|
|
dispatch.id,
|
|
'--outcome',
|
|
'succeeded',
|
|
'--files-modified',
|
|
'src/a.ts, src/b.ts',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_PANE_KEY: 'tab_owner:leaf_owner'
|
|
}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(db.getTask(task.id)).toMatchObject({
|
|
status: 'completed',
|
|
result: expect.stringContaining('src/a.ts')
|
|
})
|
|
} finally {
|
|
db.close()
|
|
}
|
|
})
|
|
|
|
it("still accepts an older host's --dispatch-capability through the SSH bridge", async () => {
|
|
const db = new OrchestrationDb(':memory:')
|
|
const runtime = new OrcaRuntimeService()
|
|
runtime.setOrchestrationDb(db)
|
|
vi.spyOn(runtime, 'deliverPendingMessagesForHandle').mockImplementation(() => {})
|
|
vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
|
|
vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue('tab_ssh:leaf_ssh')
|
|
vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockReturnValue('ssh_runtime:pty:1')
|
|
const run = db.createRun({
|
|
objective: 'SSH capability transport',
|
|
coordinatorHandle: 'term_coord',
|
|
coordinatorPaneKey: 'tab_coord:leaf_coord'
|
|
})
|
|
const task = db.createTask({ spec: 'remote work', runId: run.id })
|
|
const started = db.createStartingWorkerDispatch({
|
|
creator: { kind: 'system' },
|
|
maxDepth: Number.MAX_SAFE_INTEGER,
|
|
taskId: task.id,
|
|
startOptions: {}
|
|
})
|
|
db.prepareStartingWorkerAuthority({
|
|
dispatchId: started.dispatch.id,
|
|
handle: 'term_ssh',
|
|
paneKey: 'tab_ssh:leaf_ssh',
|
|
processIncarnation: 'ssh_runtime:pty:1',
|
|
worktreeId: 'repo::/home/alice/repo',
|
|
setupState: 'not_applicable',
|
|
effects: []
|
|
})
|
|
db.markWorkerDispatchReady(started.dispatch.id)
|
|
|
|
try {
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--type',
|
|
'worker_done',
|
|
'--subject',
|
|
'Done',
|
|
'--task-id',
|
|
task.id,
|
|
'--dispatch-id',
|
|
started.dispatch.id,
|
|
'--outcome',
|
|
'succeeded',
|
|
'--dispatch-capability',
|
|
'dcap_from_an_old_host',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_ssh',
|
|
ORCA_PANE_KEY: 'tab_ssh:leaf_ssh'
|
|
}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(db.getTask(task.id)).toMatchObject({ status: 'completed' })
|
|
expect(db.getWorkerDispatch(started.dispatch.id)).toMatchObject({ state: 'succeeded' })
|
|
} finally {
|
|
db.close()
|
|
}
|
|
})
|
|
|
|
it('rejects identity-less lifecycle sends in the legacy fallback', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--to',
|
|
'term_coord',
|
|
'--subject',
|
|
'Done',
|
|
'--type',
|
|
'worker_done',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: {}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
ok: false,
|
|
error: { code: 'no_active_sender_terminal' }
|
|
})
|
|
expect(db.insertMessage).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects mixed raw and structured payload flags in the legacy fallback', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--from',
|
|
'term_ssh',
|
|
'--to',
|
|
'term_coord',
|
|
'--subject',
|
|
'Done',
|
|
'--payload',
|
|
'{"taskId":"task_1"}',
|
|
'--task-id',
|
|
'task_1',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: {}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
ok: false,
|
|
error: { code: 'invalid_argument', message: expect.stringContaining('structured payload') }
|
|
})
|
|
expect(db.insertMessage).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('accepts equals-style orchestration flags in the remote shim', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: [
|
|
'orchestration',
|
|
'send',
|
|
'--to=term_windows',
|
|
'--subject=ping',
|
|
'--body=--literal-body',
|
|
'--json'
|
|
],
|
|
cwd: '/home/alice/repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode, result.stdout).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as { ok: boolean }
|
|
expect(payload.ok).toBe(true)
|
|
const message = db.getUnreadMessages('term_windows')[0]
|
|
expect(message?.from_handle).toBe('term_ssh')
|
|
expect(message?.body).toBe('--literal-body')
|
|
})
|
|
|
|
it('uses the remote ORCA_TERMINAL_HANDLE as orchestration check identity', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
db.insertMessage({
|
|
from: 'term_windows',
|
|
to: 'term_ssh',
|
|
subject: 'pong',
|
|
body: 'hello'
|
|
})
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['orchestration', 'check', '--all', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
result: { count: number; messages: { subject: string }[] }
|
|
}
|
|
expect(payload.ok).toBe(true)
|
|
expect(payload.result.count).toBe(1)
|
|
expect(payload.result.messages[0]?.subject).toBe('pong')
|
|
})
|
|
|
|
it('carries the remote pane key for an implicit orchestration check', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['orchestration', 'check', '--all', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_stale_ssh',
|
|
ORCA_PANE_KEY: 'tab_ssh:leaf_ssh'
|
|
}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(db.getCurrentRunForCoordinator).toHaveBeenCalledWith(
|
|
expect.objectContaining({ paneKey: 'tab_ssh:leaf_ssh' })
|
|
)
|
|
expect(db.getActiveDispatchForIdentity).toHaveBeenCalledWith(
|
|
'term_stale_ssh',
|
|
'tab_ssh:leaf_ssh'
|
|
)
|
|
})
|
|
|
|
it('does not inherit a remote pane key for explicit legacy inspection', async () => {
|
|
const { runtime, db } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['orchestration', 'check', '--terminal', 'term_legacy_worker', '--all', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: {
|
|
ORCA_TERMINAL_HANDLE: 'term_stale_ssh',
|
|
ORCA_PANE_KEY: 'tab_ssh:leaf_ssh'
|
|
}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(db.getCurrentRunForCoordinator).not.toHaveBeenCalled()
|
|
expect(db.getActiveDispatchForIdentity).toHaveBeenCalledWith('term_legacy_worker', undefined)
|
|
})
|
|
|
|
it('routes previously-unsupported commands through the full host CLI', async () => {
|
|
const { runtime } = createRuntime()
|
|
const child = createFakeChild()
|
|
const spawn = vi.fn(() => child)
|
|
|
|
const resultPromise = runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['worktree', 'create', '--repo', 'orca', '--branch', 'fix/x', '--json'],
|
|
cwd: '/home/alice/repo',
|
|
env: { ORCA_TERMINAL_HANDLE: 'term_ssh' }
|
|
},
|
|
{
|
|
execPath: '/host/electron',
|
|
cliEntryPath: '/host/app/out/cli/index.js',
|
|
userDataPath: '/host/user-data',
|
|
entryExists: () => true,
|
|
spawn: spawn as never
|
|
}
|
|
)
|
|
|
|
await Promise.resolve()
|
|
child.stdout.emit('data', Buffer.from('{"ok":true}\n'))
|
|
child.emit('close', 0)
|
|
|
|
const result = await resultPromise
|
|
expect(result).toEqual({ stdout: '{"ok":true}\n', stderr: '', exitCode: 0 })
|
|
const [, args] = spawn.mock.calls[0] as unknown as [string, string[]]
|
|
expect(args).toEqual([
|
|
'/host/app/out/cli/index.js',
|
|
'worktree',
|
|
'create',
|
|
'--repo',
|
|
'orca',
|
|
'--branch',
|
|
'fix/x',
|
|
'--json'
|
|
])
|
|
})
|
|
|
|
it('rejects host-interactive commands with a targeted error instead of bridging them', async () => {
|
|
const { runtime } = createRuntime()
|
|
const spawn = vi.fn()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['serve'], cwd: '/home/alice', env: {} },
|
|
{ ...LEGACY_FALLBACK_OPTIONS, spawn: spawn as never }
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(result.stderr).toContain('orca serve')
|
|
expect(result.stderr).toContain('SSH relay bridge')
|
|
expect(spawn).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects interactive account add but still bridges account list', async () => {
|
|
const { runtime } = createRuntime()
|
|
const spawn = vi.fn(() => createFakeChild())
|
|
|
|
const addResult = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['account', 'add'], cwd: '/home/alice', env: {} },
|
|
{ ...LEGACY_FALLBACK_OPTIONS, spawn: spawn as never }
|
|
)
|
|
|
|
expect(addResult.exitCode).toBe(1)
|
|
expect(addResult.stderr).toContain('interactive agent login')
|
|
expect(spawn).not.toHaveBeenCalled()
|
|
|
|
const child = createFakeChild()
|
|
spawn.mockReturnValueOnce(child)
|
|
const listPromise = runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['account', 'list'], cwd: '/home/alice', env: {} },
|
|
{
|
|
...LEGACY_FALLBACK_OPTIONS,
|
|
entryExists: () => true,
|
|
spawn: spawn as never
|
|
}
|
|
)
|
|
await Promise.resolve()
|
|
child.stdout.emit('data', Buffer.from('Managed Claude accounts\n'))
|
|
child.emit('close', 0)
|
|
|
|
await expect(listPromise).resolves.toEqual({
|
|
stdout: 'Managed Claude accounts\n',
|
|
stderr: '',
|
|
exitCode: 0
|
|
})
|
|
expect(spawn).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('bridges account add help because it does not start an interactive login', async () => {
|
|
const { runtime } = createRuntime()
|
|
const child = createFakeChild()
|
|
const spawn = vi.fn(() => child)
|
|
|
|
const resultPromise = runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['account', 'add', '--help'], cwd: '/home/alice', env: {} },
|
|
{
|
|
...LEGACY_FALLBACK_OPTIONS,
|
|
entryExists: () => true,
|
|
spawn: spawn as never
|
|
}
|
|
)
|
|
await Promise.resolve()
|
|
child.stdout.emit('data', Buffer.from('Usage: orca account add\n'))
|
|
child.emit('close', 0)
|
|
|
|
await expect(resultPromise).resolves.toEqual({
|
|
stdout: 'Usage: orca account add\n',
|
|
stderr: '',
|
|
exitCode: 0
|
|
})
|
|
expect(spawn).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('reports host-interactive command errors as JSON envelopes with --json', async () => {
|
|
const { runtime } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['serve', '--json'], cwd: '/home/alice', env: {} },
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
const payload = JSON.parse(result.stdout) as {
|
|
ok: boolean
|
|
error: { code: string }
|
|
}
|
|
expect(payload.ok).toBe(false)
|
|
expect(payload.error.code).toBe('unsupported_over_ssh')
|
|
})
|
|
|
|
it('explains the root cause when falling back and the command is not in the legacy switch', async () => {
|
|
const { runtime } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{ argv: ['worktree', 'list'], cwd: '/home/alice', env: {} },
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(result.stderr).toContain('Unsupported SSH Orca CLI command: worktree list')
|
|
expect(result.stderr).toContain('full Orca CLI bridge unavailable')
|
|
})
|
|
|
|
it('does not parse Android --activity values as Linear boolean flags', async () => {
|
|
const { runtime } = createRuntime()
|
|
|
|
const result = await runRemoteOrcaCli(
|
|
runtime,
|
|
{
|
|
argv: ['emulator', 'launch', 'com.acme.app', '--activity', '.MainActivity'],
|
|
cwd: '/home/alice',
|
|
env: {}
|
|
},
|
|
LEGACY_FALLBACK_OPTIONS
|
|
)
|
|
|
|
expect(result.exitCode).toBe(1)
|
|
expect(result.stderr).toContain(
|
|
'Unsupported SSH Orca CLI command: emulator launch com.acme.app'
|
|
)
|
|
expect(result.stderr).not.toContain('com.acme.app .MainActivity')
|
|
})
|
|
})
|