Files
orca/src/relay/agent-exec-handler.test.ts
T
Neil aecce221bd fix(test): deflake relay exec env, project boundary, and speech resume tests (#14446)
Three test-suite problems, all root-caused in the tests rather than in
production behavior.

1. src/relay/agent-exec-handler.test.ts (real failure, not a flake)

The two spawn-argument assertions failed with "Number of calls: 1" — spawn
ran, but the env differed. Cause: both assert
`expect.objectContaining({ ...process.env, ... })`, which demands that every
ambient variable reach the child verbatim. #7986 (1a6abc87d1) changed both
sides at once: it rewrote the assertion from `env: process.env` to that
objectContaining form, and in the same commit made the handler apply
`applyTerminalGitCredentialPromptGuard`, which appends its own entries to
Git's indexed-config protocol (GIT_CONFIG_COUNT / KEY_n / VALUE_n).

So whenever the test runner's own environment already carries that protocol —
exactly what Orca exports into its agent terminals — the snapshot expects
GIT_CONFIG_COUNT=2 while the correctly guarded child gets 4. The test passes
on a bare CI shell and fails when run from a guarded terminal.

The implementation is right: appending the guard after the caller's config is
the documented contract, and "guards wrapped agents after atomically replacing
inherited indexed config" already covers it. Fixed the test instead, by
clearing the guard-owned keys (GIT_CONFIG_* protocol and WSLENV) from the
ambient env for the duration of the suite and restoring them afterwards, so
the passthrough baseline is deterministic. No assertion was weakened or
removed.

2. project-view-wrapper-source-context-boundary.test.ts (flake: 30s timeout)

`buildProjectWorkItem` is a pure function, but it lived in
ProjectViewWrapper.tsx, so importing it pulled in the store, sonner, lucide,
and the whole UI kit — ~8.8s of transform and module evaluation for one
assertion, which tipped past the 30s limit under parallel load.

Extracted it to project-work-item.ts (its only dependency is
githubProjectHost) and pointed the test there. Both test cases are unchanged.
Also dropped the now-unneeded happy-dom environment, since nothing in the file
touches the DOM any more. 9.15s -> 0.12s.

3. model-manager-download-resume.test.ts (flake: 30s timeout)

"bounds a server that advances by pathologically tiny segments forever"
drives the loop to the MAX_TOTAL_DOWNLOAD_REQUESTS ceiling of 4096. Each
iteration did a real writeFileSync plus two statSync calls through
getPartialDownloadBytes — ~12k synchronous filesystem syscalls in a tight
loop. Fast on an idle disk, but it serializes against every other vitest
worker on a loaded machine, which is what blew the per-test timeout.

Stubbed getPartialDownloadBytes to read the byte counter the test already
maintains, so the loop is pure CPU. The file was only ever a stand-in for that
counter. Ceiling and rejection assertions are unchanged: 332ms -> 15ms.

The two remaining ~1.1s cases in that file spend their time in the real 1s
retry backoff around real stream and file-write plumbing; they are left on
real timers because faking them would mean faking the transport too, and 1.1s
leaves ample headroom.
2026-08-13 21:51:55 -07:00

478 lines
14 KiB
TypeScript

import { execFile, spawn } from 'node:child_process'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ChildProcess from 'node:child_process'
import { createFakeChild, createHandlers, requestContext } from './agent-exec-handler-test-harness'
import { TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV } from '../shared/terminal-git-credential-guard'
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal<typeof ChildProcess>()
return {
...actual,
execFile: vi.fn(),
spawn: vi.fn()
}
})
const spawnMock = vi.mocked(spawn)
const execFileMock = vi.mocked(execFile)
type AgentExecResult = { exitCode: number | null; timedOut: boolean }
const GUARD_OWNED_ENV_RE = /^(?:GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)|WSLENV)$/
describe('AgentExecHandler', () => {
let ambientGuardEnv: Record<string, string | undefined> = {}
beforeEach(() => {
spawnMock.mockReset()
execFileMock.mockReset()
// Why: the guard rewrites these, so an already-guarded runner (Orca guards
// its own agent terminals) would not see its ambient values passed through.
ambientGuardEnv = {}
for (const key of Object.keys(process.env).filter((name) => GUARD_OWNED_ENV_RE.test(name))) {
ambientGuardEnv[key] = process.env[key]
delete process.env[key]
}
})
afterEach(() => {
for (const [key, value] of Object.entries(ambientGuardEnv)) {
if (value !== undefined) {
process.env[key] = value
}
}
})
it('executes a non-interactive command with captured output and stdin', async () => {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: ['--flag', 42],
cwd: '/repo',
stdin: 'PROMPT',
timeoutMs: 5_000
},
requestContext()
)
child.stdout.emit('data', Buffer.from('message'))
child.stderr.emit('data', Buffer.from('warning'))
child.emit('close', 0)
await expect(pending).resolves.toEqual({
stdout: 'message',
stderr: 'warning',
exitCode: 0,
timedOut: false,
canceled: false
})
expect(spawnMock).toHaveBeenCalledWith('agent', ['--flag', '42'], {
cwd: '/repo',
env: expect.objectContaining({
...process.env,
GIT_TERMINAL_PROMPT: '0',
GCM_INTERACTIVE: 'never'
}),
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true
})
expect(child.stdin.end).toHaveBeenCalledWith('PROMPT')
})
it('merges caller-supplied provider environment into the spawned command environment', async () => {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'codex',
args: ['exec'],
cwd: '/repo',
stdin: 'PROMPT',
timeoutMs: 5_000,
env: {
CODEX_HOME: '/managed/codex-home',
PATH: '/managed/bin'
}
},
requestContext()
)
child.emit('close', 0)
await expect(pending).resolves.toMatchObject({
exitCode: 0,
timedOut: false
})
expect(spawnMock).toHaveBeenCalledWith('codex', ['exec'], {
cwd: '/repo',
env: expect.objectContaining({
...process.env,
CODEX_HOME: '/managed/codex-home',
PATH: '/managed/bin'
}),
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true
})
})
it('consumes an unattended marker and applies the full Git guard on the relay host', async () => {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: '/bin/bash',
args: ['-lc', 'git fetch'],
cwd: '/repo',
timeoutMs: 5_000,
env: { [TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV]: 'guard' }
},
requestContext()
)
child.emit('close', 0)
await expect(pending).resolves.toMatchObject({ exitCode: 0 })
const env = spawnMock.mock.calls[0]?.[2]?.env as Record<string, string>
expect(env[TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV]).toBeUndefined()
expect(env.GIT_TERMINAL_PROMPT).toBe('0')
expect(env.GCM_INTERACTIVE).toBe('never')
expect(Object.values(env)).toContain('credential.interactive')
expect(Object.values(env)).toContain('credential.guiPrompt')
})
it('guards wrapped agents after atomically replacing inherited indexed config', async () => {
const keys = [
'GIT_CONFIG_COUNT',
'GIT_CONFIG_KEY_0',
'GIT_CONFIG_VALUE_0',
'GIT_CONFIG_KEY_1',
'GIT_CONFIG_VALUE_1'
] as const
const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]]))
process.env.GIT_CONFIG_COUNT = '2'
process.env.GIT_CONFIG_KEY_0 = 'base.one'
process.env.GIT_CONFIG_VALUE_0 = 'one'
process.env.GIT_CONFIG_KEY_1 = 'base.two'
process.env.GIT_CONFIG_VALUE_1 = 'two'
try {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'npx',
args: ['codex', 'exec'],
cwd: '/repo',
timeoutMs: 5_000,
env: {
GIT_CONFIG_COUNT: '1',
GIT_CONFIG_KEY_0: 'http.proxy',
GIT_CONFIG_VALUE_0: 'http://proxy.invalid'
}
},
requestContext()
)
child.emit('close', 0)
await expect(pending).resolves.toMatchObject({ exitCode: 0 })
const env = spawnMock.mock.calls[0]?.[2]?.env as Record<string, string>
expect(env.GIT_TERMINAL_PROMPT).toBe('0')
expect(env.GIT_CONFIG_COUNT).toBe('3')
expect(env.GIT_CONFIG_KEY_0).toBe('http.proxy')
expect(env.GIT_CONFIG_KEY_1).toBe('credential.interactive')
expect(env.GIT_CONFIG_KEY_2).toBe('credential.guiPrompt')
expect(Object.values(env)).not.toContain('base.two')
} finally {
for (const key of keys) {
if (saved[key] === undefined) {
delete process.env[key]
} else {
process.env[key] = saved[key]
}
}
}
})
it('cancels the in-flight command for the requested cwd', async () => {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: [],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000
},
requestContext()
)
await expect(
handlers.get('agent.cancelExec')!({ cwd: '/repo' }, requestContext())
).resolves.toEqual({ canceled: true })
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12345', '/T', '/F'],
expect.any(Function)
)
} else {
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
}
child.emit('close', null)
await expect(pending).resolves.toMatchObject({
exitCode: null,
timedOut: false,
canceled: true
})
})
it('cancels only the matching operation lane for a cwd', async () => {
const commitChild = createFakeChild()
const pullRequestChild = createFakeChild()
pullRequestChild.pid = 12346
spawnMock
.mockReturnValueOnce(commitChild as never)
.mockReturnValueOnce(pullRequestChild as never)
const handlers = createHandlers()
const commit = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: [],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000,
operation: 'commit-message'
},
requestContext()
)
const pullRequest = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: [],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000,
operation: 'pull-request-fields'
},
requestContext()
)
await expect(
handlers.get('agent.cancelExec')!(
{ cwd: '/repo', operation: 'commit-message' },
requestContext()
)
).resolves.toEqual({ canceled: true })
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12345', '/T', '/F'],
expect.any(Function)
)
expect(execFileMock).not.toHaveBeenCalledWith(
'taskkill',
['/pid', '12346', '/T', '/F'],
expect.any(Function)
)
} else {
expect(commitChild.kill).toHaveBeenCalledWith('SIGKILL')
expect(pullRequestChild.kill).not.toHaveBeenCalled()
}
commitChild.emit('close', null)
pullRequestChild.stdout.emit(
'data',
Buffer.from('{"base":"main","title":"Update README","body":"Details","draft":false}')
)
pullRequestChild.emit('close', 0)
await expect(commit).resolves.toMatchObject({
exitCode: null,
timedOut: false,
canceled: true
})
await expect(pullRequest).resolves.toMatchObject({
exitCode: 0,
timedOut: false,
canceled: false
})
})
it('kills the active command when the request aborts', async () => {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const controller = new AbortController()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: [],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000
},
{ clientId: 1, isStale: () => controller.signal.aborted, signal: controller.signal }
)
controller.abort()
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12345', '/T', '/F'],
expect.any(Function)
)
} else {
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
}
child.emit('close', null)
await expect(pending).resolves.toMatchObject({
exitCode: null,
timedOut: false,
canceled: true
})
expect(child.stdout.listenerCount('data')).toBe(0)
expect(child.stderr.listenerCount('data')).toBe(0)
expect(child.listenerCount('error')).toBe(0)
expect(child.listenerCount('close')).toBe(0)
})
it('cancels a superseded command in the same operation lane', async () => {
const firstChild = createFakeChild()
const secondChild = createFakeChild()
secondChild.pid = 12346
spawnMock.mockReturnValueOnce(firstChild as never).mockReturnValueOnce(secondChild as never)
const handlers = createHandlers()
const first = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: ['first'],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000,
operation: 'commit-message'
},
requestContext()
)
const second = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: ['second'],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000,
operation: 'commit-message'
},
requestContext()
)
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12345', '/T', '/F'],
expect.any(Function)
)
expect(execFileMock).not.toHaveBeenCalledWith(
'taskkill',
['/pid', '12346', '/T', '/F'],
expect.any(Function)
)
} else {
expect(firstChild.kill).toHaveBeenCalledWith('SIGKILL')
expect(secondChild.kill).not.toHaveBeenCalled()
}
await expect(
handlers.get('agent.cancelExec')!(
{ cwd: '/repo', operation: 'commit-message' },
requestContext()
)
).resolves.toEqual({ canceled: true })
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12346', '/T', '/F'],
expect.any(Function)
)
} else {
expect(secondChild.kill).toHaveBeenCalledWith('SIGKILL')
}
firstChild.emit('close', null)
secondChild.emit('close', null)
await expect(first).resolves.toMatchObject({ canceled: true })
await expect(second).resolves.toMatchObject({ canceled: true })
})
it('reports when cancellation has no matching in-flight command', async () => {
const handlers = createHandlers()
await expect(
handlers.get('agent.cancelExec')!({ cwd: '/repo' }, requestContext())
).resolves.toEqual({ canceled: false })
})
it('settles timed-out commands even when the killed child does not close', async () => {
vi.useFakeTimers()
try {
const child = createFakeChild()
spawnMock.mockReturnValue(child as never)
const handlers = createHandlers()
const pending = handlers.get('agent.execNonInteractive')!(
{
binary: 'agent',
args: [],
cwd: '/repo',
stdin: null,
timeoutMs: 5_000
},
requestContext()
) as Promise<AgentExecResult>
const outcomePromise = pending.then((result) =>
result.timedOut ? `timed-out:${result.exitCode}` : 'not-timed-out'
)
await vi.advanceTimersByTimeAsync(5_000)
const outcome = await Promise.race([outcomePromise, Promise.resolve('pending')])
expect(outcome).toBe('timed-out:null')
if (process.platform === 'win32') {
expect(execFileMock).toHaveBeenCalledWith(
'taskkill',
['/pid', '12345', '/T', '/F'],
expect.any(Function)
)
} else {
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
}
expect(child.stdout.listenerCount('data')).toBe(0)
expect(child.stderr.listenerCount('data')).toBe(0)
expect(child.listenerCount('error')).toBe(0)
expect(child.listenerCount('close')).toBe(0)
} finally {
vi.useRealTimers()
}
})
})