mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
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.
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
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 })
|
||
}
|
||
})
|