Files
orca/tests/e2e/agent-descendant-process-kill.spec.ts
Neil f1ab15045f test(e2e): keep the descendant-kill stand-in's marker path out of argv (#39201)
Claude launches now get --plugin-dir prepended (subagent navigation, #33322),
so the stand-in read '--plugin-dir' as its marker path and the spec never saw
the detached child. Route the spec on descendant-kill and Claude launch-flag
sources.
2026-11-06 22:58:52 +00:00

94 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
// Reproduce detached tool-child leakage after agent PTY kill (STA-1800); name the stand-in claude for agent recognition.
test('killing an agent PTY terminates its detached-pgid descendants', async ({ orcaPage }) => {
test.skip(process.platform === 'win32', 'descendant tree-kill is POSIX-only for now')
const stage = mkdtempSync(join(tmpdir(), 'orca-agent-descendant-'))
const markerPath = join(stage, 'detached-child.pid')
const spawnerPath = join(stage, 'spawn-detached.cjs')
writeFileSync(
spawnerPath,
[
"const { spawn } = require('node:child_process')",
// setsid creates the detached process group a dying shell’s SIGHUP cannot reach.
"const child = spawn('sleep', ['31337'], { detached: true, stdio: 'ignore' })",
'child.unref()',
"require('node:fs').writeFileSync(process.argv[2], String(child.pid))",
// Keep the parent alive until kill so this tests descendants, not a child already orphaned by a crash.
'setInterval(() => {}, 1000)',
''
].join('\n')
)
const fakeAgentPath = join(stage, 'claude')
// Orca prepends its own Claude flags (e.g. --plugin-dir), so the marker path cannot ride in argv.
writeFileSync(
fakeAgentPath,
`#!/bin/sh\nexec "${process.execPath}" "${spawnerPath}" "${markerPath}"\n`
)
chmodSync(fakeAgentPath, 0o755)
let detachedChildPid = 0
try {
await waitForSessionReady(orcaPage)
const worktreeId = await waitForActiveWorktree(orcaPage)
const ptyId = await orcaPage.evaluate(
async ({ command, cwd, worktreeId: wt }) => {
const result = await window.api.pty.spawn({
cols: 120,
rows: 40,
cwd,
command,
launchAgent: 'claude',
worktreeId: wt
})
return result.id
},
{ command: `'${fakeAgentPath}'`, cwd: stage, worktreeId }
)
expect(ptyId).toBeTruthy()
await expect
.poll(() => existsSync(markerPath), {
timeout: 20_000,
message: 'stand-in agent never spawned its detached child'
})
.toBe(true)
detachedChildPid = Number(readFileSync(markerPath, 'utf8').trim())
expect(detachedChildPid).toBeGreaterThan(0)
expect(isProcessAlive(detachedChildPid)).toBe(true)
await orcaPage.evaluate((id) => window.api.pty.kill(id), ptyId)
await expect
.poll(() => isProcessAlive(detachedChildPid), {
timeout: 15_000,
message: `detached descendant ${detachedChildPid} survived the agent PTY kill`
})
.toBe(false)
} finally {
if (detachedChildPid > 0 && isProcessAlive(detachedChildPid)) {
try {
process.kill(detachedChildPid, 'SIGKILL')
} catch {
/* already gone */
}
}
rmSync(stage, { recursive: true, force: true })
}
})