mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* feat(agent.launch): a launch can name chat or terminal, and the CLI gets --surface chat|terminal agent.launch gains an optional `surface` field, read by hosts advertising agent.launch.surface.v1. `terminal` always runs the agent's CLI; `chat` runs a chat even when the user's default is a terminal, and one the host can prove it cannot open is refused with a typed no-effects error before anything is created, so a retry answers with the same refusal. The pick is part of the replay fingerprint. `orca agent launch` and `orca worktree create --agent` accept --surface chat|terminal and refuse it on a host that would drop it, including the legacy worktree.create fallback. * test(agent.launch): pin the surface pick beside the other pre-start failures * fix(agent.launch): settle requested surfaces from resolved workspace facts * fix(agent.launch): describe fallback boundaries and correct test types
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { createServer } from 'node:net'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { runProcess } from '@orca/process-host'
|
|
import { expect, it } from 'vitest'
|
|
import { z } from 'zod'
|
|
import {
|
|
AGENT_LAUNCH_RUNTIME_CAPABILITIES,
|
|
AGENT_LAUNCH_SURFACE_RUNTIME_CAPABILITY
|
|
} from '../shared/agent-launch-runtime-capability'
|
|
import { agentLaunchFixture } from './agent-launch-test-fixtures'
|
|
|
|
it('real CLI processes carry surface choices and refuse invalid or unsupported picks before mutation', async () => {
|
|
const manifest = createRequire(resolve('package.json')).resolve('bun/package.json')
|
|
const { bin } = z
|
|
.object({ bin: z.object({ bun: z.string() }) })
|
|
.parse(JSON.parse(readFileSync(manifest, 'utf8')))
|
|
const program = join(dirname(manifest), bin.bun)
|
|
const root = mkdtempSync(join(tmpdir(), 'orca-launch-process-'))
|
|
const endpoint =
|
|
process.platform === 'win32' ? `\\\\.\\pipe\\launch-${randomUUID()}` : join(root, 'rpc')
|
|
const requests: { method: string; params: Record<string, unknown> }[] = []
|
|
let capabilities: readonly string[] = AGENT_LAUNCH_RUNTIME_CAPABILITIES
|
|
const server = createServer((socket) => {
|
|
let buffer = ''
|
|
socket.setEncoding('utf8')
|
|
socket.on('data', (chunk: string) => {
|
|
buffer += chunk
|
|
if (!buffer.includes('\n')) {
|
|
return
|
|
}
|
|
const request = z
|
|
.object({
|
|
id: z.string(),
|
|
method: z.string(),
|
|
authToken: z.literal('fixture-token'),
|
|
params: z.record(z.string(), z.unknown()).default({})
|
|
})
|
|
.parse(JSON.parse(buffer.slice(0, buffer.indexOf('\n'))))
|
|
requests.push(request)
|
|
socket.end(
|
|
`${JSON.stringify({
|
|
id: request.id,
|
|
ok: true,
|
|
result:
|
|
request.method === 'status.get'
|
|
? { capabilities }
|
|
: request.method === 'worktree.show'
|
|
? { worktree: { id: 'wt-fixture', lineage: null } }
|
|
: agentLaunchFixture('wt-fixture').result,
|
|
_meta: { runtimeId: 'fixture-host' }
|
|
})}\n`
|
|
)
|
|
})
|
|
})
|
|
try {
|
|
await new Promise<void>((ready) => server.listen(endpoint, ready))
|
|
writeFileSync(
|
|
join(root, 'orca-runtime.json'),
|
|
JSON.stringify({
|
|
runtimeId: 'fixture-host',
|
|
pid: process.pid,
|
|
startedAt: 1,
|
|
authToken: 'fixture-token',
|
|
transports: [{ kind: process.platform === 'win32' ? 'named-pipe' : 'unix', endpoint }]
|
|
})
|
|
)
|
|
const env = {
|
|
...Object.fromEntries(
|
|
'HOME USERPROFILE APPDATA LOCALAPPDATA XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME ORCA_E2E_HOME_DIR ORCA_E2E_USER_DATA_DIR ORCA_USER_DATA_PATH TEMP TMP TMPDIR'
|
|
.split(' ')
|
|
.map((key) => [key, root])
|
|
),
|
|
CODEX_HOME: join(root, 'codex'),
|
|
CLAUDE_CONFIG_DIR: join(root, 'claude'),
|
|
ORCA_BACKGROUND_LAUNCH: '1',
|
|
ORCA_DISABLE_MACOS_LOGIN_SHELL: '1',
|
|
PATH: process.env.PATH,
|
|
SystemRoot: process.env.SystemRoot,
|
|
ComSpec: process.env.ComSpec
|
|
}
|
|
const mutate = () =>
|
|
requests.filter(
|
|
({ method }) => method.startsWith('agent.launch') || method === 'worktree.create'
|
|
)
|
|
for (const command of [
|
|
['agent', 'launch', '--worktree', 'id:wt-fixture'],
|
|
['worktree', 'create', '--repo', 'id:repo-fixture', '--name', 'task', '--no-parent']
|
|
]) {
|
|
const run = (flags: string[]) =>
|
|
runProcess({
|
|
program,
|
|
args: [resolve('src/cli/index.ts'), ...command, '--agent', 'claude', '--json', ...flags],
|
|
cwd: root,
|
|
env,
|
|
timeoutMs: 15_000
|
|
})
|
|
for (const surface of ['chat', 'terminal']) {
|
|
const response = await run(['--surface', surface])
|
|
expect(response.code, response.stderr + response.stdout).toBe(0)
|
|
expect(mutate().at(-1)).toMatchObject({
|
|
method: 'agent.launchReplay',
|
|
params: {
|
|
surface,
|
|
agent: 'claude',
|
|
launchSource: 'cli',
|
|
presentation: 'background',
|
|
target:
|
|
command[0] === 'agent'
|
|
? { kind: 'existing', worktree: 'id:wt-fixture' }
|
|
: { kind: 'create-worktree', create: { repo: 'id:repo-fixture', name: 'task' } }
|
|
}
|
|
})
|
|
}
|
|
const beforeInvalid = requests.length
|
|
for (const flags of [['--surface='], ['--surface'], ['--surface', 'invalid']]) {
|
|
const response = await run(flags)
|
|
expect(response.code).toBe(1)
|
|
expect(response.stdout).toContain('invalid_argument')
|
|
}
|
|
expect(requests).toHaveLength(beforeInvalid)
|
|
const beforeUnsupported = mutate().length
|
|
for (const oldCapabilities of [
|
|
[],
|
|
AGENT_LAUNCH_RUNTIME_CAPABILITIES.filter(
|
|
(capability) => capability !== AGENT_LAUNCH_SURFACE_RUNTIME_CAPABILITY
|
|
)
|
|
]) {
|
|
capabilities = oldCapabilities
|
|
const response = await run(['--surface', 'chat'])
|
|
expect(response.code).toBe(1)
|
|
expect(response.stdout).toContain('incompatible_runtime')
|
|
}
|
|
expect(mutate()).toHaveLength(beforeUnsupported)
|
|
capabilities = AGENT_LAUNCH_RUNTIME_CAPABILITIES
|
|
}
|
|
} finally {
|
|
await new Promise<void>((done) => server.close(() => done()))
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
}, 60_000)
|