Files
orca/tests/e2e/agent-launch-cli.unit.test.ts
T
Brennan Benson c20971dbd1 fix(native-chat): send launch prompts and worker tasks through the retrying start (#33832)
* Make opening launch prompts own the retrying chat start

* test: observe launch inventory through surface publication

* fix: keep blank chat launch prompts unsent

* fix: bound launch seed filtering and update CLI replay fixture

* fix: preserve encoded launch selections after filtering
2026-10-26 10:45:40 -07:00

379 lines
13 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-runtime-capability'
import type { StructuredAgentSessionFirstMessage } from '../../src/shared/structured-agent-session-create'
import { openTestAgentSessionRecordStore } from '../../src/main/runtime/agent-session-record-store-test-harness'
import { RuntimeClient, RuntimeClientError } from '../../src/cli/runtime-client'
import { callAgentLaunch, readCliAgentLaunch } from '../../src/cli/agent-launch-request'
import { parseArgs } from '../../src/cli/args'
import { AGENT_LAUNCH_HANDLERS } from '../../src/cli/handlers/agent-launch'
import { createWorktreeWithAgent } from '../../src/cli/handlers/worktree-create-agent-launch'
import {
methodNamed,
rpcContext,
runtimeStub,
setAgentLaunchRecordStore
} from '../../src/main/runtime/rpc/methods/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-agent-session-create', () => ({
createStructuredAgentSessionForWorktree: chatCreate
}))
vi.mock('../../src/main/runtime/rpc/methods/agent-launch-terminal-prompt', () => ({
deliverTerminalAgentLaunchPrompt: terminalPrompt
}))
const { AGENT_LAUNCH_METHODS } = await import('../../src/main/runtime/rpc/methods/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 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('keeps a new worktree background chat inactive', async () => {
await callAgentLaunch(
hostClient(runtimeStub()),
{ agent: 'codex' },
{ target: create, presentation: 'background' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
expect(chatCreate.mock.calls[0]?.[0].activate).toBe(false)
})
it('honors explicit local chat focus', async () => {
await callAgentLaunch(
hostClient(runtimeStub()),
{ agent: 'codex' },
{ target: existing, presentation: 'focused' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
expect(chatCreate.mock.calls[0]?.[0].activate).toBe(true)
})
it('passes local terminal focus to the runtime', async () => {
const runtime = runtimeStub({ settings: {} })
await callAgentLaunch(
hostClient(runtime),
{ agent: 'codex' },
{ target: existing, presentation: 'focused' },
AGENT_LAUNCH_RUNTIME_CAPABILITIES
)
expect(runtime.createTerminal).toHaveBeenCalledWith(
'id:wt-7',
expect.objectContaining({ presentation: 'focused' })
)
})
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('refuses replay on an older replacement host without sending any launch', async () => {
const client = hostClient(runtimeStub())
const operationId = `${Date.now()}-${'a'.repeat(32)}`
await expect(
callAgentLaunch(client, { agent: 'codex', operationId }, { target: existing }, [
'agent.launch.v2',
'agent.launch.local-presentation.v1'
])
).rejects.toThrow('do not retry without --retry-launch')
expect(client.call).not.toHaveBeenCalled()
})
it.each([existing, create])(
'refuses a v2-only prompted launch before creating anything: %j',
async (target) => {
const runtime = runtimeStub()
await expect(
callAgentLaunch(
hostClient(runtime),
{ agent: 'codex', prompt: { text: 'Long\nMultiline', delivery: 'submit' } },
{ target },
['agent.launch.v2']
)
).rejects.toThrow('cannot safely carry launch prompts')
expect(chatCreate).not.toHaveBeenCalled()
expect(runtime.createTerminal).not.toHaveBeenCalled()
}
)
it('refuses hosts without local presentation before creating anything', async () => {
const client = hostClient(runtimeStub())
await expect(
callAgentLaunch(
client,
{ agent: 'codex' },
{ target: existing, presentation: 'background' },
['agent.launch.v2']
)
).rejects.toThrow('cannot honor CLI launch focus or background')
expect(client.call).not.toHaveBeenCalled()
})
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.each([undefined, 'current'])(
'uses the innermost native folder for %s before Git fallback',
async (selector) => {
const client = hostClient(runtimeStub())
vi.spyOn(console, 'log').mockImplementation(() => {})
const call = vi.mocked(client.call)
call.mockResolvedValueOnce({
id: 'folders',
ok: true,
result: {
folderWorkspaces: [
{ id: 'outer', folderPath: '/tmp/repo' },
{ id: 'remote', folderPath: '/tmp/repo/nested/deeper', connectionId: 'ssh-1' },
{ id: 'nested', folderPath: '/tmp/repo/nested' }
]
},
_meta: { runtimeId: 'runtime-1' }
})
const flags = new Map<string, string | boolean>([['agent', 'codex']])
if (selector) {
flags.set('worktree', selector)
}
await AGENT_LAUNCH_HANDLERS['agent launch']({
flags,
client,
cwd: '/tmp/repo/nested/deeper',
json: true
})
expect(call).not.toHaveBeenCalledWith('worktree.list', expect.anything())
expect(call).toHaveBeenCalledWith(
'agent.launchReplay',
expect.objectContaining({ target: { kind: 'existing', worktree: 'id:folder:nested' } })
)
}
)
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()
})
})