Files
orca/src/main/runtime/structured-agent-launch-composition.test-support.ts
T
Brennan Benson 76735edf31 feat(acp): chat agents can declare a managed runtime that a start runs but never installs (#28460)
* feat(acp): ACP agents can declare a managed runtime that a start runs but never installs

A start resolves PATH and the agent's install folders first, then the newest whole release under
Orca's data folder (its completion receipt and member sizes checked); with none it refuses with
runtimeNotInstalled instead of downloading.

* fix(runtime): tell an unreadable runtime release from a broken one, and point a not-installed chat at the chat itself

A transient read error (EIO, EMFILE) no longer counts a working release as incomplete; only
definite absence or a mismatch does. The not-installed refusal no longer sends a remote chat's
user to this computer's Settings.

* fix(acp): a start runs Orca's installed release before a copy on PATH, and a copy elsewhere only beside every file it needs
2026-10-15 14:17:32 -07:00

153 lines
6.4 KiB
TypeScript

// Each agent's composed launch resolver over test doubles, built the way its registration builds
// it, so resolver tests exercise the shared launch path rather than a copy of it.
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import type { AgentChatPermissionMode } from '../../shared/agent-chat-permission-mode'
import type { AcpLaunchSpec } from '../acp/acp-launch-specs'
import { acpStructuredAgentDefinition } from '../acp/acp-structured-agent-definitions'
import {
acpStructuredLaunchPart,
type AcpStructuredLaunchPartDeps
} from '../acp/acp-structured-launch-resolution'
import { CLAUDE_STRUCTURED_AGENT } from '../claude/claude-structured-agent-definition'
import type { ClaudeEnvDeps } from '../claude/claude-structured-child-env'
import {
claudeStructuredLaunchPart,
type ClaudeStructuredLaunchPartDeps
} from '../claude/claude-structured-launch-resolution'
import { CODEX_STRUCTURED_AGENT } from '../codex/codex-structured-agent-definition'
import {
codexStructuredLaunchPart,
type CodexStructuredLaunchPartDeps
} from '../codex/codex-structured-launch-resolution'
import type { PrepareNativeChatVisuals } from '../native-chat/native-chat-visuals-delivery'
import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution'
import { PI_RPC_AGENT } from '../pi/rpc-agent-definition'
import { piRpcLaunchPart, type PiRpcLaunchPartDeps } from '../pi/rpc-launch-resolution'
import type { StructuredAgentDefinition } from '../native-chat/agent-session-wire/structured-agent-definition'
import {
composeStructuredLaunch,
composeStructuredLaunchRead,
type StructuredAgentLaunchPartResolver,
type StructuredAgentLaunchSources
} from './structured-agent-launch-composition'
export const TEST_SPAWN_TOKEN = 'test-spawn-token'
export type LaunchTestSources = Pick<StructuredAgentLaunchSources, 'store'> & {
resolveWorkspacePath?: (workspaceId: string) => Promise<string>
/** The base env; none is an empty one. */
resolveEnvironment?: () => Promise<NodeJS.ProcessEnv>
resolveLaunchEnv?: (agent: string) => Record<string, string>
resolveCommandSettings?: () => StructuredAgentCommandSettings
prepareVisuals?: PrepareNativeChatVisuals
refreshAfterMissingProgram?: (askedAt: number) => Promise<void>
}
function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
return Object.fromEntries(
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)
)
}
function launchSources(deps: LaunchTestSources): StructuredAgentLaunchSources {
const resolveEnvironment = deps.resolveEnvironment
const resolveBaseEnvironment = async () => definedEnv((await resolveEnvironment?.()) ?? {})
return {
store: deps.store,
resolveWorkspacePath: deps.resolveWorkspacePath ?? (async (id) => `/repos/${id}`),
resolveBaseEnvironment,
resolveAgentEnvironment: async (agent) => ({
...(await resolveBaseEnvironment()),
...deps.resolveLaunchEnv?.(agent)
}),
...(deps.resolveLaunchEnv ? { resolveAgentLaunchEnv: deps.resolveLaunchEnv } : {}),
...(deps.resolveCommandSettings ? { resolveCommandSettings: deps.resolveCommandSettings } : {}),
...(deps.prepareVisuals ? { prepareVisuals: deps.prepareVisuals } : {}),
...(deps.refreshAfterMissingProgram
? { refreshAfterMissingProgram: deps.refreshAfterMissingProgram }
: {})
}
}
/** Sealed with a spawn token, as an adapter's start calls it; with none, the unsealed read. */
function withTestSpawnToken<L>(
definition: StructuredAgentDefinition,
sources: StructuredAgentLaunchSources,
part: StructuredAgentLaunchPartResolver<L>,
defaultToken: string | null
) {
const resolve = composeStructuredLaunch(definition, sources, part)
const read = composeStructuredLaunchRead(definition, sources, part)
return (input: {
identity: AgentSessionJournalIdentity
spawnToken?: string | null
permissionMode?: AgentChatPermissionMode
}) => {
const spawnToken = input.spawnToken === undefined ? defaultToken : input.spawnToken
return spawnToken === null
? read(input.identity)
: resolve({
identity: input.identity,
spawnToken,
...(input.permissionMode ? { permissionMode: input.permissionMode } : {})
})
}
}
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
export function createCodexStructuredLaunchResolver(
deps: LaunchTestSources & CodexStructuredLaunchPartDeps
) {
return withTestSpawnToken(
CODEX_STRUCTURED_AGENT,
launchSources(deps),
codexStructuredLaunchPart(deps),
null
)
}
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
export function createAcpStructuredLaunchResolver(
spec: AcpLaunchSpec,
deps: LaunchTestSources &
Omit<AcpStructuredLaunchPartDeps, 'dataDirectory'> &
Partial<Pick<AcpStructuredLaunchPartDeps, 'dataDirectory'>>
) {
return withTestSpawnToken(
acpStructuredAgentDefinition(spec),
launchSources(deps),
acpStructuredLaunchPart(spec, { dataDirectory: '/orca-data', ...deps }),
null
)
}
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
export function createPiRpcLaunchResolver(deps: LaunchTestSources & PiRpcLaunchPartDeps) {
const resolve = withTestSpawnToken(PI_RPC_AGENT, launchSources(deps), piRpcLaunchPart(deps), null)
return (identity: AgentSessionJournalIdentity, spawnToken: string | null = null) =>
resolve({ identity, spawnToken })
}
/** Sealed with `TEST_SPAWN_TOKEN` unless the test passes another: Claude's launch env has always
* carried the session's identity. `resolveEnv` is Claude's overlay; `resolveInheritedEnv`, its
* base, defaults to this process's env as a launch without one inherits it. */
export function createClaudeStructuredLaunchResolver(
deps: Omit<LaunchTestSources, 'resolveEnvironment' | 'resolveLaunchEnv'> &
Pick<ClaudeEnvDeps, 'resolveInheritedEnv'> & {
resolveEnv?: () => Record<string, string> | undefined
} & ClaudeStructuredLaunchPartDeps
) {
const { resolveEnv, resolveInheritedEnv } = deps
return withTestSpawnToken(
CLAUDE_STRUCTURED_AGENT,
launchSources({
...deps,
resolveEnvironment: resolveInheritedEnv ?? (async () => process.env),
...(resolveEnv ? { resolveLaunchEnv: () => resolveEnv() ?? {} } : {})
}),
claudeStructuredLaunchPart(deps),
TEST_SPAWN_TOKEN
)
}