Files
orca/src/cli/index.test.ts
T

4946 lines
145 KiB
TypeScript

/* eslint-disable max-lines -- Why: CLI parser tests share one mocked runtime client and fixture queue; splitting this file would duplicate setup and make command coverage harder to audit. */
import path from 'node:path'
import { chmodSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', () => {
class RuntimeClient {
readonly isRemote: boolean
call = callMock
getCliStatus = vi.fn()
openOrca = vi.fn()
constructor(
_userDataPath?: string,
_requestTimeoutMs?: number,
remotePairingCode?: string | null,
environmentSelector?: string | null
) {
runtimeClientConstructorMock()
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)
}
}
class RuntimeClientError extends Error {
readonly code: string
readonly data?: unknown
constructor(code: string, message: string, data?: unknown) {
super(message)
this.code = code
this.data = data
}
}
class RuntimeRpcFailureError extends RuntimeClientError {
readonly response: unknown
constructor(response: unknown) {
super('runtime_error', 'runtime_error')
this.response = response
}
}
return {
RuntimeClient,
RuntimeClientError,
RuntimeRpcFailureError,
serveOrcaApp: serveOrcaAppMock,
getDefaultUserDataPath: getDefaultUserDataPathMock
}
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
listEnvironments: listEnvironmentsMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('child_process', async () => {
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
})
}
})
import {
buildCurrentWorktreeSelector,
COMMAND_SPECS,
main,
normalizeWorktreeSelector
} from './index'
import { GLOBAL_FLAGS, specPaths } from './args'
import { RuntimeRpcFailureError } from './runtime-client'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../shared/pairing'
describe('COMMAND_SPECS collision check', () => {
it('has no duplicate command or alias paths', () => {
// Why: first-match resolution would silently shadow duplicate aliases.
const seen = new Set<string>()
for (const spec of COMMAND_SPECS) {
for (const path of specPaths(spec)) {
const key = path.join(' ')
expect(seen.has(key), `Duplicate command/alias path: "${key}"`).toBe(false)
seen.add(key)
}
}
})
it('allows every flag documented in command usage strings', () => {
const flagPattern = /--([a-zA-Z0-9-]+)/g
for (const spec of COMMAND_SPECS) {
const allowed = new Set([...GLOBAL_FLAGS, ...spec.allowedFlags])
for (const match of spec.usage.matchAll(flagPattern)) {
const flag = match[1]
expect(
allowed.has(flag),
`Documented flag --${flag} is not allowed for command: ${spec.path.join(' ')}`
).toBe(true)
}
}
})
})
describe('command aliases dispatch to the canonical handler', () => {
let logSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
callMock.mockReset()
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
callMock.mockReset()
// Why: restore console.log so a downstream describe's vi.spyOn starts from a
// clean spy — otherwise this block's --json output leaks into its calls[0].
logSpy.mockRestore()
})
it('runs `worktree remove` as the canonical `worktree rm` (the incident)', async () => {
queueFixtures(callMock, okFixture('req', { removed: true }))
await main(['worktree', 'remove', '--worktree', 'id:wt-1', '--force', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith(
'worktree.rm',
expect.objectContaining({ worktree: 'id:wt-1', force: true })
)
})
it('runs `worktree delete` as the canonical `worktree rm`', async () => {
queueFixtures(callMock, okFixture('req', { removed: true }))
await main(['worktree', 'delete', '--worktree', 'id:wt-1', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith(
'worktree.rm',
expect.objectContaining({ worktree: 'id:wt-1' })
)
})
it('still runs `terminal focus` after the handler de-duplication', async () => {
queueFixtures(callMock, okFixture('req', { focus: { ok: true } }))
await main(['terminal', 'focus', '--terminal', 'term_abc', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith(
'terminal.focus',
expect.objectContaining({ navigation: 'host' })
)
})
it('serves `agent-context --json` without contacting the runtime', async () => {
runtimeClientConstructorMock.mockClear()
await main(['agent-context', '--json'], '/tmp/repo')
// Why: pure local read — proves the SSH/offline property (no RPC).
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
expect(callMock).not.toHaveBeenCalled()
const schema = JSON.parse(String(logSpy.mock.calls.at(-1)?.[0]))
expect(schema.schemaVersion).toBe(1)
const rm = schema.commands.find(
(command: { command: string }) => command.command === 'worktree rm'
)
expect(rm.aliases).toContainEqual(['worktree', 'remove'])
})
it('keeps `agent-context` local when remote environment variables are set', async () => {
vi.stubEnv('ORCA_PAIRING_CODE', 'pairing-code')
vi.stubEnv('ORCA_ENVIRONMENT', 'stale-environment')
try {
await main(['agent-context', '--json'], '/tmp/repo')
expect(process.exitCode).not.toBe(1)
expect(callMock).not.toHaveBeenCalled()
} finally {
vi.unstubAllEnvs()
}
})
})
describe('unknown command surfaces a suggestion', () => {
let errorSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
callMock.mockReset()
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
errorSpy.mockRestore()
process.exitCode = 0
})
it('prints did-you-mean for a near-miss command and exits non-zero', async () => {
await main(['worktree', 'remov'], '/tmp/repo')
expect(process.exitCode).toBe(1)
const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n')
expect(stderr).toContain('Unknown command: worktree remov')
expect(stderr).toContain('orca worktree')
})
it('reports a mistyped pre-command flag without swallowing the command', async () => {
await main(['--jso', 'worktree', 'list'], '/tmp/repo')
expect(process.exitCode).toBe(1)
const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n')
expect(stderr).toContain('Unknown flag --jso for command: worktree list')
expect(stderr).toContain('--json')
})
it('reports a pre-command flag that belongs to another command', async () => {
await main(['--workspace', 'worktree', 'list'], '/tmp/repo')
expect(process.exitCode).toBe(1)
const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n')
expect(stderr).toContain('Unknown flag --workspace for command: worktree list')
})
it('reports a pre-command typo when a global flag splits the command path', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['--jso', 'worktree', '--json', 'list'], '/tmp/repo')
expect(process.exitCode).toBe(1)
expect(logSpy.mock.calls.flat().join('\n')).toContain(
'Unknown flag --jso for command: worktree list'
)
expect(callMock).not.toHaveBeenCalled()
logSpy.mockRestore()
})
it.each(['environment', 'pairing-code'])(
'rejects --%s without a selector before runtime construction',
async (flag) => {
runtimeClientConstructorMock.mockClear()
await main([`--${flag}`, 'worktree', 'list'], '/tmp/repo')
expect(process.exitCode).toBe(1)
const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n')
expect(stderr).toContain(`Flag --${flag} requires a value.`)
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
expect(callMock).not.toHaveBeenCalled()
}
)
})
describe('unknown help command surfaces a suggestion', () => {
it.each([
['help prefix', ['help', 'worktree', 'remov']],
['help flag', ['worktree', 'remov', '--help']]
])('prints did-you-mean for the %s form', async (_label, argv) => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(argv, '/tmp/repo')
expect(process.exitCode).toBe(1)
expect(logSpy.mock.calls.flat().join('\n')).toContain('Did you mean: orca worktree')
logSpy.mockRestore()
process.exitCode = 0
})
})
describe('orca root help', () => {
it('advertises machine-readable agent discovery', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main([], '/tmp/repo')
expect(logSpy.mock.calls.flat().join('\n')).toContain('agent-context')
logSpy.mockRestore()
})
it('advertises computer-use capabilities discovery', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['--help'], '/tmp/repo')
expect(logSpy.mock.calls[0][0]).toContain(
'computer capabilities Show computer-use provider capabilities'
)
expect(logSpy.mock.calls[0][0]).toContain(
'computer permissions Show or open computer-use permission setup'
)
expect(logSpy.mock.calls[0][0]).toContain(
'computer press-key Press a single key such as Return or Escape'
)
expect(logSpy.mock.calls[0][0]).toContain(
'project setup-existing-folder Make a project available on a host by importing an existing folder'
)
expect(logSpy.mock.calls[0][0]).toContain(
'project setup-create Create independent project host setup metadata'
)
expect(logSpy.mock.calls[0][0]).toContain(
'project setup-update Update project host setup metadata'
)
expect(logSpy.mock.calls[0][0]).toContain(
'project setup-delete Remove a project host setup'
)
expect(logSpy.mock.calls[0][0]).toContain('Agent Sessions And Worktrees:')
expect(logSpy.mock.calls[0][0]).toContain(
'`worktree create --agent` creates a new checkout with an agent.'
)
expect(logSpy.mock.calls[0][0]).toContain(
'orca terminal create --worktree active --command "codex"'
)
expect(logSpy.mock.calls[0][0]).toContain(
'orchestration worker-start Start a supervised worker locally or on a connected Orca server'
)
expect(logSpy.mock.calls[0][0]).toContain(
'orchestration ask Ask the coordinator a blocking question'
)
expect(logSpy.mock.calls[0][0]).toContain(
'orchestration worker-abandon Fence an uncertain worker without claiming it stopped'
)
expect(callMock).not.toHaveBeenCalled()
})
it('progressively discloses Linear commands', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['--help'], '/tmp/repo')
const rootHelp = String(logSpy.mock.calls[0][0])
expect(rootHelp).toContain('Linear:')
expect(rootHelp).toContain('linear Read Linear ticket context for agents')
expect(rootHelp).not.toContain('linear issue')
expect(rootHelp).not.toContain('linear search')
logSpy.mockClear()
await main(['linear', '--help'], '/tmp/repo')
const groupHelp = String(logSpy.mock.calls[0][0])
expect(groupHelp).toContain('orca linear')
expect(groupHelp).toContain('issue')
expect(groupHelp).toContain('search')
expect(groupHelp).not.toContain('--comments')
expect(groupHelp).not.toContain('--attachments')
logSpy.mockClear()
await main(['linear', 'issue', '--help'], '/tmp/repo')
const issueHelp = String(logSpy.mock.calls[0][0])
expect(issueHelp).toContain('orca linear issue [<id>]')
expect(issueHelp).toContain('--comments Include threaded Linear comments')
expect(issueHelp).toContain('--attachments Include attachment metadata and URLs')
expect(issueHelp).toContain('--activity Include issue field-change history')
expect(issueHelp).toContain('--workspace <id> Connected Linear workspace id')
expect(issueHelp).toContain('--id <id> Linear issue key, id, or URL')
logSpy.mockClear()
await main(['linear', 'search', '--help'], '/tmp/repo')
const searchHelp = String(logSpy.mock.calls[0][0])
expect(searchHelp).toContain('orca linear search <query>')
expect(searchHelp).toContain('--workspace <id|all> Connected Linear workspace id, or all')
expect(searchHelp).toContain('--query <text> Text to search across Linear issues')
logSpy.mockClear()
await main(['linear', 'list-issues', '--help'], '/tmp/repo')
const listIssuesHelp = String(logSpy.mock.calls[0][0])
expect(listIssuesHelp).toContain(
'--cursor <cursor> Opaque cursor returned by a previous list-issues page'
)
expect(listIssuesHelp).toContain('--workspace <id|all> Connected Linear workspace id, or all')
expect(listIssuesHelp).not.toContain('Line cursor from a previous read')
expect(callMock).not.toHaveBeenCalled()
})
it('describes worker-read cursors as opaque', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['orchestration', 'worker-read', '--help'], '/tmp/repo')
const help = String(logSpy.mock.calls[0][0])
expect(help).toContain(
'--cursor <cursor> Opaque cursor returned by a previous worker-read page'
)
expect(help).not.toContain('Line cursor from a previous read')
expect(callMock).not.toHaveBeenCalled()
})
it('advertises Linear issue linking on worktree create and set help', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['worktree', 'create', '--help'], '/tmp/repo')
expect(String(logSpy.mock.calls[0][0])).toContain('--linear-issue <identifier-or-url>')
logSpy.mockClear()
await main(['worktree', 'set', '--help'], '/tmp/repo')
const setHelp = String(logSpy.mock.calls[0][0])
expect(setHelp).toContain('--linear-issue <identifier-or-url|null>')
expect(setHelp).toContain('--linear-issue <id|url|null> Linked Linear issue identifier or URL')
expect(callMock).not.toHaveBeenCalled()
})
it('advertises explicit orchestration task display labels', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['orchestration', 'task-create', '--help'], '/tmp/repo')
const help = String(logSpy.mock.calls[0][0])
expect(help).toContain('[--task-title <text>] [--display-name <text>]')
expect(help).toContain('--task-title <text> Concise title for the orchestration task')
expect(help).toContain('--display-name <text> UI label shown for dispatched worker rows')
expect(callMock).not.toHaveBeenCalled()
})
it('hides removed parent-workspace help and scopes create parent selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['--help'], '/tmp/repo')
const rootHelp = String(logSpy.mock.calls[0][0])
expect(rootHelp).not.toContain('--parent-workspace')
expect(rootHelp).toContain('[--parent-worktree <selector>] [--no-parent]')
logSpy.mockClear()
await main(['worktree', 'create', '--help'], '/tmp/repo')
const createHelp = String(logSpy.mock.calls[0][0])
expect(createHelp).not.toContain('--parent-workspace')
expect(createHelp).not.toContain('checkout/workspace')
expect(createHelp).not.toContain('caller workspace')
expect(createHelp).not.toContain('current workspace')
expect(createHelp).not.toContain('active Orca workspace')
expect(createHelp).not.toContain('folderWorkspaceId')
expect(createHelp).toContain('folder:<id>')
expect(createHelp).toContain('folder:<folderId>')
expect(createHelp).toContain('worktree:<worktreeId>')
expect(createHelp).toContain(
'--no-parent only affects Orca lineage; omit --base-branch to use the repo default base'
)
logSpy.mockClear()
await main(['worktree', 'set', '--help'], '/tmp/repo')
const setHelp = String(logSpy.mock.calls[0][0])
expect(setHelp).not.toContain('--parent-workspace')
expect(setHelp).not.toContain('folder:<id>')
expect(setHelp).not.toContain('worktree:<id>')
expect(callMock).not.toHaveBeenCalled()
})
it('distinguishes new worktrees from fresh agent terminals in command help', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
logSpy.mockClear()
await main(['worktree', 'create', '--help'], '/tmp/repo')
expect(String(logSpy.mock.calls[0][0])).toContain('This creates a new checkout.')
expect(String(logSpy.mock.calls[0][0])).toContain(
'orca terminal create --worktree active --command "codex"'
)
logSpy.mockClear()
await main(['terminal', 'create', '--help'], '/tmp/repo')
const terminalHelp = String(logSpy.mock.calls[0][0])
expect(terminalHelp).toContain('Use this, not worktree create')
expect(terminalHelp).toContain(
'orca terminal create --worktree active --command "codex" --json'
)
expect(callMock).not.toHaveBeenCalled()
})
})
describe('orca cli worktree awareness', () => {
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(() => {
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
serveOrcaAppMock.mockReset()
getDefaultUserDataPathMock.mockClear()
addEnvironmentFromPairingCodeMock.mockReset()
listEnvironmentsMock.mockReset()
spawnMock.mockClear()
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'
})
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
}
})
it('builds the current worktree selector from cwd', () => {
expect(buildCurrentWorktreeSelector('/tmp/repo/feature')).toBe(
`path:${path.resolve('/tmp/repo/feature')}`
)
})
it('normalizes active/current worktree selectors to cwd', () => {
const resolved = path.resolve('/tmp/repo/feature')
expect(normalizeWorktreeSelector('active', '/tmp/repo/feature')).toBe(`path:${resolved}`)
expect(normalizeWorktreeSelector('current', '/tmp/repo/feature')).toBe(`path:${resolved}`)
expect(normalizeWorktreeSelector('branch:feature/foo', '/tmp/repo/feature')).toBe(
'branch:feature/foo'
)
})
it('shows the enclosing worktree for `worktree current`', async () => {
queueFixtures(
callMock,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main'),
buildWorktree('/tmp/repo/feature', 'feature/foo'),
buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'duplicate-repo')
]),
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'current', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
expect(logSpy).toHaveBeenCalledTimes(1)
})
it('resolves the invocation cwd from ORCA_CLI_CWD when no cwd is passed', async () => {
// Why: the SSH relay bridge runs the CLI on the Orca host with the remote
// shell's cwd carried in ORCA_CLI_CWD (#7716); cwd-based selectors must
// resolve against it, not the host process cwd.
process.env.ORCA_CLI_CWD = '/tmp/repo/feature/src'
try {
queueFixtures(
callMock,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main'),
buildWorktree('/tmp/repo/feature', 'feature/foo')
]),
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'current', '--json'])
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
} finally {
delete process.env.ORCA_CLI_CWD
}
})
it.skipIf(process.platform === 'win32')(
'prepares and starts Claude Agent Teams in the current Orca terminal',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(['claude-teams'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('agentTeams.prepareLaunch', {
paneKey: 'tab-1:11111111-1111-4111-8111-111111111111',
env: expect.objectContaining({
ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111'
})
})
expect(spawnMock).toHaveBeenCalledWith('claude', ['--teammate-mode', 'auto'], {
stdio: 'inherit',
env: expect.objectContaining({
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX_PANE: '%1'
})
})
}
)
it.skipIf(process.platform === 'win32')(
'passes Claude Agent Teams arguments through to Claude Code',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(
['claude-teams', '--resume', 'session-1', '--model', 'sonnet', 'review this'],
'/tmp/repo'
)
expect(spawnMock).toHaveBeenCalledWith(
'claude',
['--teammate-mode', 'auto', '--resume', 'session-1', '--model', 'sonnet', 'review this'],
{
stdio: 'inherit',
env: expect.objectContaining({
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX_PANE: '%1'
})
}
)
}
)
it.skipIf(process.platform === 'win32')(
'does not duplicate an explicit Claude teammate mode',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(['claude-teams', '--teammate-mode', 'in-process'], '/tmp/repo')
expect(spawnMock).toHaveBeenCalledWith(
'claude',
['--teammate-mode', 'in-process'],
expect.objectContaining({ stdio: 'inherit' })
)
}
)
it('rejects remote `worktree current` without listing worktrees from client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'current', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo/src'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'current is a local cwd shortcut and cannot be resolved against a remote runtime.'
)
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'id:<repo-id>::<path>'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('uses cwd when active is passed to worktree.set', async () => {
queueFixtures(
callMock,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main', 'aaa'),
buildWorktree('/tmp/repo/feature', 'feature/foo')
]),
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature',
comment: 'hello'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'set', '--worktree', 'active', '--comment', 'hello', '--json'],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.set', {
worktree: 'id:repo::/tmp/repo/feature',
displayName: undefined,
linkedIssue: undefined,
comment: 'hello',
parentWorktree: undefined,
noParent: false
})
})
it('passes parent lineage through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_set_parent', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
parentWorktreeId: 'repo::/tmp/repo/parent',
childWorktreeIds: [],
lineage: {
worktreeId: 'repo::/tmp/repo/child',
worktreeInstanceId: 'child-instance',
parentWorktreeId: 'repo::/tmp/repo/parent',
parentWorktreeInstanceId: 'parent-instance',
origin: 'manual',
capture: { source: 'manual-action', confidence: 'explicit' },
createdAt: 1
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--parent-worktree',
'id:repo::/tmp/repo/parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
comment: undefined,
parentWorktree: 'id:repo::/tmp/repo/parent',
noParent: false
})
})
it('resolves current for explicit parent-worktree on set', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/parent', 'feature/parent')]),
okFixture('req_set_parent', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
parentWorktreeId: 'repo::/tmp/repo/parent',
childWorktreeIds: [],
lineage: null
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--parent-worktree',
'current',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
comment: undefined,
parentWorktree: 'id:repo::/tmp/repo/parent',
noParent: false
})
})
it('rejects contradictory parent flags on worktree.set before resolving selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--parent-worktree',
'current',
'--no-parent',
'--json'
],
'/tmp/not-managed'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either --parent-worktree or --no-parent, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects bare parent-worktree on worktree.set', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'set', '--worktree', 'id:repo::/tmp/repo/child', '--parent-worktree', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing required --parent-worktree'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes parent removal through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_clear_parent', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'set', '--worktree', 'id:repo::/tmp/repo/child', '--no-parent', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
comment: undefined,
workspaceStatus: undefined,
parentWorktree: undefined,
noParent: true
})
})
it('passes Linear URL metadata through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_set_linear', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'https://linear.app/stably/issue/STA-335/test-issue',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably',
comment: undefined,
workspaceStatus: undefined,
parentWorktree: undefined,
noParent: false
})
})
it('clears all Linear metadata through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_clear_linear', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
linkedLinearIssue: null,
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'null',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
linkedLinearIssue: null,
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null,
comment: undefined,
workspaceStatus: undefined,
parentWorktree: undefined,
noParent: false
})
})
it('rejects invalid Linear issue values on worktree.set before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--linear-issue',
'not-a-linear-link',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Pass a Linear issue identifier like STA-335'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes workspace status through worktree.set', async () => {
queueFixtures(
callMock,
okFixture('req_set_status', {
worktree: {
...buildWorktree('/tmp/repo/child', 'feature/child'),
workspaceStatus: 'in-review'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'set',
'--worktree',
'id:repo::/tmp/repo/child',
'--workspace-status',
'in-review',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/repo/child',
displayName: undefined,
linkedIssue: undefined,
comment: undefined,
workspaceStatus: 'in-review',
parentWorktree: undefined,
noParent: false
})
})
it('passes Linear issue metadata through worktree.create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create_linear', {
worktree: {
...buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1'),
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'https://linear.app/stably/issue/STA-335/test-issue',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: 'stably',
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('normalizes bare Linear identifiers through worktree.create', async () => {
queueFixtures(
callMock,
okFixture('req_create_linear_id', {
worktree: {
...buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1'),
linkedLinearIssue: 'STA-335'
},
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'sta-335',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
linkedLinearIssue: 'STA-335',
linkedLinearIssueWorkspaceId: null,
linkedLinearIssueOrganizationUrlKey: null,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
noParent: true,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('rejects null Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'null',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Omit --linear-issue on create'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects invalid Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'not-a-linear-link',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Pass a Linear issue identifier like STA-335'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects missing Linear issue values on worktree.create before RPC', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'feature',
'--linear-issue',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --linear-issue'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes explicit activation through worktree.create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1')
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'feature', '--activate', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: true,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('resolves project and host flags to the matching repo for worktree.create', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-local',
path: '/tmp/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-gpu',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
}),
okFixture('req_create', {
worktree: buildWorktree('/srv/orca/feature', 'feature', 'abc', 'repo-gpu'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--name',
'feature',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'projectHostSetup.list')
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-gpu',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
noParent: true,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('resolves project-host-setup directly for worktree.create', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-gpu',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
}),
okFixture('req_create', {
worktree: buildWorktree('/srv/orca/feature', 'feature', 'abc', 'repo-gpu'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--project-host-setup',
'setup-gpu',
'--name',
'feature',
'--no-parent',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(
2,
'worktree.create',
expect.objectContaining({ repo: 'id:repo-gpu' })
)
})
it('rejects mixing repo and project target flags on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-local',
'--project',
'github:stablyai/orca',
'--name',
'feature',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either --repo or project target flags, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes an explicit parent through worktree.create without cwd inference', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: {
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
parentWorktreeId: 'repo-1::/tmp/repo/parent',
lineage: {
worktreeId: 'repo-1::/tmp/repo/child',
worktreeInstanceId: 'child-instance',
parentWorktreeId: 'repo-1::/tmp/repo/parent',
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
}
},
lineage: {
worktreeId: 'repo-1::/tmp/repo/child',
worktreeInstanceId: 'child-instance',
parentWorktreeId: 'repo-1::/tmp/repo/parent',
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'id:repo-1::/tmp/repo/parent',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'id:repo-1::/tmp/repo/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes traditional parent-worktree selectors through parentWorktree', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: {
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
parentWorktreeId: 'repo-1::/tmp/repo/parent'
},
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'branch:feature/parent',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'branch:feature/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes workspace-key parent-worktree selectors through parentWorkspace', async () => {
const cases = [
{ selector: 'folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
},
{ selector: 'id:folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'id:worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
}
]
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const testCase of cases) {
callMock.mockReset()
queueFixtures(
callMock,
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: testCase.parentWorkspace,
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
testCase.selector,
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: testCase.parentWorkspace,
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
}
})
it('passes folder workspace environment lineage through worktree.create', async () => {
process.env.ORCA_WORKSPACE_ID = 'folder:folder-1'
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'env-workspace', confidence: 'inferred' },
createdAt: 1
},
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
envParentWorkspace: 'folder:folder-1',
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('resolves current for explicit parent-worktree on create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/parent', 'feature/parent', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'current',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'id:repo-1::/tmp/repo/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes active/current folder workspace parent selectors through parentWorkspace on create', async () => {
const folderWorkspace = {
...buildWorktree('/tmp/folder', '', '', 'folder-workspace:group-1'),
id: 'folder:folder-1',
repoId: 'folder-workspace:group-1',
displayName: 'Folder'
}
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const parentSelector of ['current', 'active']) {
callMock.mockReset()
queueFixtures(
callMock,
worktreeListFixture([folderWorkspace]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
parentSelector,
'--json'
],
'/tmp/folder/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:folder-1',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
}
})
it('rejects contradictory parent flags on worktree.create before resolving selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'current',
'--no-parent',
'--json'
],
'/tmp/not-managed'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either one parent selector or --no-parent.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects removed parent-workspace on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
const outputModes = [[], ['--json']]
for (const outputArgs of outputModes) {
logSpy.mockClear()
errSpy.mockClear()
process.exitCode = priorExitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-workspace',
'folder:folder-1',
...outputArgs
],
'/tmp/repo'
)
const output = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(output).toContain('Unknown flag --parent-workspace for command: worktree create')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
}
process.exitCode = priorExitCode
})
it('rejects bare parent-worktree on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing required --parent-worktree'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('reports runtime parent selector failures without hidden flag guidance', async () => {
callMock.mockRejectedValueOnce(
new RuntimeRpcFailureError({
id: 'req_create',
ok: false,
error: {
code: 'LINEAGE_PARENT_NOT_FOUND',
message: 'Parent selector was not found.',
data: {
nextSteps: [
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<worktreeId>, id:<repo-id>::<path>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.'
]
}
},
_meta: { runtimeId: 'runtime-1' }
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'folder:missing',
'--json'
],
'/tmp/repo'
)
const output = String(logSpy.mock.calls[0][0])
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:missing',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
expect(output).toContain('"ok": false')
expect(output).toContain('Parent selector was not found.')
expect(output).toContain('--parent-worktree selector')
expect(output).not.toContain('--parent-workspace')
expect(errSpy).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes no-parent through worktree.create and skips cwd inference', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--no-parent', '--json'],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
noParent: true,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('passes caller terminal handle through worktree.create with cwd fallback', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_parent'
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledTimes(2)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: 'term_parent',
cliProvenanceRequest: { callerTerminalHandle: 'term_parent' }
})
})
it('marks every worktree.create as CLI-created even from an external shell', async () => {
// Why: the sidebar badge/filter must catch hand-typed creates too, so the
// provenance request is sent with no terminal handle rather than omitted.
delete process.env.ORCA_TERMINAL_HANDLE
queueFixtures(
callMock,
okFixture('req_create_external', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--no-parent', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'worktree.create',
expect.objectContaining({ cliProvenanceRequest: {} })
)
})
it('starts a foreground headless server through `serve`', async () => {
serveOrcaAppMock.mockResolvedValue(0)
process.env.ORCA_ENVIRONMENT = 'stale-env'
await main(
['serve', '--json', '--port', '6768', '--pairing-address', '100.64.1.20', '--no-pairing'],
'/tmp/repo'
)
expect(serveOrcaAppMock).toHaveBeenCalledWith({
json: true,
port: '6768',
pairingAddress: '100.64.1.20',
noPairing: true,
mobilePairing: false,
recipeJson: false,
projectRoot: null
})
})
it('starts a foreground headless server with mobile pairing enabled', async () => {
serveOrcaAppMock.mockResolvedValue(0)
await main(
['serve', '--pairing-address', '100.64.1.20', '--mobile-pairing', '--json'],
'/tmp/repo'
)
expect(serveOrcaAppMock).toHaveBeenCalledWith({
json: true,
port: null,
pairingAddress: '100.64.1.20',
noPairing: false,
mobilePairing: true,
recipeJson: false,
projectRoot: null
})
})
it('starts a recipe JSON headless server for VM recipes', async () => {
serveOrcaAppMock.mockResolvedValue(0)
await main(
[
'serve',
'--pairing-address',
'wss://sandbox.example.com',
'--project-root',
'/workspace/repo',
'--recipe-json'
],
'/tmp/repo'
)
expect(serveOrcaAppMock).toHaveBeenCalledWith({
json: false,
port: null,
pairingAddress: 'wss://sandbox.example.com',
noPairing: false,
mobilePairing: false,
recipeJson: true,
projectRoot: '/workspace/repo'
})
})
it('runs vm recipe doctor locally without contacting the app runtime', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-'))
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
const startScript = path.join(repoPath, 'scripts', 'orca-vm', 'start.sh')
const cleanupScript = path.join(repoPath, 'scripts', 'orca-vm', 'cleanup.sh')
writeFileSync(startScript, '#!/bin/sh\n')
writeFileSync(cleanupScript, '#!/bin/sh\n')
chmodSync(startScript, 0o755)
chmodSync(cleanupScript, 0o755)
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
' create: ./scripts/orca-vm/start.sh',
' destroy: ./scripts/orca-vm/cleanup.sh'
].join('\n')
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['vm', 'recipe', 'doctor', 'cloud-sandbox', '--repo-path', repoPath, '--json'])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string }[]
}
if (!output.ok) {
throw new Error(JSON.stringify(output))
}
expect(output.ok).toBe(true)
expect(output.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'orca_yaml.parse', status: 'pass' }),
expect.objectContaining({ id: 'recipe.exists', status: 'pass' }),
expect.objectContaining({ id: 'recipe.create', status: 'pass' }),
expect.objectContaining({ id: 'recipe.destroy', status: 'pass' })
])
)
expect(callMock).not.toHaveBeenCalled()
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('warns when vm recipe doctor finds no cleanup hook', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-'))
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(path.join(repoPath, 'scripts', 'orca-vm', 'start.sh'), '#!/bin/sh\n')
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: manual-sandbox',
' name: Manual Sandbox',
' create: ./scripts/orca-vm/start.sh'
].join('\n')
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['vm', 'recipe', 'doctor', 'manual-sandbox', '--repo-path', repoPath, '--json'])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string; remediation?: string }[]
}
if (!output.ok) {
throw new Error(JSON.stringify(output))
}
expect(output.ok).toBe(true)
expect(output.checks).toContainEqual(
expect.objectContaining({
id: 'recipe.destroy',
status: 'warn',
remediation: 'Add destroy or explicitly set destroy: none.'
})
)
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('runs vm recipe doctor provision mode and invokes cleanup', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-provision-'))
const pairingCode = encodePairingOffer({
v: PAIRING_OFFER_VERSION,
endpoint: 'ws://sandbox.example.com:6767',
deviceToken: 'token',
publicKeyB64: 'public-key'
})
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(
path.join(repoPath, 'scripts', 'orca-vm', 'start.js'),
[
'console.log(JSON.stringify({',
' schemaVersion: 1,',
` pairingCode: ${JSON.stringify(pairingCode)},`,
" projectRoot: '/workspace/repo'",
'}))'
].join('\n')
)
writeFileSync(
path.join(repoPath, 'scripts', 'orca-vm', 'cleanup.js'),
[
"const fs = require('fs')",
"const input = fs.readFileSync(0, 'utf8')",
'const payload = JSON.parse(input)',
"fs.writeFileSync('cleanup-ran.json', JSON.stringify(payload))"
].join('\n')
)
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
` create: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/start.js`)}`,
` destroy: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/cleanup.js`)}`
].join('\n')
)
const { EventEmitter } = await import('node:events')
const startChild = 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()
})
const cleanupChild = 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()
})
spawnMock
.mockImplementationOnce(() => {
process.nextTick(() => {
startChild.stdout.emit(
'data',
JSON.stringify({
schemaVersion: 1,
pairingCode,
projectRoot: '/workspace/repo'
})
)
startChild.emit('exit', 0, null)
startChild.emit('close', 0, null)
})
return startChild
})
.mockImplementationOnce(() => {
process.nextTick(() => {
cleanupChild.emit('exit', 0, null)
cleanupChild.emit('close', 0, null)
})
return cleanupChild
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main([
'vm',
'recipe',
'doctor',
'cloud-sandbox',
'--repo-path',
repoPath,
'--provision',
'--json'
])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string }[]
provisionTranscript?: {
provision: { exitCode: number | null; stdout: string; stderr: string }
destroy?: { exitCode: number | null; stdout: string; stderr: string }
}
}
if (!output.ok) {
throw new Error(JSON.stringify(output))
}
expect(output.ok).toBe(true)
expect(output.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'recipe.provision', status: 'pass' }),
expect.objectContaining({ id: 'recipe.result.endpoint.public_ws', status: 'warn' }),
expect.objectContaining({ id: 'recipe.result.project_root', status: 'pass' }),
expect.objectContaining({ id: 'recipe.destroy.run', status: 'pass' })
])
)
// The transcript carries both stages so the agent can self-diagnose.
expect(output.provisionTranscript?.provision.exitCode).toBe(0)
expect(output.provisionTranscript?.destroy?.exitCode).toBe(0)
const cleanupPayload = JSON.parse(
String(vi.mocked(cleanupChild.stdin.end).mock.calls[0]?.[0])
) as { recipeId: string; recipeResult: { projectRoot: string } }
expect(cleanupPayload).toMatchObject({
recipeId: 'cloud-sandbox',
recipeResult: { projectRoot: '/workspace/repo' }
})
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('returns the full create transcript when provision fails so the agent can self-diagnose', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-provision-fail-'))
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(path.join(repoPath, 'scripts', 'orca-vm', 'start.js'), 'process.exit(0)')
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
` create: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/start.js`)}`,
' destroy: none'
].join('\n')
)
const { EventEmitter } = await import('node:events')
const startChild = 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()
})
// create emits a non-JSON line to stdout + a real diagnostic to stderr, then exits 0
spawnMock.mockImplementationOnce(() => {
process.nextTick(() => {
startChild.stdout.emit('data', 'Provisioning sandbox...\n')
startChild.stderr.emit('data', 'vercel: error: missing scope\n')
startChild.emit('exit', 0, null)
startChild.emit('close', 0, null)
})
return startChild
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main([
'vm',
'recipe',
'doctor',
'cloud-sandbox',
'--repo-path',
repoPath,
'--provision',
'--json'
])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string }[]
provisionTranscript?: {
provision: {
exitCode: number | null
stdout: string
stderr: string
parseError?: string
}
}
}
expect(output.ok).toBe(false)
expect(output.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'recipe.provision', status: 'fail' })
])
)
// The agent gets the full create output, not a 500-char tail.
expect(output.provisionTranscript?.provision.stdout).toContain('Provisioning sandbox...')
expect(output.provisionTranscript?.provision.stderr).toContain('missing scope')
expect(output.provisionTranscript?.provision.parseError).toBeTruthy()
process.exitCode = priorExitCode
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('rejects recipe JSON output without a project root', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['serve', '--recipe-json'], '/tmp/repo')
expect(serveOrcaAppMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Recipe JSON output requires --project-root.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects recipe JSON output with mobile pairing', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['serve', '--recipe-json', '--project-root', '/workspace/repo', '--mobile-pairing'],
'/tmp/repo'
)
expect(serveOrcaAppMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Recipe JSON output requires runtime pairing; remove --mobile-pairing.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects contradictory serve pairing flags', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['serve', '--mobile-pairing', '--no-pairing', '--json'], '/tmp/repo')
expect(serveOrcaAppMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Use either --mobile-pairing or --no-pairing, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects invalid serve ports before launching the app', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['serve', '--port', 'not-a-port', '--json'], '/tmp/repo')
expect(serveOrcaAppMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Invalid --port value: not-a-port'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects value-less serve ports before launching the app', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['serve', '--port', '--json'], '/tmp/repo')
expect(serveOrcaAppMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --port.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('lists saved environments even when ORCA_ENVIRONMENT is set', async () => {
process.env.ORCA_ENVIRONMENT = 'stale-env'
listEnvironmentsMock.mockReturnValue([addEnvironmentFromPairingCodeMock()])
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['environment', 'list', '--json'], '/tmp/repo')
expect(listEnvironmentsMock).toHaveBeenCalledWith('/tmp/orca-user-data')
expect(callMock).not.toHaveBeenCalled()
expect(logSpy.mock.calls[0]?.[0]).not.toContain('token')
expect(logSpy.mock.calls[0]?.[0]).not.toContain('publicKeyB64')
})
it('adds saved environments even when ORCA_ENVIRONMENT is set', async () => {
process.env.ORCA_ENVIRONMENT = 'stale-env'
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['environment', 'add', '--name', 'desk', '--pairing-code', 'orca://pair#abc', '--json'],
'/tmp/repo'
)
expect(addEnvironmentFromPairingCodeMock).toHaveBeenCalledWith('/tmp/orca-user-data', {
name: 'desk',
pairingCode: 'orca://pair#abc'
})
expect(callMock).not.toHaveBeenCalled()
expect(logSpy.mock.calls[0]?.[0]).not.toContain('token')
expect(logSpy.mock.calls[0]?.[0]).not.toContain('publicKeyB64')
})
it('resolves repo.add paths against the invoking cli cwd', async () => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: path.resolve('/tmp/repo/apps/web'),
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['repo', 'add', '--path', './apps/web', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: path.resolve('/tmp/repo/apps/web')
})
})
it('lists projects through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_list', {
projects: [
{
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
providerIdentity: {
provider: 'github',
owner: 'stablyai',
repo: 'orca'
},
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
}
]
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'list', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('project.list')
})
it('filters project host setups locally after fetching setup compatibility state', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-local',
path: '/tmp/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-remote',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-remote',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['project', 'setups', '--project', 'github:stablyai/orca', '--host', 'runtime:gpu'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.list')
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-remote')
expect(logSpy.mock.calls[0]?.[0]).not.toContain('setup-local')
})
it('sets up an existing project folder with a path resolved against the local cli cwd', async () => {
queueFixtures(
callMock,
okFixture('req_project_setup', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-1',
path: path.resolve('/tmp/orca'),
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 1
},
repo: {
id: 'repo-1',
path: path.resolve('/tmp/orca'),
displayName: 'Orca',
badgeColor: '#7c3aed',
addedAt: 1
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-existing-folder',
'--project',
'github:stablyai/orca',
'--host',
'local',
'--path',
'..',
'--kind',
'git',
'--display-name',
'Orca',
'--json'
],
'/tmp/orca/worktrees/feature'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.setupExistingFolder', {
projectId: 'github:stablyai/orca',
hostId: 'local',
path: path.resolve('/tmp/orca/worktrees'),
kind: 'git',
displayName: 'Orca'
})
})
it('rejects remote project setup relative paths instead of resolving against client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'project',
'setup-existing-folder',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--path',
'./orca',
'--pairing-code',
'remote-runtime',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Remote project setup requires --path to be an absolute path on the remote server.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects remote repo.add relative paths instead of resolving against client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['repo', 'add', '--path', './apps/web', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Remote repo add requires --path to be an absolute path on the remote server.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('sends remote repo.add absolute paths unchanged', async () => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: '/srv/orca/web',
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['repo', 'add', '--path', '/srv/orca/web', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: '/srv/orca/web'
})
})
it.each(['C:\\repo', 'C:/repo', '\\\\server\\share\\repo', '//server/share/repo'])(
'sends remote repo.add server absolute path %s unchanged',
async (serverPath) => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: serverPath,
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['repo', 'add', '--path', serverPath, '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: serverPath
})
}
)
it('opts into setup and activation when worktree.create runs hooks', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/feature', 'feature', 'abc', 'repo-1')
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'feature', '--run-hooks', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'feature',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: true,
activate: true,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('passes agent prompt and setup policy through worktree.create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/agent-task', 'agent-task', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'agent-task',
'--agent',
'codex',
'--prompt',
'hi',
'--setup',
'run',
'--json'
],
'/tmp/repo/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'agent-task',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: true,
setupDecision: 'run',
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {},
startupAgent: 'codex',
startupPrompt: 'hi'
})
})
it('infers the repo from the current worktree on worktree.create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/agent-task', 'agent-task', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--name',
'agent-task',
'--agent',
'codex',
'--prompt',
'hi',
'--json'
],
'/tmp/repo/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'agent-task',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: true,
parentWorktree: undefined,
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {},
startupAgent: 'codex',
startupPrompt: 'hi'
})
})
it('rejects prompt without agent on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--prompt', 'hi', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'--prompt requires --agent'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects unknown agents on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--agent', 'wat', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: 'Unknown TUI agent "wat"'
}
})
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects agent, prompt, and setup flags without values on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--agent'],
'/tmp/repo'
)
expect(callMock.mock.calls.some(([method]) => method === 'worktree.create')).toBe(false)
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --agent'
)
callMock.mockClear()
logSpy.mockClear()
errSpy.mockClear()
process.exitCode = priorExitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--agent',
'codex',
'--prompt'
],
'/tmp/repo'
)
expect(callMock.mock.calls.some(([method]) => method === 'worktree.create')).toBe(false)
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --prompt'
)
callMock.mockClear()
logSpy.mockClear()
errSpy.mockClear()
process.exitCode = priorExitCode
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--setup'],
'/tmp/repo'
)
expect(callMock.mock.calls.some(([method]) => method === 'worktree.create')).toBe(false)
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing value for --setup'
)
process.exitCode = priorExitCode
})
it('rejects contradictory setup flags on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--run-hooks',
'--setup',
'skip',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either --run-hooks or --setup run'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes explicit focus through terminal.create', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'RUNNER'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'RUNNER',
'--focus',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: undefined,
title: 'RUNNER',
focus: true,
presentation: 'focused'
})
})
it('prints terminal.read fallback screen lines in json mode', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_read', {
terminal: {
handle: 'term_worker',
status: 'running',
tail: ['Claude Code', 'Checking files', 'Waiting for input'],
truncated: false,
limited: true,
oldestCursor: '0',
nextCursor: '3000',
latestCursor: '3000',
returnedLineCount: 3
}
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['terminal', 'read', '--terminal', 'term_worker', '--limit', '120', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.read', {
terminal: 'term_worker',
limit: 120
})
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.result.terminal.tail).toEqual([
'Claude Code',
'Checking files',
'Waiting for input'
])
})
it('keeps interactive Codex startup commands backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex',
'--command',
'codex',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex',
title: 'Codex',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps explicit focus semantics when forcing Codex through the renderer path', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex',
'--command',
'codex',
'--focus',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex',
title: 'Codex',
focus: true,
presentation: 'focused',
rendererBacked: true,
activate: true
})
})
it('does not force the visible terminal path for explicit Codex exec commands', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex exec'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex exec',
'--command',
'codex exec summarize',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex exec summarize',
title: 'Codex exec',
focus: false
})
})
it('does not force the visible terminal path for Codex exec commands after global options', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex exec'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex exec',
'--command',
'codex -m gpt-5 --sandbox workspace-write exec summarize',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 --sandbox workspace-write exec summarize',
title: 'Codex exec',
focus: false
})
})
it('does not force the visible terminal path for Codex review commands after long options', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex review'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex review',
'--command',
'codex --model=gpt-5 --sandbox=workspace-write review',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex --model=gpt-5 --sandbox=workspace-write review',
title: 'Codex review',
focus: false
})
})
it('does not force the visible terminal path for Codex help commands', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex help'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex help',
'--command',
'codex --help',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex --help',
title: 'Codex help',
focus: false
})
})
it('keeps Codex prompts after global options backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Codex prompt'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Codex prompt',
'--command',
'codex -m gpt-5 "fix the flaky test"',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'codex -m gpt-5 "fix the flaky test"',
title: 'Codex prompt',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps interactive Claude startup commands backgrounded unless focus is explicit', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Claude'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Claude',
'--command',
'claude',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'claude',
title: 'Claude',
focus: false,
rendererBacked: true,
activate: false
})
})
it('keeps Claude print commands on the background terminal path', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/tmp/repo/feature',
title: 'Claude print'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'path:/tmp/repo/feature',
'--title',
'Claude print',
'--command',
'claude -p "summarize"',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'path:/tmp/repo/feature',
command: 'claude -p "summarize"',
title: 'Claude print',
focus: false
})
})
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_show', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'show', '--worktree', 'current', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
})
it('formats group orchestration sends in text mode', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_sender'
callMock.mockResolvedValueOnce({
id: 'req_send',
ok: true,
result: {
messages: [{ id: 'msg_1' }, { id: 'msg_2' }],
recipients: 2
},
_meta: {
runtimeId: 'runtime-1'
}
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['orchestration', 'send', '--to', '@all', '--subject', 'hello'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('orchestration.send', {
from: 'term_sender',
to: '@all',
subject: 'hello',
body: undefined,
type: undefined,
priority: undefined,
threadId: undefined,
payload: undefined,
devMode: false
})
expect(logSpy).toHaveBeenCalledWith('Sent 2 messages to 2 recipients')
})
it('rejects no-flag orchestration reset before calling the runtime', async () => {
await main(['orchestration', 'reset'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
})
it.each([
{
args: ['orchestration', 'reset', '--all'],
params: { all: true, tasks: undefined, messages: undefined },
reset: 'all'
},
{
args: ['orchestration', 'reset', '--tasks'],
params: { all: undefined, tasks: true, messages: undefined },
reset: 'tasks'
},
{
args: ['orchestration', 'reset', '--messages'],
params: { all: undefined, tasks: undefined, messages: true },
reset: 'messages'
}
])('passes explicit reset flags through for $args', async ({ args, params, reset }) => {
callMock.mockResolvedValueOnce(okFixture('req_reset', { reset }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(args, '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('orchestration.reset', params)
})
it.each([
['orchestration', 'reset', '--tasks', '--messages'],
['orchestration', 'reset', '--all', '--tasks']
])('rejects conflicting reset scopes for $args', async (...args) => {
await main(args, '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
})
it('rejects unknown task-update status with an enum-aware error', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_coord'
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['orchestration', 'task-update', '--id', 'task_x', '--status', 'complete'],
'/tmp/repo'
)
const output = [...errSpy.mock.calls, ...logSpy.mock.calls]
.flat()
.map((v) => (typeof v === 'string' ? v : JSON.stringify(v)))
.join('\n')
expect(output).toContain("invalid status 'complete'")
expect(output).toContain('pending, ready, dispatched, completed, failed, blocked')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
// Reset exitCode so subsequent tests don't inherit the failure.
process.exitCode = priorExitCode
errSpy.mockRestore()
})
it('passes the caller terminal handle through orchestration task-create', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_creator'
callMock.mockResolvedValueOnce({
id: 'req_task_create',
ok: true,
result: {
task: { id: 'task_1', status: 'ready' }
},
_meta: {
runtimeId: 'runtime-1'
}
})
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'orchestration',
'task-create',
'--spec',
'spawn child workspace',
'--task-title',
'Child workspace',
'--display-name',
'Spawn child workspace'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('orchestration.taskCreate', {
spec: 'spawn child workspace',
taskTitle: 'Child workspace',
displayName: 'Spawn child workspace',
deps: undefined,
parent: undefined,
callerTerminalHandle: 'term_creator'
})
})
it('passes dev mode to injected orchestration dispatches', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_sender'
process.env.ORCA_USER_DATA_PATH = '/tmp/orca-dev'
callMock.mockResolvedValueOnce({
id: 'req_dispatch',
ok: true,
result: {
dispatch: { id: 'ctx_1', task_id: 'task_1', status: 'dispatched' }
},
_meta: {
runtimeId: 'runtime-1'
}
})
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['orchestration', 'dispatch', '--task', 'task_1', '--to', 'term_worker', '--inject'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('orchestration.dispatch', {
task: 'task_1',
to: 'term_worker',
from: 'term_sender',
inject: true,
devMode: true
})
})
it('passes dev mode from an explicit dev CLI marker with a custom profile path', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_sender'
process.env.ORCA_USER_DATA_PATH = '/tmp/federation-acceptance-profile'
process.env.ORCA_DEV_CLI_INVOCATION = '1'
callMock.mockResolvedValueOnce({
id: 'req_dispatch',
ok: true,
result: {
dispatch: { id: 'ctx_1', task_id: 'task_1', status: 'dispatched' }
},
_meta: {
runtimeId: 'runtime-1'
}
})
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['orchestration', 'dispatch', '--task', 'task_1', '--to', 'term_worker', '--inject'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith(
'orchestration.dispatch',
expect.objectContaining({ devMode: true })
)
})
it('uses the resolved enclosing worktree for terminal consumers', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_term', { terminals: [], totalCount: 0, truncated: false })
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['terminal', 'list', '--worktree', 'active', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.list', {
worktree: 'id:repo::/tmp/repo/feature',
limit: undefined
})
})
it('rejects implicit remote terminal create instead of resolving from client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['terminal', 'create', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/client/repo/src'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Remote terminal create requires --worktree'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('sends explicit remote terminal create worktree selectors unchanged', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/srv/orca/feature',
title: 'Server terminal'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'id:repo-1::/srv/orca/feature',
'--pairing-code',
'remote-runtime',
'--json'
],
'/tmp/client/repo/src'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'id:repo-1::/srv/orca/feature',
command: undefined,
title: undefined,
focus: false
})
})
it('collects and formats memory diagnostics', async () => {
queueFixtures(
callMock,
okFixture('req_memory', {
app: {
cpu: 1.25,
memory: 1024 * 1024,
main: { cpu: 0.5, memory: 512 * 1024 },
renderer: { cpu: 0.5, memory: 384 * 1024 },
other: { cpu: 0.25, memory: 128 * 1024 },
history: [1024 * 1024]
},
worktrees: [
{
worktreeId: 'repo::/tmp/repo/feature',
worktreeName: 'feature',
repoId: 'repo',
repoName: 'Orca',
cpu: 2.5,
memory: 1024 * 1024,
sessions: [
{
sessionId: 'pty-1',
paneKey: null,
pid: 123,
cpu: 2.5,
memory: 1024 * 1024
}
],
history: [1024 * 1024]
}
],
host: {
totalMemory: 8 * 1024 * 1024,
freeMemory: 2 * 1024 * 1024,
availableMemory: 2 * 1024 * 1024,
availableMemorySource: 'free-memory',
usedMemory: 6 * 1024 * 1024,
memoryUsagePercent: 75,
cpuCoreCount: 8,
loadAverage1m: 1.25
},
processMemoryMetric: 'rss',
totalCpu: 3.75,
totalMemory: 2 * 1024 * 1024,
collectedAt: 1000
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['diagnostics', 'memory'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('diagnostics.memory')
const output = logSpy.mock.calls.flat().join('\n')
expect(output).toContain('totalMemory: 2.0 MB')
expect(output).toContain('processMemoryMetric: summed RSS; shared or aliased pages may repeat')
expect(output).toContain('hostAvailable: 2.0 MB (free-memory)')
expect(output).toContain('app: 1.0 MB')
expect(output).toContain('- feature 1.0 MB 2.5% 1 session')
})
it('exits nonzero when terminal wait returns an unsatisfied blocked result', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_worker'
callMock.mockResolvedValueOnce({
id: 'req_terminal_wait',
ok: true,
result: {
wait: {
handle: 'term_worker',
condition: 'tui-idle',
satisfied: false,
status: 'running',
exitCode: null,
blockedReason: 'codex-cwd-prompt'
}
},
_meta: {
runtimeId: 'runtime-1'
}
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['terminal', 'wait', '--terminal', 'term_worker', '--for', 'tui-idle'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith(
'terminal.wait',
{
terminal: 'term_worker',
for: 'tui-idle',
timeoutMs: undefined
},
{
timeoutMs: 300000
}
)
expect(logSpy.mock.calls.flat().join('\n')).toContain('blockedReason: codex-cwd-prompt')
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('does not force remote Codex terminal creates through a local renderer path', async () => {
queueFixtures(
callMock,
okFixture('req_terminal_create', {
terminal: {
handle: 'term_1',
worktreeId: 'repo-1::/srv/orca/feature',
title: 'Codex'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'terminal',
'create',
'--worktree',
'id:repo-1::/srv/orca/feature',
'--command',
'codex',
'--title',
'Codex',
'--pairing-code',
'remote-runtime',
'--json'
],
'/tmp/client/repo/src'
)
expect(callMock).toHaveBeenCalledWith('terminal.create', {
worktree: 'id:repo-1::/srv/orca/feature',
command: 'codex',
title: 'Codex',
focus: false
})
})
it('does not resolve implicit remote browser targets from client cwd', async () => {
queueFixtures(
callMock,
okFixture('req_tab_current', {
tab: {
browserPageId: 'page-1',
index: 0,
url: 'https://example.com',
title: 'Example',
active: true,
worktreeId: 'repo-1::/srv/orca/feature'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['tab', 'current', '--pairing-code', 'remote-runtime', '--json'], '/tmp/client/src')
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('browser.tabCurrent', {
worktree: undefined
})
})
it('passes emulator gesture points through to the runtime', async () => {
const points = [
{ type: 'begin', x: 0.5, y: 0.98, edge: 3 },
{ type: 'move', x: 0.5, y: 0.4, edge: 3 },
{ type: 'end', x: 0.5, y: 0.2, edge: 3 }
]
queueFixtures(callMock, okFixture('req_emulator_gesture', { ok: true }))
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['emulator', 'gesture', JSON.stringify(points), '--worktree', 'id:wt-1', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('emulator.gesture', {
points,
device: undefined,
emulator: undefined,
worktree: 'id:wt-1'
})
})
it('rejects emulator gesture points outside normalized coordinates', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'emulator',
'gesture',
JSON.stringify([
{ type: 'begin', x: 1.2, y: 0.8 },
{ type: 'end', x: 0.5, y: 0.2 }
]),
'--worktree',
'id:wt-1',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: '--points[0].x must be between 0 and 1'
}
})
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects emulator gesture points with invalid edge markers', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'emulator',
'gesture',
JSON.stringify([
{ type: 'begin', x: 0.5, y: 0.98, edge: 8 },
{ type: 'end', x: 0.5, y: 0.2, edge: 8 }
]),
'--worktree',
'id:wt-1',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: 'gesture point 0 edge must be an integer between 0 and 4'
}
})
process.exitCode = priorExitCode
})
it('creates an automation for the enclosing worktree by default', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]),
okFixture('req_automation_create', {
automation: {
id: 'auto-1',
name: 'Daily review',
prompt: 'Review open changes',
agentId: 'codex',
projectId: 'repo-1',
executionTargetType: 'local',
executionTargetId: 'local',
schedulerOwner: 'local_host_service',
workspaceMode: 'existing',
workspaceId: 'repo-1::/tmp/repo/feature',
baseBranch: null,
timezone: 'America/Toronto',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: 1,
enabled: true,
nextRunAt: 2,
missedRunPolicy: 'run_once_within_grace',
missedRunGraceMinutes: 720,
createdAt: 1,
updatedAt: 1
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--json'
],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(2, 'automation.create', {
name: 'Daily review',
prompt: 'Review open changes',
agentId: 'codex',
repo: undefined,
workspace: 'id:repo-1::/tmp/repo/feature',
workspaceMode: 'existing',
baseBranch: undefined,
reuseSession: undefined,
timezone: undefined,
enabled: undefined,
missedRunGraceMinutes: undefined,
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: expect.any(Number)
})
})
it('resolves project and host flags for automation create', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-local',
path: '/tmp/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-gpu',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
}),
okFixture('req_automation_create', {
automation: { id: 'auto-1', name: 'GPU review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'automations',
'create',
'--name',
'GPU review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'projectHostSetup.list')
expect(callMock).toHaveBeenNthCalledWith(
2,
'automation.create',
expect.objectContaining({
repo: 'id:repo-gpu',
runContext: {
kind: 'workspace-run',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
projectHostSetupId: 'setup-gpu',
repoId: 'repo-gpu',
path: '/srv/orca'
},
workspace: undefined,
workspaceMode: 'new_per_run'
})
)
})
it('resolves project-host-setup flags for automation edit with explicit run context', async () => {
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-gpu',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
}),
okFixture('req_edit', {
automation: { id: 'auto-1', name: 'GPU review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['automations', 'edit', 'auto-1', '--project-host-setup', 'setup-gpu', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'projectHostSetup.list')
expect(callMock).toHaveBeenNthCalledWith(
2,
'automation.update',
expect.objectContaining({
id: 'auto-1',
updates: expect.objectContaining({
repo: 'id:repo-gpu',
runContext: {
kind: 'workspace-run',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
projectHostSetupId: 'setup-gpu',
repoId: 'repo-gpu',
path: '/srv/orca'
}
})
})
)
})
it('passes automation source context JSON through create', async () => {
const sourceContext = {
kind: 'task-source',
provider: 'github',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
projectHostSetupId: 'setup-gpu',
repoId: 'repo-gpu',
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' },
accountLabel: 'gpu-bot'
}
queueFixtures(
callMock,
okFixture('req_automation_create', {
automation: { id: 'auto-1', name: 'GPU task review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'automations',
'create',
'--name',
'GPU task review',
'--trigger',
'daily',
'--prompt',
'Review open work',
'--provider',
'codex',
'--repo',
'id:repo-gpu',
'--source-context',
JSON.stringify(sourceContext),
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(
1,
'automation.create',
expect.objectContaining({
repo: 'id:repo-gpu',
sourceContext
})
)
})
it('clears automation source context on edit with null', async () => {
queueFixtures(
callMock,
okFixture('req_edit', {
automation: { id: 'auto-1', name: 'GPU task review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['automations', 'edit', 'auto-1', '--source-context', 'null', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenNthCalledWith(
1,
'automation.update',
expect.objectContaining({
id: 'auto-1',
updates: expect.objectContaining({
sourceContext: null
})
})
)
})
it('rejects invalid automation source context JSON before calling the runtime', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'create',
'--name',
'GPU task review',
'--trigger',
'daily',
'--prompt',
'Review open work',
'--provider',
'codex',
'--repo',
'id:repo-gpu',
'--source-context',
'{nope',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'--source-context must be valid JSON'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects invalid automation --day values before calling the runtime', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'create',
'--name',
'Weekly review',
'--trigger',
'weekly',
'--day',
'7',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'--day must be an integer from 0 to 6'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it.each([
{
name: 'day on daily preset',
args: ['--trigger', 'daily', '--day', '2'],
message: '--day can only be used with the weekly automation preset'
},
{
name: 'time on custom cron',
args: ['--trigger', '0 9 * * *', '--time', '10:30'],
message: '--time can only be used with preset automation triggers'
},
{
name: 'time on hourly preset',
args: ['--trigger', 'hourly', '--time', '10:30'],
message: '--time cannot be used with the hourly automation preset'
}
])('rejects automation schedule modifier mismatch: $name', async ({ args, message }) => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'create',
'--name',
'Daily review',
...args,
'--prompt',
'Review open changes',
'--provider',
'codex',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(message)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it.each([
{
name: 'create',
args: [
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--time',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--json'
]
},
{
name: 'edit',
args: ['automations', 'edit', 'auto-1', '--trigger', 'daily', '--time', '--json']
}
])('rejects bare automation --time on $name', async ({ args }) => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(args, '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'--time must use HH:MM format'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects automation edit schedule modifiers without a schedule flag', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['automations', 'edit', 'auto-1', '--day', '7', '--json'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'--day requires --trigger or --schedule'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects automation create with both repo and workspace targets', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--repo',
'id:repo-1',
'--workspace',
'id:repo-1::/tmp/repo/feature',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Use either --repo or --workspace, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes automation session reuse flags through create and edit', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]),
okFixture('req_create', {
automation: { id: 'auto-1', name: 'Daily review' }
}),
okFixture('req_edit', {
automation: { id: 'auto-1', name: 'Daily review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--workspace',
'current',
'--reuse-session',
'--json'
],
'/tmp/repo/feature/src'
)
await main(['automations', 'edit', 'auto-1', '--fresh-session', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(
2,
'automation.create',
expect.objectContaining({
workspace: 'id:repo-1::/tmp/repo/feature',
workspaceMode: 'existing',
reuseSession: true
})
)
expect(callMock).toHaveBeenNthCalledWith(3, 'automation.update', {
id: 'auto-1',
updates: expect.objectContaining({ reuseSession: false })
})
})
it('rejects conflicting automation session reuse flags', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['automations', 'edit', 'auto-1', '--reuse-session', '--fresh-session', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Use either --reuse-session or --fresh-session, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects automation edit with both repo and workspace targets', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'edit',
'auto-1',
'--repo',
'id:repo-1',
'--workspace',
'id:repo-1::/tmp/repo/feature',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Use either --repo or --workspace, not both.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it.each([
{
flag: 'enabled',
value: 'false',
message: '--enabled does not take a value'
},
{
flag: 'disabled',
value: 'false',
message: '--disabled does not take a value'
}
])('rejects automation create --$flag with a string value', async ({ flag, value, message }) => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--repo',
'id:repo-1',
`--${flag}`,
value,
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(message)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('resolves explicit automation create workspace active from cwd', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]),
okFixture('req_automation_create', {
automation: { id: 'auto-1', name: 'Daily review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'automations',
'create',
'--name',
'Daily review',
'--trigger',
'daily',
'--prompt',
'Review open changes',
'--provider',
'codex',
'--workspace',
'active',
'--json'
],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(2, 'automation.create', {
name: 'Daily review',
prompt: 'Review open changes',
agentId: 'codex',
repo: undefined,
workspace: 'id:repo-1::/tmp/repo/feature',
workspaceMode: 'existing',
baseBranch: undefined,
timezone: undefined,
enabled: undefined,
missedRunGraceMinutes: undefined,
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: expect.any(Number)
})
})
it('resolves explicit automation edit workspace current from cwd', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'repo-1')]),
okFixture('req_edit', {
automation: { id: 'auto-1', name: 'Daily review' }
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['automations', 'edit', 'auto-1', '--workspace', 'current', '--enabled', '--json'],
'/tmp/repo/feature/src'
)
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(2, 'automation.update', {
id: 'auto-1',
updates: {
name: undefined,
prompt: undefined,
agentId: undefined,
repo: undefined,
workspace: 'id:repo-1::/tmp/repo/feature',
workspaceMode: undefined,
baseBranch: undefined,
reuseSession: undefined,
timezone: undefined,
enabled: true,
missedRunGraceMinutes: undefined
}
})
})
it('passes positional automation ids to edit, remove, run, and show', async () => {
queueFixtures(
callMock,
okFixture('req_edit', { automation: { id: 'auto-1', name: 'Paused' } }),
okFixture('req_remove', { removed: true, id: 'auto-1' }),
okFixture('req_run', {
run: {
id: 'run-1',
automationId: 'auto-1',
title: 'Paused run 1',
status: 'pending',
trigger: 'manual',
scheduledFor: 1,
workspaceId: null,
sessionKind: 'terminal',
chatSessionId: null,
terminalSessionId: null,
outputSnapshot: null,
usage: null,
error: null,
startedAt: null,
dispatchedAt: null,
createdAt: 1
}
}),
okFixture('req_show', { automation: { id: 'auto-1', name: 'Paused' } })
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['automations', 'edit', 'auto-1', '--disabled', '--json'], '/tmp/repo')
await main(['automations', 'remove', 'auto-1', '--json'], '/tmp/repo')
await main(['automations', 'run', 'auto-1', '--json'], '/tmp/repo')
await main(['automations', 'show', 'auto-1', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenNthCalledWith(1, 'automation.update', {
id: 'auto-1',
updates: {
name: undefined,
prompt: undefined,
agentId: undefined,
repo: undefined,
workspace: undefined,
workspaceMode: undefined,
baseBranch: undefined,
timezone: undefined,
enabled: false,
missedRunGraceMinutes: undefined
}
})
expect(callMock).toHaveBeenNthCalledWith(2, 'automation.delete', {
id: 'auto-1'
})
expect(callMock).toHaveBeenNthCalledWith(3, 'automation.runNow', {
id: 'auto-1'
})
expect(callMock).toHaveBeenNthCalledWith(4, 'automation.show', {
id: 'auto-1'
})
})
it('rejects ambiguous positional and flag automation ids before dispatch', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(['automations', 'show', 'auto-1', '--id', 'auto-2', '--json'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
expect(logSpy).toHaveBeenCalledTimes(1)
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: 'Pass --id either positionally or as a flag, not both.'
}
})
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('updates project host setup metadata through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_setup_update', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '/srv/orca',
displayName: 'GPU VM',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-update',
'--setup',
'setup-gpu',
'--display-name',
'GPU VM',
'--path',
'/srv/orca',
'--worktree-base-path',
'../worktrees',
'--state',
'ready',
'--method',
'imported-existing-folder',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.update', {
setupId: 'setup-gpu',
updates: {
displayName: 'GPU VM',
path: path.resolve('/tmp/repo', '/srv/orca'),
worktreeBasePath: '../worktrees',
gitUsername: undefined,
kind: undefined,
setupState: 'ready',
setupMethod: 'imported-existing-folder'
}
})
})
it('creates independent project host setup metadata through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_setup_create', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '',
displayName: 'GPU VM',
setupState: 'setting-up',
setupMethod: 'provisioned',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-create',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--setup-id',
'setup-gpu',
'--display-name',
'GPU VM',
'--state',
'setting-up',
'--method',
'provisioned',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.create', {
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
setupId: 'setup-gpu',
path: undefined,
kind: undefined,
displayName: 'GPU VM',
worktreeBasePath: undefined,
gitUsername: undefined,
setupState: 'setting-up',
setupMethod: 'provisioned'
})
})
it('deletes project host setup metadata through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_setup_delete', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '/srv/orca',
displayName: 'GPU VM',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'setup-delete', '--setup', 'setup-gpu', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('projectHostSetup.delete', {
setupId: 'setup-gpu'
})
})
})