/* 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', async () => { // Why: re-export the REAL error classes rather than redefining them. format.ts // narrows with `instanceof` against ./runtime/types, so a look-alike class // here would make every CLI error fall through to the generic `runtime_error` // shape — mirroring the barrel keeps the mock faithful to production. const { RuntimeClientError, RuntimeRpcFailureError } = await import('./runtime/types.js') class RuntimeClient { readonly isRemote: boolean call = callMock getCliStatus = vi.fn() openOrca = vi.fn() constructor( _userDataPath?: string, _requestTimeoutMs?: number, remotePairingCode?: string | null, environmentSelector?: string | null ) { runtimeClientConstructorMock(remotePairingCode, environmentSelector) const effectivePairingCode = remotePairingCode === undefined ? (process.env.ORCA_PAIRING_CODE ?? process.env.ORCA_REMOTE_PAIRING) : remotePairingCode const effectiveEnvironment = environmentSelector === undefined ? process.env.ORCA_ENVIRONMENT : environmentSelector if (effectivePairingCode && effectiveEnvironment) { throw new RuntimeClientError( 'invalid_argument', 'Use either --pairing-code or --environment, not both.' ) } this.isRemote = Boolean(effectivePairingCode || effectiveEnvironment) } } return { RuntimeClient, RuntimeClientError, RuntimeRpcFailureError, serveOrcaApp: 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() 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 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('artifact runtime routing', () => { afterEach(() => { vi.unstubAllEnvs() vi.restoreAllMocks() process.exitCode = 0 }) it('uses the desktop runtime despite remote-selection environment fallbacks', async () => { vi.stubEnv('ORCA_ENVIRONMENT', 'remote-environment') vi.stubEnv('ORCA_PAIRING_CODE', 'remote-pairing-code') vi.spyOn(console, 'log').mockImplementation(() => undefined) callMock.mockResolvedValue(okFixture('artifact-list', { status: 'ok', value: [] })) runtimeClientConstructorMock.mockClear() await main(['artifacts', 'list', '--json'], '/folder-workspace') expect(process.exitCode).not.toBe(1) expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, null) expect(callMock).toHaveBeenCalledWith('artifacts.list', {}) }) }) describe('unknown command surfaces a suggestion', () => { let errorSpy: ReturnType 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 host-local account management', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await main([], '/tmp/repo') expect(logSpy.mock.calls.flat().join('\n')).toContain( 'account add Add a managed Claude or Codex account on this Orca host' ) expect(logSpy.mock.calls.flat().join('\n')).toContain( 'account list List managed Claude and Codex accounts on this Orca host' ) 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(logSpy.mock.calls[0][0]).toContain( "orchestration worker-release Release a settled worker's terminal after archiving its output" ) expect(logSpy.mock.calls[0][0]).toContain( 'orchestration worker-retain Keep a worker terminal live for debugging' ) expect(logSpy.mock.calls[0][0]).toContain( 'orchestration worker-list Report worker terminal resource accounting' ) 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 []') 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 Connected Linear workspace id') expect(issueHelp).toContain('--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 ') expect(searchHelp).toContain('--workspace Connected Linear workspace id, or all') expect(searchHelp).toContain('--query 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 Opaque cursor returned by a previous list-issues page' ) expect(listIssuesHelp).toContain('--workspace Connected Linear workspace id, or all') expect(listIssuesHelp).not.toContain('Line cursor from a previous read') expect(callMock).not.toHaveBeenCalled() }) it('documents the machine-readable terminal topology opt-in', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) logSpy.mockClear() await main(['terminal', 'list', '--help'], '/tmp/repo') const help = String(logSpy.mock.calls[0][0]) expect(help).toContain('[--include-visual-layouts] [--json]') expect(help).toContain('--include-visual-layouts Include tab and pane topology in JSON output') expect(help).toContain('JSON omits visualLayouts by default') 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 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 ') logSpy.mockClear() await main(['worktree', 'set', '--help'], '/tmp/repo') const setHelp = String(logSpy.mock.calls[0][0]) expect(setHelp).toContain('--linear-issue ') expect(setHelp).toContain('--linear-issue 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 ] [--display-name ]') expect(help).toContain('--task-title Concise title for the orchestration task') expect(help).toContain('--display-name 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 ] [--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:') expect(createHelp).toContain('folder:') expect(createHelp).toContain('worktree:') 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:') expect(setHelp).not.toContain('worktree:') 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:::' ) 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, // Why: a bare identifier carries no org, and the two scoping fields // describe the issue being replaced. Inheriting a stale org key would // pin the lookup to a workspace this issue does not live in. 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:, worktree:, id:::, branch:, issue:, 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('starts an agent worktree in the background unless activation is explicit', 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: false, setupDecision: 'run', parentWorktree: undefined, cwdParentWorktree: 'id:repo-1::/tmp/repo', noParent: false, callerTerminalHandle: undefined, cliProvenanceRequest: {}, startupAgent: 'codex', startupPrompt: 'hi' }) }) it('infers the repo and honors explicit activation 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', '--activate', '--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, includeVisualLayouts: false }) }) it('requests visual layouts only for the human-readable terminal list', 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'], '/tmp/repo/feature/src') expect(callMock).toHaveBeenNthCalledWith( 2, 'terminal.list', expect.objectContaining({ includeVisualLayouts: true }) ) }) it('allows agent JSON clients to request visual layouts explicitly', async () => { const visualLayouts = [ { worktreeId: 'repo::/tmp/repo/feature', worktreePath: '/tmp/repo/feature', root: { type: 'group', groupId: null, activeTabId: null, tabs: [] } } ] queueFixtures( callMock, worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]), okFixture('req_term', { terminals: [], visualLayouts, totalCount: 0, truncated: false }) ) const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await main( ['terminal', 'list', '--worktree', 'active', '--include-visual-layouts', '--json'], '/tmp/repo/feature/src' ) expect(callMock).toHaveBeenNthCalledWith( 2, 'terminal.list', expect.objectContaining({ includeVisualLayouts: true }) ) expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({ result: { visualLayouts } }) }) 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' }) }) })