Files
orca/src/main/runtime/structured-agent-availability.test.ts
T
Brennan Benson f87d68b4a4 Chat runs launcher Commands like op run -- codex for every agent (#35592)
* Support launcher command prefixes for all chat agents

* Preserve launcher path arguments and copy routing checks

* Match Japanese command guidance to the Agent settings label

* test(pi): expect version probe termination barrier

* test(chat): expect current command refusal guidance

* fix(acp): refuse copy arguments before launch preparation
2026-10-29 11:59:03 -07:00

181 lines
7.3 KiB
TypeScript

import { homedir } from 'node:os'
import { delimiter } from 'node:path'
import { beforeEach, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../shared/constants'
import type { GlobalSettings } from '../../shared/global-settings-types'
const mocks = vi.hoisted(() => {
const programs: Record<
string,
() => { command: string; prefixArgs: string[]; env: Record<string, string> }
> = {}
return {
refreshed: new Set<() => void>(),
releasesChanged: new Set<() => void>(),
observe: vi.fn(),
recapture: vi.fn(async () => {}),
programs,
installable: true
}
})
vi.mock('../preflight/workspace-agent-detection', () => ({
onAgentsRefreshedOnHost: (listener: () => void) => mocks.refreshed.add(listener)
}))
vi.mock('../node-runtime/managed-runtime-installation', () => ({
onManagedRuntimeReleasesChanged: (listener: () => void) => mocks.releasesChanged.add(listener)
}))
vi.mock('./structured-agent-launch-support', () => ({
observeStructuredAgentLaunch: mocks.observe,
resolveHostStructuredAgentLaunchEnvironment: async () => ({ PATH: '/usr/bin' })
}))
vi.mock('./structured-agent-shell-environment', () => ({
recaptureHostShellEnvironment: mocks.recapture
}))
// Grok stands for an agent this host cannot supervise (Windows without process start times).
vi.mock('./structured-agent-runtime-metadata', () => ({
STRUCTURED_AGENT_RUNTIME_METADATA: ['claude', 'grok', 'pi', 'opencode'].map((agent) => ({
definition: { agent },
supportsLocation: () => agent !== 'grok',
...(agent === 'grok'
? {}
: {
chatProgram: {
resolve: () => mocks.programs[agent](),
...(agent === 'opencode' ? { installable: () => mocks.installable } : {})
}
})
}))
}))
import { installStructuredAgentAvailability } from './structured-agent-availability'
const READY = { status: 'ready', version: '2.0.14' } as const
const TOO_OLD = { status: 'tooOld', version: '2.0.13', minimumVersion: '2.0.14' } as const
const opencodeAvailable = expect.objectContaining({ agent: 'opencode', state: 'available' })
const found =
(command: string, PATH = '/usr/bin', prefixArgs: string[] = []) =>
() => ({ command, prefixArgs, env: { PATH } })
beforeEach(() => {
mocks.refreshed.clear()
mocks.releasesChanged.clear()
mocks.installable = true
mocks.programs = {
claude: found('/usr/bin/launcher', '/usr/bin', ['run', '--', '/usr/bin/claude']),
pi: () => {
throw new Error('Command setting not runnable')
},
opencode: found('opencode')
}
mocks.observe.mockReset().mockResolvedValue(READY)
mocks.recapture.mockReset().mockResolvedValue(undefined)
})
function install() {
const listeners: ((updates: Partial<GlobalSettings>, settings: GlobalSettings) => void)[] = []
const store = {
onSettingsChanged: (listener: (typeof listeners)[number]) => {
listeners.push(listener)
return () => {}
}
}
const availability = installStructuredAgentAvailability(store, {
requireStore: () => ({ getSettings: () => getDefaultSettings('/home/test') })
})
const change = (updates: Partial<GlobalSettings>): void =>
listeners.forEach((listener) => listener(updates, getDefaultSettings('/home/test')))
return { availability, change }
}
it('shares a host-home snapshot even if shell recapture fails, retaining it without readers', async () => {
mocks.recapture.mockRejectedValueOnce(new Error('shell capture failed'))
const cwds: string[] = []
mocks.observe.mockImplementation(async (agent: string, cwd: () => Promise<string>) => {
cwds.push(await cwd())
if (agent === 'pi') {
throw new Error('Command setting not runnable')
}
return agent === 'opencode' ? TOO_OLD : READY
})
const { availability } = install()
const first = vi.fn()
const release = availability.subscribe(first)
await vi.waitFor(() => expect(first).toHaveBeenCalledTimes(1))
release()
expect(first.mock.calls[0][0]).toEqual([
{ agent: 'claude', state: 'available', chatProgram: 'available' },
{ agent: 'grok', state: 'refused', reason: 'agent', chatProgram: 'unknown' },
// A check that could not answer is no verdict.
{ agent: 'pi', state: 'unknown', chatProgram: 'unknown' },
{ agent: 'opencode', state: 'refused', reason: 'agent', chatProgram: 'installable' }
])
expect(cwds).toEqual([homedir(), homedir(), homedir()])
// Kept with no readers: a later one reads it at once, without probing again.
const later = vi.fn()
availability.subscribe(later)
expect(later).toHaveBeenCalledWith(first.mock.calls[0][0])
expect(mocks.observe).toHaveBeenCalledTimes(3)
})
it('recaptures for settings and uses the completed capture on Refresh', async () => {
const { availability, change } = install()
const read = vi.fn()
availability.subscribe(read)
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(1))
change({ terminalFontSize: 14 })
change({ agentCmdOverrides: { pi: '/opt/pi' } })
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(2))
const capture = Promise.withResolvers<void>()
let version: typeof READY | typeof TOO_OLD = TOO_OLD
mocks.observe.mockImplementation(async () => version)
mocks.recapture.mockImplementation(() => capture.promise)
mocks.refreshed.forEach((listener) => listener())
expect(mocks.recapture).toHaveBeenCalledTimes(2)
change({ agentDefaultEnv: { pi: { PI_TEST: 'updated' } } })
await new Promise<void>((resolve) => setImmediate(resolve))
expect(read).toHaveBeenCalledTimes(2)
expect(mocks.observe).toHaveBeenCalledTimes(9)
version = READY
capture.resolve()
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(3))
expect(read.mock.lastCall?.[0]).toContainEqual(opencodeAvailable)
})
it('captures fresh shell facts for a first subscriber even when Refresh had no listener', async () => {
const capture = Promise.withResolvers<void>()
let version: typeof READY | typeof TOO_OLD = TOO_OLD
mocks.observe.mockImplementation(async () => version)
mocks.recapture.mockImplementation(() => capture.promise)
mocks.refreshed.forEach((listener) => listener())
const { availability } = install()
const read = vi.fn()
availability.subscribe(read)
await new Promise<void>((resolve) => setImmediate(resolve))
expect(mocks.observe).not.toHaveBeenCalled()
version = READY
capture.resolve()
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(1))
expect(read.mock.lastCall?.[0]).toContainEqual(opencodeAvailable)
})
it('reads a missing program as missing, a workspace-relative PATH as unknown, and re-reads after an install', async () => {
mocks.installable = false
mocks.programs.claude = found('launcher', `bin${delimiter}/usr/bin`, [
'run',
'--',
'/usr/bin/claude'
])
const { availability } = install()
const read = vi.fn()
availability.subscribe(read)
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(1))
const programOf = (agent: string) =>
read.mock.lastCall?.[0].find((row: { agent: string }) => row.agent === agent)?.chatProgram
expect(programOf('claude')).toBe('unknown')
expect(programOf('opencode')).toBe('missing')
mocks.programs.opencode = found('/data/runtimes/opencode/bin/opencode')
mocks.releasesChanged.forEach((listener) => listener())
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(2))
expect(programOf('opencode')).toBe('available')
})