mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-11 02:13:47 +02:00
fix(test-harness): return promptly when a timed-out child leaves pipe-holding orphans
proc.kill() only signals the sh -c wrapper; the claude child survives as an orphan that inherited our stdout/stderr pipes, blocking the stream drain until it exits (observed: a 600s spawn timeout stretching to 1431s and tripping bun's per-test timeout with no result). On timeout, cancel the stdout reader; race the stderr drain against child exit + 5s grace. Streamed transcript lines survive the cancel, so callers still get their evidence. Regression test: test/session-runner-timeout.test.ts (fake claude spawns a pipe-holding orphan; fails in 30s without the fix, passes in 8s with it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,12 @@ export async function runSkillTest(options: {
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
proc.kill();
|
||||
// proc.kill() only signals the `sh -c` wrapper. The claude child it
|
||||
// spawned can survive as an orphan that inherited our stdout/stderr
|
||||
// pipes, so without cancel() the read loop below blocks until the
|
||||
// orphan finally exits (observed: a 600s timeout stretching past 1400s
|
||||
// and tripping bun's per-test timeout instead of returning a result).
|
||||
reader.cancel().catch(() => { /* stream already closed */ });
|
||||
}, timeout);
|
||||
|
||||
// Stream NDJSON from stdout for real-time progress
|
||||
@@ -289,7 +295,18 @@ export async function runSkillTest(options: {
|
||||
collectedLines.push(buf);
|
||||
}
|
||||
|
||||
stderr = await stderrPromise;
|
||||
// Same orphan hazard as stdout: an orphaned grandchild holding stderr open
|
||||
// would block the drain forever. Race it against child exit + a short grace
|
||||
// window; the normal path (pipes close with the child) still wins the race
|
||||
// and keeps full stderr.
|
||||
stderr = await Promise.race([
|
||||
stderrPromise,
|
||||
(async () => {
|
||||
await proc.exited;
|
||||
await new Promise((r) => setTimeout(r, 5_000));
|
||||
return '';
|
||||
})(),
|
||||
]);
|
||||
const exitCode = await proc.exited;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user