Files
stack/packages/conversation/tests/ctrl-child.mjs
T
jason.woltjeandClaude Opus 5.5 08b428ecf1 feat(webui,conversation): S5 WebUI views and CHAT-03 follow-ups (row 40, #1522)
Dewey's round 3 candidate, manifest
agents/dewey/work/queue-40/candidate-manifest-r3.sha256 (d0aa0ded,
27 files, checked OK in the canonical tree).

- WebUI inbox, tasks, agents and trail views, read-only over /api/bus.
  The README says the bus proof ends at the Console process.
- CHAT-03 seal: the engine command is fixed, the engine environment is
  explicit, SEAL_FLAGS has --no-approve, escalating is cleared on throw.
- Terminal input typed after Ctrl-T or Ctrl-O is held. Only the run whose
  own parse set held drains it (T1), and #run catches errors per action.
- DEFERRED keeps N2 and moves F2 to done, citing T1.

Reviews: Filbert approve (comment 27011, rev 260), Darkwing approve
(27013, rev 264). Landing gate on 8cad7722 plus the candidate: webui 22,
conversation 161, control-board 124, every scripts/test-*.sh green,
test-task 98/0. Mutant Mr survives; its flows test is the first
follow-up row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 22:05:43 -05:00

89 lines
4.1 KiB
JavaScript
Raw 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.
#!/usr/bin/env node
// A controller in its own process, for the crash fixtures (#1507: W5, W6,
// W12–W15, W20, H18, H21–H23, K-series). A test can't kill the controller it
// runs in, and the claim's owner check sees the test's own pid as live, so
// these fixtures run the controller here.
//
// argv[2] is JSON: { fx, launcher: "pgroup" | "scope", dieAt, holdAt,
// timeouts, host, fakeEnv, verifier }. Lines on stdout are JSON:
// { ready, ... } after start, { held: name } at a held barrier,
// { dying: name } just before a SIGKILL at `dieAt`. Lines on stdin:
// "go" releases a held barrier; "close" closes and exits; "kill-engine"
// closes with the engine killed; "launch <eligibility>" launches after a
// recover; "evidence" and "proof" print the controller's evidence and its
// verified cohort proof.
import { createInterface } from "node:readline";
import { join } from "node:path";
import { Controller, TEST_ENGINE } from "../src/controller.mjs";
import { AUTHORITY, PgroupLauncher, ScopeLauncher, systemdUnits } from "../src/cohort.mjs";
import { FixtureVerifier } from "../src/records.mjs";
import { defaultHost } from "../src/claim.mjs";
const cfg = JSON.parse(process.argv[2]);
const out = (v) => process.stdout.write(JSON.stringify(v) + "\n");
const dieAt = new Map(Object.entries(cfg.dieAt ?? {}));
const holdAt = new Set(cfg.holdAt ?? []);
let release = null;
// The test's stdin pipe is line-oriented control, not engine input.
const rl = createInterface({ input: process.stdin });
let ctrl = null;
rl.on("line", async (line) => {
if (line === "go" && release) {
const r = release;
release = null;
r();
} else if (line === "close" || line === "kill-engine") {
await ctrl?.close({ killEngine: line === "kill-engine" });
out({ closed: true });
process.exit(0);
} else if (line === "evidence") out({ evidence: ctrl?.evidence ?? null, binding: ctrl?.binding ?? null });
else if (line === "proof") out({ proof: ctrl?.stoppedProof?.proof ?? null });
else if (line.startsWith("launch ")) {
// The launcher's call after `recover` (Q15), never a client command.
try {
const r = await ctrl.launch(line.slice(7));
out({ launched: true, binding: r.binding, incarnation: ctrl.incarnation });
} catch (err) {
out({ launched: false, error: String(err.code ?? "error"), message: String(err.message) });
}
}
});
// `dieAt` maps a barrier name to the occurrence to die at (1 = first).
const seen = new Map();
async function barrier(name, detail) {
const n = (seen.get(name) ?? 0) + 1;
seen.set(name, n);
if (cfg.trace) out({ barrier: name, n });
if (dieAt.get(name) === n) {
out({ dying: name, n, detail });
process.kill(process.pid, "SIGKILL");
await new Promise(() => {});
}
if (holdAt.has(`${name}#${n}`) || (n === 1 && holdAt.has(name))) {
out({ held: name, n, detail });
await new Promise((r) => (release = r));
}
}
const host = cfg.host ? { machineId: () => cfg.host.machineId ?? defaultHost.machineId(), bootId: () => cfg.host.bootId ?? defaultHost.bootId() } : undefined;
const fx = cfg.fx;
const fakePi = join(import.meta.dirname, "fake-pi.mjs");
try {
ctrl = new Controller({
fixtureRoot: fx.base, claimRoot: fx.claimRoot, socketDir: cfg.socketDir ?? fx.socketDir, sessionFile: fx.sessionFile, seat: fx.seat,
launcher: cfg.launcher === "scope" ? new ScopeLauncher() : new PgroupLauncher(),
engine: { cwd: fx.proj }, [TEST_ENGINE]: { command: process.execPath, preArgs: [fakePi], env: { ...process.env, ...(cfg.fakeEnv ?? {}) } },
verifier: cfg.verifier === false ? null : new FixtureVerifier({ authorities: [AUTHORITY] }),
units: cfg.units === "absent" ? { lookup: async () => ({ state: "absent" }) } : systemdUnits,
host, barrier, timeouts: cfg.timeouts,
});
const started = await ctrl.start();
out({ ready: true, started, socketPath: ctrl.socketPath, incarnation: ctrl.incarnation, pid: process.pid, binding: ctrl.binding, claim: ctrl.claim ? { claimId: ctrl.claim.claimId, record: ctrl.claim.record } : null });
} catch (err) {
out({ error: String(err.code ?? "error"), message: String(err.message) });
process.exit(3);
}