Files
orca/src/main/agent-launch/agent-launch-opencode-model.test.ts
T
Brennan Benson 39a01f9c0b feat(agent-launch): create a folder workspace and start its agent in one host operation (#26083)
* feat(agent-launch): accept a create-folder-workspace target

agent.launch can now create a folder workspace and start its agent there,
the same operation it already runs for a new git worktree. The host
creates the folder workspace (metadata only, no startup terminal), resolves
it the way an existing target is resolved, then settles the mode and builds
the surface as for any existing folder workspace.

Hosts advertise agent.launch.create-folder-workspace.v1 so a client offers
the target only to a host that accepts it.

* fix(agent-launch): authorize, classify and navigate folder creates like their create method

A create-folder-workspace launch could do three things its two-call
equivalent never did:

- A phone could create folder workspaces. The mobile allowlist admits
  agent.launch but not folderWorkspace.create, and is checked by method
  name only. A create target now needs the authorization of the create
  method it stands for, read from the same allowlist, and a phone is
  refused with agent_launch_target_forbidden before admission, so no
  ledger row and no create. create-worktree maps to worktree.create,
  which phones already may call, so nothing changes for it.
- A create refused before anything was stored (group gone, folder
  missing, SSH host unreachable) replayed as
  agent_session_operation_unknown, and a retry under the same operation
  id was refused until the row expired. runtime.createFolderWorkspace now
  raises those refusals as FolderWorkspaceCreateRefusedError, with the
  same message text, and the launch records it as failed for any target.
  A failure after the create still replays unknown.
- A paired caller's folder create activated the new chat on the host
  window and did not select the caller's own tab. Only create-worktree,
  whose host activation runs setup, keeps the host navigation now; a
  folder create navigates like an existing workspace.

Tests cover the phone refusal, replay of a success, a refused create
settling failed and replaying that failure, an after-create failure
staying unknown, an SSH connection reaching the agent's view mode, paired
navigation, and an older host refusing the target at parse.

* fix(agent-launch): replay a refused folder create with its whole path

A folder create refused before anything was stored is recorded as the
launch's failure, and a retry under the same operation id answers from
that record. The record went through the 128-character cap every launch
failure code gets, and folder refusals carry the folder's absolute path,
so a retry could show the user a path cut short: a different folder.

The refusal is now recorded whole when it fits a path as long as Linux's
PATH_MAX after its code. Past that, which no folder a stat accepts
reaches, only the code before the path is kept, so the replay degrades to
the bare code instead of a partial path.

Also: the controller test asserts the exact refusal message, the
create-folder-workspace capability comment says a phone needs its own
gate, and the forbidden-target code says why it is not `forbidden`.

* test(folder-workspace): type the controller test's store stub without an assertion

The changed-code quality gate refuses the `as never` cast on the stub
store. It is now a RuntimeStore whose unreached members throw.

* test(agent-launch): run the folder-create launch tests in the Node runtime project

The operationId replay tests open the real SQLite record store, so the
file belongs with the other ledger-backed agent.launch tests.

* test(agent-launch): pin that a paired folder create's terminal stays off the host window

The folder-create test now asserts surfaceOwner: false on the terminal a
paired caller's launch creates, rather than matching any options. The
caller-selection header now says only an in-process worktree create keeps
worktree.create's navigation.

* test(agent-launch): drop the per-connection view-mode expectation main removed
2026-10-09 01:23:06 -07:00

90 lines
3.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
import { executeAgentLaunch } from './agent-launch-executor'
function harness() {
const readSettings = vi.fn(() => {
throw new Error('settings_not_needed_for_refusal')
})
const createSupport = vi.fn(async () => {
throw new Error('support_not_needed_for_refusal')
})
const createTerminal = vi.fn(async () => ({ handle: 'term_new' }))
const deliverPrompt = vi.fn(async () => true)
const createWorktree = vi.fn(async () => ({
worktreeId: 'wt_new',
connectionId: null,
startupTerminalHandle: 'term_new'
}))
return {
readSettings,
createTerminal,
deliverPrompt,
createWorktree,
run: (intent: AgentLaunchIntent) =>
executeAgentLaunch({
intent,
runtime: {
getClientSettings: readSettings,
getStructuredAgentSessionCreateSupport: createSupport
},
surfaces: {
createTerminalAgent: createTerminal,
createStructuredSession: async () => {
throw new Error('structured_not_expected')
},
deliverTerminalPrompt: deliverPrompt
},
workspaces: {
createWorktree,
createFolderWorkspace: async () => {
throw new Error('folder_create_not_expected')
}
}
})
}
}
const reused: AgentLaunchIntent = {
agent: 'opencode',
target: { kind: 'existing', worktree: 'folder:private' },
reuseTerminal: { handle: 'term_existing' },
prompt: { text: 'Read only', delivery: 'submit' },
sessionOptions: { model: 'private-proof/model-b' }
}
const creating: AgentLaunchIntent = {
...reused,
reuseTerminal: undefined,
target: { kind: 'create-worktree', create: { repo: 'id:private', name: 'task' } }
}
describe('OpenCode model preferences on unsupported launch placements', () => {
it.each([reused, creating])(
'refuses before reading settings or creating or delivering',
async (intent) => {
const h = harness()
await expect(h.run(intent)).rejects.toMatchObject({ code: 'capability_unsupported' })
expect(h.readSettings).not.toHaveBeenCalled()
expect(h.createTerminal).not.toHaveBeenCalled()
expect(h.createWorktree).not.toHaveBeenCalled()
expect(h.deliverPrompt).not.toHaveBeenCalled()
}
)
it('preserves prompt delivery to a reused terminal without model preferences', async () => {
const h = harness()
expect((await h.run({ ...reused, sessionOptions: undefined })).outcome).toEqual({
kind: 'terminal',
handle: 'term_existing'
})
expect(h.deliverPrompt).toHaveBeenCalledOnce()
expect(h.createWorktree).not.toHaveBeenCalled()
})
it('preserves ordinary worktree creation without model preferences', async () => {
const h = harness()
expect((await h.run({ ...creating, sessionOptions: undefined })).worktreeId).toBe('wt_new')
expect(h.createWorktree).toHaveBeenCalledOnce()
})
})