Files
orca/tests/e2e/agent-launch-cli.unit.test.ts
T
Neil 560a22e00f refactor(cli): organize command dispatch and orchestration owners (#41562)
* refactor(cli): organize command dispatch and orchestration owners

* test(orchestration): retire superseded unbound mail refusal case
2026-11-12 08:12:55 -08:00

274 lines
10 KiB
TypeScript

import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AGENT_LAUNCH_RUNTIME_CAPABILITIES } from '../../src/shared/agent-launch/contracts/agent-launch-runtime-capability'
import type { StructuredAgentSessionFirstMessage } from '../../src/shared/agent-session/structured-agent-session-create'
import { openTestAgentSessionRecordStore } from '../../src/main/runtime/agent-session/records/agent-session-record-store-test-harness'
import { closeTestJournalHostDatabase } from '../../src/main/native-chat/agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { RuntimeClient } from '../../src/cli/runtime/client'
import { RuntimeClientError } from '../../src/cli/runtime/types'
import { callAgentLaunch, readCliAgentLaunch } from '../../src/cli/agents/agent-launch-request'
import { parseArgs } from '../../src/cli/arguments/args'
import { AGENT_LAUNCH_HANDLERS } from '../../src/cli/agents/agent-launch'
import { createWorktreeWithAgent } from '../../src/cli/workspaces/worktree-create-agent-launch'
import {
methodNamed,
rpcContext,
runtimeStub,
setAgentLaunchRecordStore
} from '../../src/main/runtime/rpc/methods/agent-launch/agent-launch.test-fixture'
const chatCreate = vi.hoisted(() => vi.fn())
const terminalPrompt = vi.hoisted(() => vi.fn(async () => true))
vi.mock(
'../../src/main/runtime/rpc/methods/structured-sessions/creation/structured-agent-session-create',
() => ({
createStructuredAgentSessionForWorktree: chatCreate
})
)
vi.mock('../../src/main/runtime/rpc/methods/agent-launch/agent-launch-terminal-prompt', () => ({
deliverTerminalAgentLaunchPrompt: terminalPrompt
}))
const { AGENT_LAUNCH_METHODS } =
await import('../../src/main/runtime/rpc/methods/agent-launch/agent-launch')
let directory: string
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'orca-launch-cli-'))
setAgentLaunchRecordStore(await openTestAgentSessionRecordStore(directory))
chatCreate
.mockReset()
.mockImplementation(async (args: { firstMessage?: StructuredAgentSessionFirstMessage }) => ({
ok: true,
value: {
sessionId: 'codex_test',
...(args.firstMessage
? { firstMessage: { clientMessageId: args.firstMessage.clientMessageId } }
: {})
}
}))
terminalPrompt.mockClear()
})
afterEach(async () => {
setAgentLaunchRecordStore(null)
vi.restoreAllMocks()
await closeTestJournalHostDatabase(directory)
await rm(directory, { recursive: true, force: true })
})
function hostClient(runtime: ReturnType<typeof runtimeStub>, loseReply = false) {
const client = new RuntimeClient(directory, 60_000, null, null)
let lost = false
vi.spyOn(client, 'call').mockImplementation(async (name, params) => {
if (name === 'status.get') {
return {
id: 'status',
ok: true,
result: { capabilities: [...AGENT_LAUNCH_RUNTIME_CAPABILITIES] },
_meta: { runtimeId: 'runtime-1' }
}
}
if (name === 'projectGroup.list') {
return { id: 'groups', ok: true, result: { groups: [] }, _meta: { runtimeId: 'runtime-1' } }
}
if (name === 'repo.list') {
return { id: 'repos', ok: true, result: { repos: [] }, _meta: { runtimeId: 'runtime-1' } }
}
const method = methodNamed(AGENT_LAUNCH_METHODS, 'agent.launchReplay')
expect(name).toBe('agent.launchReplay')
const result = await method.handler(method.params.parse(params), rpcContext(runtime, {}))
if (loseReply && !lost) {
lost = true
throw new RuntimeClientError(
'runtime_unavailable',
'The Orca runtime closed the connection before responding. Restart Orca and try again.'
)
}
return { id: 'reply', ok: true, result, _meta: { runtimeId: 'runtime-1' } }
})
return client
}
const existing = { kind: 'existing', worktree: 'id:wt-7' } as const
const create = {
kind: 'create-worktree',
create: { repo: 'id:repo-1', name: 'task', activate: false }
} as const
describe('CLI launch through host surface creation and replay admission', () => {
it.each([existing, create])(
'a lost accepted reply replays without a second chat or prompt: %j',
async (target) => {
const runtime = runtimeStub()
const launch = {
agent: 'codex',
prompt: { text: 'Fix the repo\nThen test it', delivery: 'submit' }
} as const
let operationId: string | undefined
try {
await callAgentLaunch(
hostClient(runtime, true),
launch,
{ target, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
throw new Error('Expected lost reply')
} catch (error) {
expect(error).toBeInstanceOf(RuntimeClientError)
if (
!(error instanceof RuntimeClientError) ||
!error.data ||
typeof error.data !== 'object' ||
!('operationId' in error.data) ||
typeof error.data.operationId !== 'string'
) {
throw error
}
operationId = error.data.operationId
expect(error.message).not.toContain('try again')
expect(error.data).toMatchObject({ retrySafe: true, operationId })
}
const result = await callAgentLaunch(
hostClient(runtime),
readCliAgentLaunch(
new Map([
['prompt', launch.prompt.text],
['retry-launch', operationId ?? '']
]),
'codex',
{ createsWorktree: target.kind === 'create-worktree' }
),
{ target, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
const firstMessage = chatCreate.mock.calls[0]?.[0].firstMessage
expect(firstMessage).toMatchObject({
clientMessageId: expect.any(String),
body: { blocks: [{ type: 'text', text: launch.prompt.text }] }
})
expect(result.result.prompt).toMatchObject({
outcome: 'journaled',
messageId: firstMessage.clientMessageId
})
expect(chatCreate).toHaveBeenCalledTimes(1)
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(
target.kind === 'create-worktree' ? 1 : 0
)
}
)
it.each([existing, create])(
'rejects empty recovery IDs after a lost terminal reply and preserves valid replay: %j',
async (target) => {
const runtime = runtimeStub({ settings: {} })
const argv = ['--agent', 'codex', '--prompt', 'Fix the repo\nThen test']
const launch = readCliAgentLaunch(parseArgs(argv).flags, 'codex', {
createsWorktree: target.kind === 'create-worktree'
})
const initialClient = hostClient(runtime, true)
let operationId: string | undefined
try {
await callAgentLaunch(
initialClient,
launch,
{ target, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
throw new Error('Expected lost reply')
} catch (error) {
if (
!(error instanceof RuntimeClientError) ||
!error.data ||
typeof error.data !== 'object' ||
!('operationId' in error.data) ||
typeof error.data.operationId !== 'string'
) {
throw error
}
operationId = error.data.operationId
}
const retryClient = hostClient(runtime)
for (const retryArgs of [['--retry-launch', ''], ['--retry-launch=']]) {
await expect(async () => {
const retry = readCliAgentLaunch(parseArgs([...argv, ...retryArgs]).flags, 'codex', {
createsWorktree: target.kind === 'create-worktree'
})
await callAgentLaunch(
retryClient,
retry,
{ target, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
}).rejects.toMatchObject({ code: 'invalid_argument' })
}
expect(retryClient.call).not.toHaveBeenCalled()
await callAgentLaunch(
retryClient,
readCliAgentLaunch(
parseArgs([...argv, '--retry-launch', operationId ?? '']).flags,
'codex',
{
createsWorktree: target.kind === 'create-worktree'
}
),
{ target, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
expect(retryClient.call).toHaveBeenCalledExactlyOnceWith(
'agent.launchReplay',
vi.mocked(initialClient.call).mock.calls[0]?.[1]
)
expect(terminalPrompt).toHaveBeenCalledTimes(1)
expect(runtime.createTerminal).toHaveBeenCalledTimes(target.kind === 'existing' ? 1 : 0)
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(
target.kind === 'create-worktree' ? 1 : 0
)
}
)
it('gives inspection-only recovery after an ambiguous legacy create and never falls back again', async () => {
const client = new RuntimeClient(directory, 60_000, null, null)
const call = vi
.spyOn(client, 'call')
.mockResolvedValueOnce({
id: 'status',
ok: true,
result: { capabilities: [] },
_meta: { runtimeId: 'runtime-1' }
})
.mockRejectedValueOnce(
new RuntimeClientError('runtime_unavailable', 'Restart Orca and try again.')
)
await expect(
createWorktreeWithAgent({
client,
flags: new Map(),
create: create.create,
launch: { agent: 'codex' },
json: true
})
).rejects.toMatchObject({
code: 'agent_session_operation_unknown',
data: {
retrySafe: false,
nextSteps: expect.arrayContaining([expect.stringContaining('Do not repeat')])
}
})
expect(call).toHaveBeenCalledTimes(2)
})
it('refuses remote cwd inference for current', async () => {
const client = hostClient(runtimeStub())
vi.spyOn(client, 'isRemote', 'get').mockReturnValue(true)
const flags = new Map<string, string | boolean>([
['agent', 'codex'],
['worktree', 'current']
])
await expect(
AGENT_LAUNCH_HANDLERS['agent launch']({ flags, client, cwd: '/tmp/repo', json: true })
).rejects.toMatchObject({ code: 'invalid_argument' })
expect(client.call).not.toHaveBeenCalled()
})
})