test: fail unit tests that run a real agent CLI from PATH (#38961)

* test: stop unit tests from running the developer's real agent CLIs

A live relay test reached the host's real `claude --version` through the
inherited PATH. On a machine whose claude wrapper never exits, each run left
an orphaned loop behind (14 were found on one host, running for days).

- Add a unit-test setup guard that shadows agent CLI names on PATH with
  stand-ins that fail the test which ran them.
- Give the live relay test its own claude/codex stubs.
- Kill the whole process tree when the WSL guest's version probe times out.

* test: keep structured-session tests off the real codex CLI

These hosts get a scripted transport, but the at-rest `/` command read
spawns the resolved codex command directly, so on a machine with codex
installed each run started a real `codex app-server`. Point them at a
missing binary, as on CI.

* test: isolate agent discovery in session fixtures
This commit is contained in:
Neil
2026-11-06 12:56:01 -08:00
committed by GitHub
parent f0bfce8fce
commit c85d633837
11 changed files with 176 additions and 9 deletions
@@ -15,6 +15,7 @@ export const UNIT_INCLUDE = [
export const UNIT_SETUP_FILES = [
'config/scripts/test-runtime/environment/vitest-real-agent-home-write-guard.ts',
'config/scripts/test-runtime/environment/vitest-real-agent-cli-guard.ts',
'config/scripts/test-runtime/environment/vitest-bun-node-builtins.ts',
'config/scripts/test-runtime/dom/vitest-dom-setup.ts',
'config/scripts/test-runtime/environment/vitest-host-ports-setup.ts',
@@ -23,7 +23,8 @@ const BUILD_SCRIPTS = [
'config/scripts/test-runtime/dom/happy-dom-offscreen-canvas.ts',
'config/scripts/test-runtime/dom/happy-dom-mutation-observer-retention.ts',
'config/scripts/test-runtime/environment/vitest-host-ports-setup.ts',
'config/scripts/test-runtime/environment/vitest-real-agent-home-write-guard.ts'
'config/scripts/test-runtime/environment/vitest-real-agent-home-write-guard.ts',
'config/scripts/test-runtime/environment/vitest-real-agent-cli-guard.ts'
]
const NODE_SERVER_POLICY_TESTS = new Set([
'config/scripts/node-server/qualification/node-server-change-scope.test.mjs'
@@ -0,0 +1,27 @@
import { spawnSync } from 'node:child_process'
import { afterEach, describe, expect, it } from 'vitest'
import { takeRealAgentCliCalls } from './vitest-real-agent-cli-guard'
afterEach(() => {
takeRealAgentCliCalls()
})
describe.skipIf(!globalThis.orcaRealAgentCliGuard)('vitest real-agent-cli guard', () => {
it('blocks version discovery without treating an absent agent as a test failure', () => {
const result = spawnSync('claude', ['--version'], { encoding: 'utf8', env: process.env })
expect(result.status).toBe(127)
expect(takeRealAgentCliCalls()).toEqual([])
})
it('blocks and records an agent CLI resolved from the inherited PATH', () => {
const result = spawnSync('claude', ['-p', 'hello'], { encoding: 'utf8', env: process.env })
expect(result.status).toBe(127)
expect(result.stderr).toMatch(/real-agent-cli guard/)
expect(takeRealAgentCliCalls()).toEqual(['claude -p hello'])
})
it('records calls from a shell that keeps the inherited PATH', () => {
spawnSync('/bin/sh', ['-c', 'copilot --help; codex exec x'], { env: process.env })
expect(takeRealAgentCliCalls()).toEqual(['copilot --help', 'codex exec x'])
})
})
@@ -0,0 +1,103 @@
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { afterAll, afterEach } from 'vitest'
// Why: a test that inherits PATH can launch the developer's real agent CLI. A wrapper that never
// exits there (e.g. a version-manager shim that re-resolves itself) outlives the worker as an
// orphan loop, so stand-ins shadow those names and fail the test that reached them.
const AGENT_CLI_NAMES = [
'amp',
'auggie',
'claude',
'codex',
'copilot',
'crush',
'cursor-agent',
'droid',
'gemini',
'goose',
'grok',
'kimi',
'omp',
'opencode',
'pi',
'qwen'
]
// Real-agent opt-in suites intentionally run the installed CLIs.
const REAL_AGENT_CLI_SWITCHES = [
'ORCA_REAL_CLAUDE_CLI_TEST',
'ORCA_REAL_CLAUDE_SUPERVISED_STOP',
'ORCA_CODEX_CONTRACT_BINARY',
'ORCA_CODEX_NO_DAEMON_CONTRACT_BINARY',
'ORCA_CODEX_TRUST_CONTRACT_BINARY',
'ORCA_REPRO_CODEX_BINARY'
]
type GuardState = { dir: string; log: string }
function quoteSh(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`
}
function install(): GuardState | null {
if (process.platform === 'win32' || REAL_AGENT_CLI_SWITCHES.some((name) => process.env[name])) {
return null
}
const dir = mkdtempSync(join(tmpdir(), 'orca-agent-cli-guard-'))
const log = join(dir, 'calls.log')
writeFileSync(log, '')
const script = [
'#!/bin/sh',
// Discovery is expected in daemon tests; report the CLI as unavailable.
'if [ "$#" -eq 1 ] && [ "$1" = "--version" ]; then exit 127; fi',
`printf '%s %s\\n' "\${0##*/}" "$*" >> ${quoteSh(log)}`,
`echo "[vitest real-agent-cli guard] blocked real \${0##*/}; give the test its own stub on PATH" >&2`,
'exit 127',
''
].join('\n')
for (const name of AGENT_CLI_NAMES) {
const path = join(dir, name)
writeFileSync(path, script)
chmodSync(path, 0o755)
}
process.env.PATH = process.env.PATH ? `${dir}${delimiter}${process.env.PATH}` : dir
process.once('exit', () => rmSync(dir, { recursive: true, force: true }))
return { dir, log }
}
declare global {
// Install once per process; per-file installs would stack PATH entries.
var orcaRealAgentCliGuard: GuardState | null | undefined
}
if (globalThis.orcaRealAgentCliGuard === undefined) {
globalThis.orcaRealAgentCliGuard = install()
}
const state = globalThis.orcaRealAgentCliGuard
/** Drains recorded calls; only the guard's own self-test should need this. */
export function takeRealAgentCliCalls(): string[] {
if (!state) {
return []
}
let text = ''
try {
text = readFileSync(state.log, 'utf8')
writeFileSync(state.log, '')
} catch {
return []
}
return text.split('\n').filter(Boolean)
}
function failOnCalls(): void {
const calls = takeRealAgentCliCalls()
if (calls.length > 0) {
throw new Error(
`[vitest real-agent-cli guard] test ran an agent CLI from the inherited PATH:\n${calls.join('\n')}`
)
}
}
afterEach(failOnCalls)
afterAll(failOnCalls)
+1
View File
@@ -8,6 +8,7 @@
"./scripts/test-runtime/environment/vitest-caller-identity-env-setup.ts",
"./scripts/test-runtime/environment/vitest-inherited-git-env-setup.ts",
"./scripts/test-runtime/environment/vitest-real-agent-home-write-guard.ts",
"./scripts/test-runtime/environment/vitest-real-agent-cli-guard.ts",
"./scripts/test-runtime/environment/vitest-bun-node-builtins.ts",
"./scripts/test-runtime/dom/vitest-dom-setup.ts",
"./scripts/test-runtime/dom/happy-dom-offscreen-canvas.ts",
@@ -1,7 +1,15 @@
// Run the real bundle, stdio mux, installers, and HTTP ingest using node in place of wsl.exe.
import { execFileSync, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { delimiter, join } from 'node:path'
import { createServer } from 'node:net'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
@@ -57,6 +65,16 @@ describe.skipIf(process.platform === 'win32')(
it('delivers a Claude hook POST from the live relay into ingestRemote and installs guest hooks', async () => {
fakeHome = mkdtempSync(join('/tmp', 'wsl-live-home-'))
// Guest detection runs `claude --version`; stub it so the relay never runs the host's real CLI.
const agentBin = join(fakeHome, 'agent-bin')
mkdirSync(agentBin)
for (const [name, version] of [
['claude', '2.1.0 (Claude Code)'],
['codex', 'codex-cli 0.160.0']
]) {
writeFileSync(join(agentBin, name), `#!/bin/sh\necho '${version}'\n`)
chmodSync(join(agentBin, name), 0o755)
}
const preferredPort = await pickFreePort()
const version = readFileSync(join(BUNDLE_DIR, '.version'), 'utf8').trim()
@@ -87,9 +105,12 @@ describe.skipIf(process.platform === 'win32')(
listDistros: async () => ['LiveDistro'],
spawnRelay: (_distro, env) => {
child = spawn(process.execPath, [BUNDLE_JS], {
env: { ...env, HOME: fakeHome },
stdio: ['pipe', 'pipe', 'pipe']
}) as ChildProcessWithoutNullStreams
env: {
...env,
HOME: fakeHome,
PATH: `${agentBin}${delimiter}${env.PATH ?? process.env.PATH ?? ''}`
}
})
return child
},
runInstall: async () => {
@@ -52,6 +52,15 @@ afterEach(async () => {
})
export class AuthorityRuntime extends OrcaRuntimeService {
protected override structuredAgentCatalogDependencies() {
const missing = () => ({ command: join(root, 'agent-not-installed'), prefixArgs: [] })
return {
...super.structuredAgentCatalogDependencies(),
resolveClaudeCommand: missing,
resolveCodexCommand: missing
}
}
protected override async refreshMobileSessionPtyRecords() {
return new Set<string>()
}
@@ -121,7 +121,8 @@ describe('the chat strip and the session list read the same host child records',
resolveWorkspacePath: async () => root,
resolveLaunchArgs: () => [],
resolveClaudeAuthPolicy: () => ({ account: 'managed' }),
resolveCodexCommand: () => ({ command: 'codex', prefixArgs: [] }),
// Side reads (at-rest `/` commands) spawn the CLI directly; a missing binary keeps them off the real codex.
resolveCodexCommand: () => ({ command: join(root, 'codex-not-installed'), prefixArgs: [] }),
resolveEnvironment: async () => ({ PATH: process.env.PATH }),
...scriptedCodexTransport(openConnection),
statusSink
@@ -87,7 +87,7 @@ describe('structured Codex child work through the production runtime', () => {
resolveWorkspacePath: async () => root!,
resolveLaunchArgs: () => [],
resolveClaudeAuthPolicy: () => ({ account: 'managed' }),
resolveCodexCommand: () => ({ command: 'codex', prefixArgs: [] }),
resolveCodexCommand: () => ({ command: join(root!, 'codex-not-installed'), prefixArgs: [] }),
resolveEnvironment: async () => ({ PATH: process.env.PATH }),
...scriptedCodexTransport(openConnection),
statusSink: {
+2
View File
@@ -151,6 +151,8 @@ async function probeCommandVersion(executablePath: string): Promise<string | nul
? `${executableDir}${path.delimiter}${inheritedPath}`
: executableDir
},
// Why: a CLI wrapper that never exits keeps its children alive after a root-only kill.
terminationBarrier: true,
timeoutMs: 5_000,
maxOutputBytes: 4_096
})
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
import { join } from 'node:path'
import {
fakeCodex,
THREAD_ID as CODEX_THREAD_ID
@@ -519,7 +520,7 @@ it('publishes accepted Codex fresh policy to desktop and phone without stale sta
resolveWorkspacePath: async () => root,
resolveLaunchArgs: () => [],
resolveClaudeAuthPolicy: () => ({ account: 'managed' as const }),
resolveCodexCommand: () => ({ command: 'codex', prefixArgs: [] }),
resolveCodexCommand: () => ({ command: join(root, 'codex-not-installed'), prefixArgs: [] }),
readCodexInstallation: async () => ({ version: '0.136.0' }),
readProcessStartTime: async () => 1_700_000_000_000,
openCodexConnection: codex.openConnection