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 () => { const { createRuntimeClientModuleMock } = await import('./index-test-harness.js') return createRuntimeClientModuleMock({ callMock, runtimeClientConstructorMock, serveOrcaAppMock, getDefaultUserDataPathMock }) }) vi.mock('./runtime/environments', () => ({ addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock, listEnvironments: listEnvironmentsMock, removeEnvironment: vi.fn(), resolveEnvironment: vi.fn() })) vi.mock('child_process', async () => { const { createChildProcessModuleMock } = await import('./index-test-harness.js') return createChildProcessModuleMock(spawnMock) }) import { COMMAND_SPECS, main } from './index' import { formatFlagHelp } from './help' import { GLOBAL_FLAGS, specPaths } from './args' import { okFixture, queueFixtures } from './test-fixtures' 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_show', { worktree: { hostId: 'local' } }), okFixture('req', { removed: true }) ) await main(['worktree', 'remove', '--worktree', 'id:wt-1', '--force', '--json'], '/tmp/repo') expect(callMock).toHaveBeenNthCalledWith( 2, 'worktree.rm', expect.objectContaining({ worktree: 'id:wt-1', hostId: 'local', force: true }) ) }) it('runs `worktree delete` as the canonical `worktree rm`', async () => { queueFixtures( callMock, okFixture('req_show', { worktree: { hostId: 'runtime:env-1' } }), okFixture('req', { removed: true }) ) await main(['worktree', 'delete', '--worktree', 'id:wt-1', '--json'], '/tmp/repo') expect(callMock).toHaveBeenNthCalledWith( 2, 'worktree.rm', expect.objectContaining({ worktree: 'id:wt-1', hostId: 'runtime:env-1' }) ) }) it('says the checkout is still being deleted when the host accepted a background removal', async () => { queueFixtures( callMock, okFixture('req_show', { worktree: { hostId: 'local' } }), okFixture('req', { removed: true, removing: true }) ) await main(['worktree', 'rm', '--worktree', 'id:wt-1'], '/tmp/repo') expect(logSpy).toHaveBeenCalledWith( 'removed: true\nOrca is still deleting the checkout in the background.' ) }) it('fails closed when worktree removal cannot resolve a host', async () => { queueFixtures(callMock, okFixture('req_show', { worktree: { id: 'wt-1' } })) const priorExitCode = process.exitCode try { await main(['worktree', 'rm', '--worktree', 'id:wt-1', '--json'], '/tmp/repo') expect(process.exitCode).toBe(1) expect(callMock).toHaveBeenCalledTimes(1) expect(callMock).toHaveBeenCalledWith('worktree.show', { worktree: 'id:wt-1' }) } finally { process.exitCode = priorExitCode } }) // #19334: a failed archive hook blocks removal, so the CLI must exit non-zero rather than // report a delete that did not happen — and the waiver must ride its own flag, never --force. it('exits non-zero when worktree removal is refused by a failed archive hook', async () => { queueFixtures(callMock, okFixture('req_show', { worktree: { hostId: 'local' } })) callMock.mockRejectedValueOnce( Object.assign(new Error('Archive hook failed for worktree: /tmp/wt — exited 23.'), { code: 'worktree_archive_hook_failed' }) ) const priorExitCode = process.exitCode try { await main( ['worktree', 'rm', '--worktree', 'id:wt-1', '--force', '--run-hooks', '--json'], '/tmp/repo' ) expect(process.exitCode).toBe(1) expect(callMock).toHaveBeenNthCalledWith( 2, 'worktree.rm', expect.objectContaining({ runHooks: true, allowFailedArchiveHook: false }) ) } finally { process.exitCode = priorExitCode } }) // #19334 S4: the waiver only applies to a hook that ran, so alone it silently does nothing. it('rejects the archive-hook waiver without --run-hooks instead of ignoring it', async () => { queueFixtures(callMock, okFixture('req_show', { worktree: { hostId: 'local' } })) const priorExitCode = process.exitCode try { await main( ['worktree', 'rm', '--worktree', 'id:wt-1', '--allow-failed-archive-hook', '--json'], '/tmp/repo' ) expect(process.exitCode).toBe(1) // The removal must never have been attempted. expect(callMock).not.toHaveBeenCalledWith('worktree.rm', expect.anything()) } finally { process.exitCode = priorExitCode } }) it('forwards the explicit archive-hook waiver on worktree rm', async () => { queueFixtures( callMock, okFixture('req_show', { worktree: { hostId: 'local' } }), okFixture('req', { removed: true }) ) await main( [ 'worktree', 'rm', '--worktree', 'id:wt-1', '--run-hooks', '--allow-failed-archive-hook', '--json' ], '/tmp/repo' ) expect(callMock).toHaveBeenNthCalledWith( 2, 'worktree.rm', expect.objectContaining({ runHooks: true, allowFailedArchiveHook: true }) ) }) 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('names the offending --worktree value and the valid forms on selector_not_found', async () => { const { RuntimeRpcFailureError } = await import('./runtime/types.js') callMock.mockRejectedValue( new RuntimeRpcFailureError({ id: 'req_selector', ok: false, error: { code: 'selector_not_found', message: 'selector_not_found' }, _meta: { runtimeId: 'runtime_local' } }) ) await main( ['orchestration', 'worker-start', '--task', 't1', '--worktree', 'repo-1', '--agent', 'codex'], '/tmp/repo' ) expect(process.exitCode).toBe(1) const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n') expect(stderr).toContain('No Orca workspace matched the worktree selector "repo-1"') expect(stderr).toContain('id:repo-1::') expect(stderr).toContain('Valid selector forms:') }) 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('nested command group help', () => { it.each([ ['browser', ['browser'], ['identity get', 'identity set']], ['browser identity', ['browser', 'identity'], ['get', 'set']] ])( 'prints successful help for %s without constructing a runtime client', async (_, path, commands) => { const previousExitCode = process.exitCode const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) runtimeClientConstructorMock.mockClear() process.exitCode = 0 try { await main([...path, '--help'], '/tmp/repo') expect(process.exitCode).toBe(0) const output = logSpy.mock.calls.flat().join('\n') expect(output).toContain(`orca ${path.join(' ')}`) for (const command of commands) { expect(output).toContain(command) } expect(output).not.toContain('Unknown command') expect(runtimeClientConstructorMock).not.toHaveBeenCalled() expect(callMock).not.toHaveBeenCalled() } finally { process.exitCode = previousExitCode logSpy.mockRestore() } } ) }) 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 agent account on this Orca host' ) expect(logSpy.mock.calls.flat().join('\n')).toContain( 'account list List managed agent accounts on this Orca host' ) logSpy.mockRestore() }) it('labels retired coordinator scheduler commands at the root', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await main(['--help'], '/tmp/repo') const output = String(logSpy.mock.calls[0]?.[0]) expect(output).toContain( 'orchestration coordinator-start Retired: load the current orchestration skill' ) expect(output).toContain( 'orchestration coordinator-stop Retired: load the current orchestration skill' ) expect(output).not.toContain('Start the legacy automatic coordinator loop') expect(output).not.toContain('Stop the legacy automatic coordinator loop') 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(logSpy.mock.calls[0][0]).not.toContain('orchestration worker-cleanup') 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 and write Linear issues 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 from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace' ) expect(listIssuesHelp).toContain('--workspace Connected Linear workspace id, or all') expect(listIssuesHelp).toContain('0=none, 1=urgent, 2=high, 3=medium, 4=low') expect(listIssuesHelp).toContain( '--limit Max issues to return; omit to return every match' ) 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('describes worker-list cursors as opaque page cursors', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) logSpy.mockClear() await main(['orchestration', 'worker-list', '--help'], '/tmp/repo') const help = String(logSpy.mock.calls[0][0]) expect(help).toContain('[--cursor ]') expect(help).toContain('--cursor Opaque page cursor copied from page.nextCursor') expect(help).toContain('Continue with the opaque page.nextCursor value unchanged.') expect(help).not.toContain('--cursor ') 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]') expect(rootHelp).toContain('identity:') 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(formatFlagHelp('parent-worktree')).toContain('identity:') 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() }) })