Files
orca/src/main/agent-launch/agent-launch-executor.test.ts
T
Brennan Benson 4b87bc718e refactor(agent-launch): redefine the agent.launch contract (#20999)
* refactor(agent-launch): redefine the agent.launch contract

`agent.launch` has no clients yet, so the contract is redefined in place
rather than versioned.

- params require `operation.id`, pinned to the shipped operation-id mint so
  the host can read the embedded timestamp back. No caller-supplied
  fingerprint: the host derives its own.
- the result carries `disposition` ('created' | 'replayed', the same
  vocabulary `RuntimeCreateAgentSessionResult` already uses) and a single
  top-level `warning` instead of one on the terminal arm only.
- the prompt receipt becomes an outcome enum, so a receipt can under-claim
  instead of reporting a bare `delivered: false`.
- the dead `customization` field is deleted, and the mode-reason union and
  receipt are declared once in shared with main re-exporting.
- `clientMutationId` joins the reserved create fields, with a test pinning
  the list to the create schema in both directions.

Contract only; no behaviour change and no ledger wiring.

* docs(agent-launch): stop calling the stripped set "agent fields"

`clientMutationId` joined AGENT_LAUNCH_RESERVED_CREATE_FIELDS, so three
comments describing the stripped set as agent fields now teach the wrong
model — including a SAFETY rationale, where a reader is trusting it most.
The rationale's claim is unchanged and still sound: deleting keys from a
parsed object leaves the rest the parsed shape.

* refactor(agent-launch): make the attempt id the launch's only idempotency key

Review follow-ups on the contract redefinition.

`operation: { id }` becomes a flat `clientOperationId`, spelled the way
`terminal.createAgentSession` and the structured mutation envelope already
spell the same concept, and admitted by the shipped
`parseAgentSessionOperationTimestamp` rather than a second copy of its
pattern — so `agent-session-host-authority` keeps the regex private.

The handler now dedupes on that id instead of the create payload's
`clientMutationId`. That field is optional, so keying on it left any launch
that omitted one with no idempotency at all, while the required attempt id
did nothing. Reserving `clientMutationId` is still right, but for the reason
the comments now give: `createManagedWorktree` never reads it, so a copy left
in the forwarded payload is inert while still reading as a guarantee. The
previous rationale — that it was a second live dedupe key — was not true.

`messageId` moves onto the prompt receipt's `journaled` arm so a producer
cannot report the text as committed without saying where, and `rpcCallerKey`
picks up the `terminal.create` call site it was lifted from instead of
shipping with no callers.

* docs(agent-launch): record why disposition is two-valued only for now

The ledger admits attempts whose outcome was never recorded, and neither
`created` nor `replayed` can say "I cannot tell you" — a caller handed
`created` for an unresolved attempt starts a second agent. Noted at the type
rather than in review, so whoever wires the ledger reads it where they edit.

* fix(agent-launch): keep contract within implemented guarantees
2026-09-16 16:39:38 -07:00

250 lines
9.3 KiB
TypeScript

/**
* The executor's ordering contract, which is the defect this module exists to remove.
*
* The old shape created a new worktree agent-first, so its startup terminal WAS the agent and the
* structured branch below it could not be reached for any new worktree. The assertions that matter
* here are therefore about *order and arguments*, not just the returned mode: a structured launch
* must create the worktree with `startupAgent: undefined`, and it must ask the host only after the
* workspace exists.
*/
import { describe, expect, it, vi } from 'vitest'
import {
AgentLaunchStructuredSessionRefusedError,
executeAgentLaunch,
type AgentLaunchExecution
} from './agent-launch-executor'
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
const STRUCTURED_PREFERENCE = {
experimentalNativeChat: true,
experimentalStructuredNativeChat: true,
openAgentTabsInChatByDefault: true
}
function harness(options: {
settings?: Record<string, unknown> | null
createSupport?: { supported: boolean; reason?: 'agent' | 'remote' | 'wsl' }
createSupportThrows?: boolean
structuredCreateError?: Error
}) {
const calls: string[] = []
const createWorktree = vi.fn(
async (args: { create: Record<string, unknown>; startupAgent: string | undefined }) => {
calls.push(`createWorktree(startupAgent=${String(args.startupAgent)})`)
return {
worktreeId: 'wt-new',
startupTerminalHandle: args.startupAgent ? 'term_agent_first' : undefined
}
}
)
const getStructuredAgentSessionCreateSupport = vi.fn(async () => {
calls.push('createSupport')
if (options.createSupportThrows) {
throw new Error('host unreachable')
}
return options.createSupport ?? { supported: true }
})
const createStructuredSession = vi.fn(async () => {
calls.push('createStructuredSession')
if (options.structuredCreateError) {
throw options.structuredCreateError
}
return { sessionId: 'sess-1', handle: 'handle_structured' }
})
const createTerminalAgent = vi.fn(async () => {
calls.push('createTerminalAgent')
return { handle: 'term_1' }
})
const runtime = {
getClientSettings: () =>
options.settings === undefined ? STRUCTURED_PREFERENCE : options.settings,
getStructuredAgentSessionCreateSupport
}
return {
calls,
createWorktree,
createStructuredSession,
createTerminalAgent,
run: (intent: AgentLaunchIntent) =>
executeAgentLaunch({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements only the two runtime methods the executor reaches, and each test asserts the calls made, so an omitted method throws rather than reading a wrong value.
runtime: runtime as unknown as AgentLaunchExecution['runtime'],
intent,
surfaces: { createStructuredSession, createTerminalAgent },
workspaces: { createWorktree }
})
}
}
const CREATE_INTENT: AgentLaunchIntent = {
agent: 'claude',
target: { kind: 'create-worktree', create: { repo: 'id:repo-1', name: 'task' } }
}
describe('a structured launch that creates its own worktree', () => {
it('creates the worktree with no startup agent, then asks the host, then opens a session', async () => {
const h = harness({})
const result = await h.run(CREATE_INTENT)
// The whole defect in one assertion: the worktree must not be created agent-first.
expect(h.calls).toEqual([
'createWorktree(startupAgent=undefined)',
'createSupport',
'createStructuredSession'
])
expect(result.outcome).toEqual({
kind: 'structured',
sessionId: 'sess-1',
handle: 'handle_structured'
})
expect(result.worktreeId).toBe('wt-new')
expect(result.receipt.mode).toBe('structured')
})
it('asks the host only after the workspace exists, never before', async () => {
const h = harness({})
await h.run(CREATE_INTENT)
expect(h.calls.indexOf('createSupport')).toBeGreaterThan(
h.calls.indexOf('createWorktree(startupAgent=undefined)')
)
})
it('falls back to a terminal in the worktree it just created when the host refuses', async () => {
const h = harness({ createSupport: { supported: false, reason: 'wsl' } })
const result = await h.run(CREATE_INTENT)
expect(h.calls).toEqual([
'createWorktree(startupAgent=undefined)',
'createSupport',
'createTerminalAgent'
])
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' })
// Not a failed launch, and the workspace is the one just created.
expect(result.worktreeId).toBe('wt-new')
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'wsl_execution_runtime' })
})
it('falls back to a terminal when the host cannot be reached at all', async () => {
const h = harness({ createSupportThrows: true })
const result = await h.run(CREATE_INTENT)
expect(result.outcome.kind).toBe('terminal')
expect(result.receipt).toMatchObject({ reason: 'structured_support_unknown' })
})
it('falls back only for a definitive structured refusal after the worktree exists', async () => {
const h = harness({
structuredCreateError: new AgentLaunchStructuredSessionRefusedError(
'structured_agent_session_unsupported',
'unsupported'
)
})
const result = await h.run(CREATE_INTENT)
expect(h.calls).toEqual([
'createWorktree(startupAgent=undefined)',
'createSupport',
'createStructuredSession',
'createTerminalAgent'
])
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' })
expect(result.receipt).toMatchObject({
mode: 'terminal',
reason: 'structured_unsupported_on_host'
})
})
it('does not create a duplicate terminal when structured creation is unknown', async () => {
const h = harness({
structuredCreateError: new AgentLaunchStructuredSessionRefusedError(
'agent_session_operation_unknown',
'unknown'
)
})
await expect(h.run(CREATE_INTENT)).rejects.toThrow('unknown')
expect(h.calls).toEqual([
'createWorktree(startupAgent=undefined)',
'createSupport',
'createStructuredSession'
])
})
it('strips a stale startupAgent out of a migrated create payload', async () => {
const h = harness({})
await h.run({
agent: 'claude',
target: {
kind: 'create-worktree',
// Exactly what mobile sends `worktree.create` today.
create: { repo: 'id:repo-1', name: 'task', startupAgent: 'claude', startupDraft: 'url' }
}
})
const passed = h.createWorktree.mock.calls[0]?.[0]
expect(passed?.create).not.toHaveProperty('startupAgent')
expect(passed?.create).not.toHaveProperty('startupDraft')
expect(passed?.create).toMatchObject({ repo: 'id:repo-1', name: 'task' })
})
})
describe('a launch the user did not ask to be structured', () => {
it('creates the worktree agent-first and never asks the host', async () => {
const h = harness({ settings: null })
const result = await h.run(CREATE_INTENT)
// Agent-first is preserved for PTY launches: it is what sequences the agent's startup command
// behind the setup runner, so the wait-for-setup gate comes for free.
expect(h.calls).toEqual(['createWorktree(startupAgent=claude)'])
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_agent_first' })
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'user_default' })
})
})
describe('a launch into a workspace that already exists', () => {
it('opens a session without creating anything', async () => {
const h = harness({})
const result = await h.run({ agent: 'codex', target: { kind: 'existing', worktree: 'wt-7' } })
expect(h.calls).toEqual(['createSupport', 'createStructuredSession'])
expect(h.createWorktree).not.toHaveBeenCalled()
expect(result.worktreeId).toBe('wt-7')
})
it('reuses a running terminal without creating or asking', async () => {
const h = harness({})
const result = await h.run({
agent: 'claude',
target: { kind: 'existing', worktree: 'wt-7' },
reuseTerminal: { handle: 'term_live' }
})
expect(h.calls).toEqual([])
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_live' })
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'reused_terminal' })
})
})
describe('an agent with no structured session', () => {
it('stays a terminal without asking the host', async () => {
const h = harness({})
const result = await h.run({ agent: 'grok', target: { kind: 'existing', worktree: 'wt-7' } })
expect(h.calls).toEqual(['createTerminalAgent'])
expect(result.receipt).toMatchObject({ reason: 'agent_without_structured_session' })
})
})
describe('the prompt receipt', () => {
it('reports a requested prompt as not delivered rather than omitting it', async () => {
const h = harness({})
const result = await h.run({
...CREATE_INTENT,
prompt: { text: 'do the thing', delivery: 'draft' }
})
// The executor delivers nothing, so the only honest outcome is the one that under-claims.
expect(result.prompt).toEqual({ delivery: 'draft', outcome: 'not-delivered' })
})
it('omits the receipt when no prompt was requested', async () => {
const h = harness({})
expect((await h.run(CREATE_INTENT)).prompt).toBeUndefined()
})
})