test(conversation): fake-pi tool child signals ready after its TERM handler (row 46, #1528)

Fixes the K1, K3 and K10 race: a child could get SIGTERM before it
installed its handler. Author: Darkwing. Reviewer: Dewey (approve,
comment 26873). BUILD-LOG landing entry with gate counts and the
correction to decision 72's probe claim.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 08:30:01 -05:00
co-authored by Claude Opus 5.5
parent 1a481a1f4d
commit 2d308abd16
2 changed files with 41 additions and 6 deletions
+27 -6
View File
@@ -615,14 +615,34 @@ const children = [];
// K2); `forkLoop` forks every 5 ms (K12); `ignoreTerm` survives SIGTERM, so
// only the kill phase ends it (K3, K10, K11). With `pidLog`, the fork loop
// appends each child's pid and a `term` line when it gets SIGTERM (K12).
//
// It resolves only once the child writes its ready byte, which it does after
// installing its TERM handler. Node takes 15 to 80 ms to get there, and a
// force stop can send TERM sooner (12 ms when the claim store is on a fast
// disk). A TERM before the handler kills a child that is meant to ignore it
// (#1528).
function spawnChild({ setsid = false, forkLoop = false, ignoreTerm = false, pidLog = null } = {}) {
const note = pidLog ? `const note=(s)=>require('node:fs').appendFileSync(${JSON.stringify(pidLog)},s+'\\n');` : "const note=()=>{};";
const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + (forkLoop
const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + "process.stdout.write('r');" + (forkLoop
? "const {spawn}=require('node:child_process');setInterval(()=>{try{const c=spawn('sleep',['1000'],{stdio:'ignore'});if(c.pid)note(String(c.pid))}catch{}},5);setInterval(()=>{},1e9)"
: "setInterval(()=>{},1e9)");
const child = spawn(process.execPath, ["-e", code], { stdio: "ignore", detached: setsid });
const child = spawn(process.execPath, ["-e", code], { stdio: ["ignore", "pipe", "ignore"], detached: setsid });
children.push(child.pid);
return child.pid;
return new Promise((resolve, reject) => {
const fail = (why) => {
clearTimeout(timer);
reject(new Error(`tool child ${child.pid} ${why} before it was ready`));
};
const timer = setTimeout(() => fail("took 10 s"), 10000);
child.once("error", (err) => fail(err.code ?? err.message));
child.once("exit", (code, signal) => fail(`exited (${signal ?? code})`));
child.stdout.once("data", () => {
clearTimeout(timer);
child.removeAllListeners("exit");
child.stdout.destroy();
resolve(child.pid);
});
});
}
// K13: a member writes its own pid to another cgroup's cgroup.procs.
@@ -671,7 +691,7 @@ async function main() {
drop: () => fake.dropResponse(req.type, req.n ?? 1),
extension: () => void fake.extensionPrompt(req.args ?? {}),
state: () => ({ streaming: fake.streaming, runs: fake.runs.length, commands: fake.commands, pid: process.pid, children, appends: fake.appends }),
child: () => ({ pid: spawnChild(req.args ?? {}) }),
child: () => spawnChild(req.args ?? {}).then((pid) => ({ pid })),
escape: () => escape(req.target),
cgroup: () => readFileSync(`/proc/${req.pid ?? process.pid}/cgroup`, "utf8"),
waitPaused: () => fake.waitPaused(req.point),
@@ -679,12 +699,13 @@ async function main() {
stall: () => void process.stdin.pause(),
};
if (!ops[req.op]) return sock.write(encodeLine({ id: req.id, ok: false, error: `unknown op ${req.op}` }));
const failed = (err) => sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
try {
const out = ops[req.op]();
if (out && typeof out.then === "function") out.then(reply);
if (out && typeof out.then === "function") out.then(reply, failed);
else reply(out ?? null);
} catch (err) {
sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
failed(err);
}
});
sock.on("data", (c) => splitter.push(c));