R1 and R3 block: a late broker reply shifts later replies on the host IPC channel, and a dangling workspace symlink passes the gate. R2 (close waits on an open launch.sock client) should be fixed in the same round. Probes, mutants and suite outputs alongside. Comment 26995 on #1523, queue rev 247. Co-Authored-By: Claude Opus 5.5 <[email protected]>
44 lines
2.9 KiB
JavaScript
44 lines
2.9 KiB
JavaScript
// Does launcher.close() SIGKILL a session whose runner never answers its
|
|
// SIGTERM? Launch the PM, SIGSTOP its runner from outside the namespace (a
|
|
// stopped process handles no signal but SIGKILL), close the host, and time
|
|
// it. Imports from the review worktree by absolute path.
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
const WT = process.env.WT;
|
|
const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`);
|
|
const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`);
|
|
const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`);
|
|
const { readSessions, launchLogFile } = await import(`${WT}/packages/seat/src/session.mjs`);
|
|
const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`);
|
|
const root = mkdtempSync(join(tmpdir(), "r41-kill-"));
|
|
const f = fixture(root, "acme", (doc) => {
|
|
doc.roles.coder.vars.model = "claude-sonnet-5-5";
|
|
doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } };
|
|
return doc;
|
|
});
|
|
mkdirSync(f.dataRoot, { mode: 0o700 });
|
|
const adapter = join(root, "adapter.sh");
|
|
writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`);
|
|
const system = loadSystem({ env: f.env });
|
|
const logs = [];
|
|
const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => logs.push(`host: ${l}`) });
|
|
const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => logs.push(`launcher: ${l}`) });
|
|
await launcher.listen();
|
|
const pm = await launcher.launchPm();
|
|
const [s] = readSessions(f.dataRoot);
|
|
const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log");
|
|
for (let i = 0; i < 300 && !readFileSync(runnerLog, "utf8").includes("claimed by run"); i++) await new Promise((r) => setTimeout(r, 50));
|
|
console.log(`pm run ${pm.run}: session pid ${s.pid}, runner pid ${s.runnerPid}, claimed`);
|
|
process.kill(s.runnerPid, "SIGSTOP");
|
|
console.log("runner SIGSTOPped");
|
|
const t0 = Date.now();
|
|
const code = await host.close(0);
|
|
console.log(`host.close -> ${code} after ${Date.now() - t0} ms`);
|
|
console.log(`session pid alive: ${startTimeOf(s.pid) === s.startTime}; runner pid alive: ${startTimeOf(s.runnerPid) === s.runnerStartTime}`);
|
|
const ends = readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l)).filter((e) => e.event === "end");
|
|
console.log("launch-log end lines:", JSON.stringify(ends.map((e) => ({ run: e.run, reason: e.reason, exitCode: e.exitCode, signal: e.signal }))));
|
|
console.log("sessions.json after:", JSON.stringify(readSessions(f.dataRoot)));
|
|
console.log(logs.filter((l) => /ended|run /.test(l)).join("\n"));
|
|
rmSync(root, { recursive: true, force: true });
|