Files
stack/packages/conversation/tests/cohort.test.mjs
T
jason.woltjeandClaude Opus 5.5 243e153c8b feat(conversation): CHAT-03 I1, mediated control of a sealed headless Pi (#1507)
Controller, claim store, live-session guard, engine link and seal,
turn tracker, cohort force stop and recovery, client library,
transcript and mediated terminal, with the fake engine and tests.
Fixtures only; no live cutover.

Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694)
approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files).
Suites on an export: conversation 152/152, control-board 124, webui 14,
seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green.
Follow-ups for I3 are in DEFERRED. Gate E stays with Jason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 15:47:53 -05:00

717 lines
33 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.
// CHAT-03 §6 stop, cohort proof and recovery (#1507): K1–K18. The scope
// fixtures run the fake engine as a real process under ScopeLauncher, so the
// shim, the `engine` cgroup and systemd's invocation ID are all real; they
// skip when systemd user scopes are unavailable. K2 runs on the process-group
// fallback. K6–K9 and K16–K18 use the in-process fake, whose force stop is a
// fixture stand-in (see FakeLauncher). Controllers that must die run in
// ctrl-child.mjs.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { appendFileSync, chmodSync, copyFileSync, mkdirSync, readFileSync, rmdirSync, writeFileSync } from "node:fs";
import { spawn, spawnSync } from "node:child_process";
import { dirname, join } from "node:path";
import { ClaimStore, FOREIGN_HOST } from "../src/claim.mjs";
import { ConversationClient } from "../src/client.mjs";
import { AUTHORITY, PgroupLauncher, ScopeLauncher, scopeAvailable, shimRequest, systemctlShow, systemdUnits } from "../src/cohort.mjs";
import { Controller, ELIGIBILITY } from "../src/controller.mjs";
import { ENGINE_PIN_MISMATCH } from "../src/pi-pin.mjs";
import { FixtureVerifier, newId } from "../src/records.mjs";
import { ControlClient, FakeLauncher } from "./fake-pi.mjs";
import { FAST, REPO, assistantEntry, claimRecords, cleanupAll, controllerFor, fixture, killChildren, noUnits, reap, receiptState, spawnController, started, tick } from "./harness.mjs";
const reaped = [];
const strays = new Set();
after(() => {
for (const pid of strays) {
try {
process.kill(pid, "SIGKILL");
} catch {
// gone
}
}
for (const fx of reaped) reap(fx);
killChildren();
cleanupAll();
});
const track = (fx) => (reaped.push(fx), fx);
const SCOPE = scopeAvailable();
const NEEDS_SCOPE = { skip: !SCOPE && "systemd user scopes unavailable", timeout: 60000 };
const FAKE_PI = join(import.meta.dirname, "fake-pi.mjs");
async function until(pred, ms = 4000, what = "condition") {
const end = Date.now() + ms;
while (!(await pred())) {
if (Date.now() > end) throw new Error(`timed out waiting for ${what}`);
await tick(10);
}
}
// A zombie has exited; only its parent hasn't reaped it yet.
function alive(pid) {
try {
const st = readFileSync(`/proc/${pid}/stat`, "utf8");
return st.slice(st.lastIndexOf(")") + 2)[0] !== "Z";
} catch {
return false;
}
}
class SpyVerifier extends FixtureVerifier {
constructor() {
super({ authorities: [AUTHORITY] });
this.posted = [];
}
post(p) {
this.posted.push(structuredClone(p));
return super.post(p);
}
}
// Holds the controller at named barriers (as in races.test.mjs).
function gate() {
const want = new Set(), held = new Map();
return {
barrier: async (name) => {
if (!want.has(name)) return;
want.delete(name);
await new Promise((r) => held.set(name, r));
},
hold: (name) => want.add(name),
waitHeld: (name, ms = 8000) => until(() => held.has(name), ms, `barrier ${name}`),
release: (name) => {
const r = held.get(name);
held.delete(name);
r?.();
},
};
}
// A controller in this process on a real engine process: the fake engine
// under ScopeLauncher ("scope") or PgroupLauncher ("pgroup").
async function live({ kind = "scope", barrier = null, verifier = new FixtureVerifier({ authorities: [AUTHORITY] }) } = {}) {
const fx = track(fixture());
const control = join(fx.base, "fake.sock");
const ctrl = new Controller({
fixtureRoot: fx.base, claimRoot: fx.claimRoot, socketDir: fx.socketDir, sessionFile: fx.sessionFile, seat: fx.seat,
launcher: kind === "scope" ? new ScopeLauncher() : new PgroupLauncher(),
engine: { command: process.execPath, preArgs: [FAKE_PI], env: { ...process.env, FAKE_PI_CONTROL: control, FAKE_PI_LOG: join(fx.base, "fake.log") }, cwd: fx.proj },
verifier, units: kind === "scope" ? systemdUnits : noUnits, timeouts: FAST, barrier,
});
await ctrl.start();
assert.equal(ctrl.binding.state, "active", JSON.stringify(ctrl.evidence.uncertain));
const c = new ConversationClient({ socketPath: ctrl.socketPath });
await c.connect();
assert.equal((await c.takeover()).outcome, "transferred");
const fake = new ControlClient(control);
await fake.connect();
const close = async () => {
c.close();
fake.close();
await ctrl.close({ killEngine: true });
reap(fx);
};
return { fx, ctrl, c, fake, rec: () => ctrl.claim.record, close };
}
const childOf = async (h, args) => {
const r = await h.fake.call("child", { args });
assert.ok(r.ok, JSON.stringify(r));
strays.add(r.result.pid);
return r.result.pid;
};
const memberPids = async (shim) => {
const m = await shimRequest(shim, "members");
assert.ok(m.ok, JSON.stringify(m));
return m.members.map((x) => x.pid);
};
// A confirmed force stop, waited to its end (`stopped` or `uncertain`).
async function forceStop(h, c = h.c, ms = 20000) {
const fs = await c.confirmed("force-stop");
assert.equal(fs.outcome, "force-stop-fenced", JSON.stringify(fs));
await until(() => ["stopped", "uncertain"].includes(h.ctrl.binding.state), ms, "the force stop to end");
return fs.stop.id;
}
// ---- scope fixtures --------------------------------------------------------
test("K1: force stop kills a tool child that called setsid; stopped with a verified proof", NEEDS_SCOPE, async () => {
const h = await live();
try {
// It also ignores TERM, so only the cgroup kill ends it.
const child = await childOf(h, { setsid: true, ignoreTerm: true });
assert.ok((await memberPids(h.rec().shim)).includes(child), "setsid leaves the process group, not the cgroup");
const stop = await forceStop(h);
assert.equal(h.ctrl.binding.state, "stopped", JSON.stringify(h.ctrl.evidence.stops));
const { proof } = h.ctrl.stoppedProof;
assert.equal(proof.stop, stop);
assert.equal(proof.membershipComplete, true);
assert.equal(proof.membershipEpoch, h.rec().invocationId);
assert.ok(proof.members.some((m) => m.pid === child), "the escaped child is a listed member");
assert.ok(h.ctrl.verifier.cohort(proof, h.ctrl.stoppedProof.effects, { binding: h.ctrl.binding, stop, now: new Date(), epoch: h.rec().invocationId }));
assert.equal(alive(child), false);
} finally {
await h.close();
}
});
test("K2: K1 on the process-group fallback ends uncertain, never stopped", async () => {
const h = await live({ kind: "pgroup" });
try {
const child = await childOf(h, { setsid: true });
await forceStop(h);
assert.equal(h.ctrl.binding.state, "uncertain");
assert.equal(h.ctrl.stoppedProof ?? null, null);
const last = h.ctrl.evidence.stops.at(-1);
assert.equal(last.outcome, "uncertain");
assert.match(last.reason, /process-group fallback/);
assert.ok(alive(child), "the setsid child left the group; a stopped claim here would have been false");
assert.equal((await h.c.prompt("after an uncertain stop")).refusal, "fenced");
} finally {
await h.close();
}
});
test("K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM", NEEDS_SCOPE, async () => {
const g = gate();
const h = await live({ barrier: g.barrier });
try {
const child = await childOf(h, { ignoreTerm: true });
g.hold("phase-kill");
const fs = await h.c.confirmed("force-stop");
assert.equal(fs.outcome, "force-stop-fenced");
await g.waitHeld("phase-kill");
assert.equal(h.ctrl.binding.state, "stopping");
assert.notEqual(h.ctrl.stops.get(fs.stop.id).state, "stopped");
assert.equal(h.rec().state, "stopping");
assert.equal(h.rec().proof ?? null, null);
assert.equal(h.rec().stop.phaseStarted, "kill");
assert.ok(alive(child), "the member ignored TERM");
assert.equal((await shimRequest(h.rec().shim, "events")).populated, 1);
g.release("phase-kill");
await until(() => h.ctrl.binding.state !== "stopping", 15000, "the kill phase");
assert.equal(h.ctrl.binding.state, "stopped");
assert.equal(alive(child), false);
assert.ok(h.ctrl.stoppedProof.proof.members.some((m) => m.pid === child));
} finally {
g.release("phase-kill");
await h.close();
}
});
test("K4: two engines; force stop one; the other survives by independent observation", NEEDS_SCOPE, async () => {
const a = await live();
const b = await live();
try {
assert.notEqual(a.rec().unitName, b.rec().unitName);
await forceStop(a);
assert.equal(a.ctrl.binding.state, "stopped");
const ev = await shimRequest(b.rec().shim, "events");
assert.equal(ev.populated, 1, "b's own engine cgroup is still populated");
const st = await b.fake.call("state");
assert.ok(st.ok);
assert.equal(st.result.pid, b.rec().engine.pid);
assert.equal(systemctlShow(b.rec().unitName).invocationId, b.rec().invocationId);
const p = await b.c.prompt("still here?");
assert.equal(p.outcome, "admitted", JSON.stringify(p));
await receiptState(b.c, p.receipt.id, "finished", 8000);
assert.equal(b.ctrl.binding.state, "active");
} finally {
await a.close();
await b.close();
}
});
test("K5: a stop during a tool call leaves the effect uncertain, and it is shown", NEEDS_SCOPE, async () => {
const h = await live();
try {
await h.fake.call("script", { steps: [{ tool: { id: "call-k5", name: "bash", args: { command: "touch x" }, hold: true } }, { text: "never", stop: "stop" }] });
await h.fake.call("arm", { point: "tool:call-k5" });
const p = await h.c.prompt("run a tool");
assert.equal(p.outcome, "admitted");
await h.fake.call("waitPaused", { point: "tool:call-k5" });
await until(() => [...(h.ctrl.exec?.tools.values() ?? [])].some((t) => t.start && !t.end), 4000, "the tool start");
const stop = await forceStop(h);
assert.equal(h.ctrl.binding.state, "stopped");
const s = h.ctrl.stops.get(stop);
assert.equal(s.externalEffects, "uncertain");
const inv = h.ctrl.stoppedProof.effects.invocations;
assert.equal(inv.length, 1);
assert.equal(inv[0].disposition, "uncertain", "killing is never a rollback");
await until(() => h.c.pushes.some((m) => m.kind === "stop" && m.stop.id === stop && m.stop.state === "stopped" && m.stop.externalEffects === "uncertain"), 4000, "the stop push");
} finally {
await h.close();
}
});
// The freeze is shown two ways that don't depend on timing: engine's
// cgroup.freeze still reads 1 after the stop (the shim holds the scope), and
// every child the loop forked after its SIGTERM, which nothing else ends
// before the kill, is in the proof's list. Mutants r2-B5 (no freeze write,
// `frozen 1` answered) and r2-B5b (freeze written, not waited on) fail here.
test("K12: a member forking in a loop: the freeze stops it, enumeration is complete, populated 0 after cgroup.kill", NEEDS_SCOPE, async () => {
const h = await live();
try {
const pidLog = join(h.fx.base, "fork-pids.log");
const forker = await childOf(h, { forkLoop: true, ignoreTerm: true, pidLog });
await until(async () => (await memberPids(h.rec().shim)).length >= 6, 4000, "the fork loop");
const engineDir = join("/sys/fs/cgroup", (await shimRequest(h.rec().shim, "hello")).scope, "engine");
await forceStop(h);
assert.equal(h.ctrl.binding.state, "stopped", JSON.stringify(h.ctrl.evidence.stops));
const { members } = h.ctrl.stoppedProof.proof;
assert.ok(members.some((m) => m.pid === forker));
assert.ok(members.length >= 2, `members ${members.length}`);
for (const m of members) assert.equal(alive(m.pid), false, `member ${m.pid}`);
assert.equal(readFileSync(join(engineDir, "cgroup.freeze"), "utf8").trim(), "1", "the freeze was written");
const lines = readFileSync(pidLog, "utf8").split("\n").filter(Boolean);
assert.ok(lines.includes("term"), "the fork loop got the TERM phase");
const listed = new Set(members.map((m) => m.pid));
const late = lines.slice(lines.indexOf("term") + 1).map(Number);
assert.ok(late.length > 0, "the loop forked after its TERM");
for (const pid of late) {
strays.add(pid);
assert.ok(listed.has(pid), `child ${pid} forked after TERM is in the proof's list`);
}
const ev = await shimRequest(h.rec().shim, "events");
assert.equal(ev.populated, 0);
assert.deepEqual(await memberPids(h.rec().shim), []);
} finally {
await h.close();
}
});
test("K13: a member writing its pid into another cgroup is refused by the namespace; the kill is complete", NEEDS_SCOPE, async () => {
const h = await live();
const engine = h.rec().engine.pid;
try {
const hello = await shimRequest(h.rec().shim, "hello");
const scope = join("/sys/fs/cgroup", hello.scope);
for (const target of [join(scope, "supervisor", "cgroup.procs"), join(dirname(scope), "cgroup.procs")]) {
const r = await h.fake.call("escape", { target });
assert.ok(r.ok, JSON.stringify(r));
assert.equal(r.result.escaped, false, `escape into ${target}`);
}
assert.ok((await memberPids(h.rec().shim)).includes(engine), "the engine is still in its cgroup");
await forceStop(h);
assert.equal(h.ctrl.binding.state, "stopped");
assert.equal(alive(engine), false);
} finally {
strays.add(engine);
await h.close();
}
});
test("K15: the shim gone, engine/cgroup.events unreadable, or the engine cgroup missing: evidence unavailable, not empty; uncertain", NEEDS_SCOPE, async () => {
{
const h = await live();
const engine = h.rec().engine.pid;
try {
const hello = await shimRequest(h.rec().shim, "hello");
process.kill(hello.shimPid, "SIGKILL");
await until(() => !alive(hello.shimPid), 4000, "the shim to die");
await forceStop(h);
assert.equal(h.ctrl.binding.state, "uncertain");
assert.match(h.ctrl.evidence.stops.at(-1).reason, /shim/);
assert.ok(alive(engine), "no signal reached the engine without the shim's evidence");
} finally {
await h.close();
}
}
{
const h = await live();
try {
const hello = await shimRequest(h.rec().shim, "hello");
chmodSync(join("/sys/fs/cgroup", hello.scope, "engine", "cgroup.events"), 0o000);
assert.equal((await shimRequest(h.rec().shim, "events")).ok, false);
await forceStop(h);
assert.equal(h.ctrl.binding.state, "uncertain");
assert.match(h.ctrl.evidence.stops.at(-1).reason, /cgroup\.events unreadable/);
assert.equal(h.ctrl.stoppedProof ?? null, null);
} finally {
await h.close();
}
}
// The `engine` cgroup path missing: its processes moved to a sibling and
// the directory removed. Absent, never empty. Mutant r2-B6 (ENOENT read as
// `populated 0`) fails here.
{
const h = await live();
const engine = h.rec().engine.pid;
try {
const scope = join("/sys/fs/cgroup", (await shimRequest(h.rec().shim, "hello")).scope);
const aside = join(scope, "aside");
mkdirSync(aside);
for (const pid of readFileSync(join(scope, "engine", "cgroup.procs"), "utf8").split("\n").filter(Boolean).map(Number)) {
strays.add(pid);
writeFileSync(join(aside, "cgroup.procs"), String(pid));
}
rmdirSync(join(scope, "engine"));
for (const op of ["events", "members"]) {
const r = await shimRequest(h.rec().shim, op);
assert.equal(r.ok, false, `${op}: ${JSON.stringify(r)}`);
assert.match(r.unavailable, /ENOENT/);
}
await forceStop(h);
assert.equal(h.ctrl.binding.state, "uncertain");
assert.match(h.ctrl.evidence.stops.at(-1).reason, /ENOENT/);
assert.equal(h.ctrl.stoppedProof ?? null, null);
assert.ok(alive(engine), "no signal reached the engine without the cgroup's evidence");
} finally {
await h.close();
}
}
});
// ---- controller death during a force stop ----------------------------------
function childFixture() {
const fx = track(fixture());
return { fx, fakeEnv: { FAKE_PI_CONTROL: join(fx.base, "fake.sock"), FAKE_PI_LOG: join(fx.base, "fake.log") } };
}
async function connectTo(socketPath, client = null) {
const c = client ?? new ConversationClient({ socketPath });
c.socketPath = socketPath;
await c.connect();
return c;
}
const stopping = (fx) => claimRecords(fx).map((r) => r.record).filter((r) => r?.state === "stopping" && r.stop);
// The controller dies at `barrier` during a confirmed force stop; a tool
// child that ignores TERM keeps the cohort populated past the TERM phase.
async function crashDuringStop(barrier) {
const { fx, fakeEnv } = childFixture();
const a = spawnController({ fx, launcher: "scope", fakeEnv, dieAt: { [barrier]: 1 } });
const ra = await a.next((m) => m.ready || m.error);
assert.ok(ra.ready, JSON.stringify(ra));
const c = await connectTo(ra.socketPath);
assert.equal((await c.takeover()).outcome, "transferred");
const fake = new ControlClient(fakeEnv.FAKE_PI_CONTROL);
await fake.connect();
const child = (await fake.call("child", { args: { ignoreTerm: true } })).result.pid;
strays.add(child);
fake.close();
void c.confirmed("force-stop");
await a.next((m) => m.dying === barrier, 15000);
await a.exited;
const recs = stopping(fx);
assert.ok(recs.length > 0, "the stop was recorded before any signal");
const last = recs.at(-1);
return { fx, fakeEnv, c, child, stopId: last.stop.id, last, engine: last.engine.pid };
}
async function restartAndResume({ fx, fakeEnv, c, stopId }) {
const b = spawnController({ fx, launcher: "scope", fakeEnv });
const rb = await b.next((m) => m.ready || m.error, 15000);
assert.ok(rb.ready, JSON.stringify(rb));
assert.equal(rb.started.launched, false);
assert.equal(rb.started.classified.state, "stopping");
await connectTo(rb.socketPath, c);
assert.ok(await c.waitFor(() => ["stopped", "uncertain"].includes(c.binding?.state), 20000), `binding ${c.binding?.state}`);
b.send("evidence");
const ev = (await b.next((m) => m.evidence !== undefined)).evidence;
b.send("proof");
const proof = (await b.next((m) => m.proof !== undefined)).proof;
const recs = claimRecords(fx).map((r) => r.record).filter((r) => r?.stop);
assert.ok(recs.every((r) => r.stop.id === stopId), "the restart continues the recorded stop; no new stop");
return { b, ev, proof };
}
for (const [id, barrier, title] of [
["K10", "phase-kill", "controller killed between the TERM and kill phases"],
["K11", "force-stop-recorded", "controller killed after the confirmation is recorded, before TERM"],
]) {
test(`${id}: ${title}: restart checks the invocation ID and re-runs from TERM for the same stop`, NEEDS_SCOPE, async () => {
const crashed = await crashDuringStop(barrier);
const { fx, c, child, stopId, last, engine } = crashed;
assert.equal(last.stop.phaseStarted, barrier === "phase-kill" ? "kill" : null);
assert.ok(!claimRecords(fx).some((r) => r.record?.state === "stopped"), "nothing recorded as stopped before the restart");
assert.ok(alive(child), "the member is alive across the crash");
if (barrier === "force-stop-recorded") assert.ok(alive(engine), "no TERM was sent before the crash");
const { b, ev, proof } = await restartAndResume(crashed);
try {
assert.equal(c.binding.state, "stopped", JSON.stringify(ev.stops));
const done = ev.stops.at(-1);
assert.equal(done.stop, stopId);
assert.equal(done.resumed, true);
assert.equal(done.outcome, "stopped");
assert.equal(proof.stop, stopId);
assert.ok(proof.members.some((m) => m.pid === child), "the member observed at the freeze is listed");
if (barrier === "phase-kill") assert.ok(!proof.members.some((m) => m.pid === engine), "the engine ended at TERM before the crash; it isn't listed as killed");
assert.equal(alive(child), false);
} finally {
c.close();
b.send("close");
await b.exited;
reap(fx);
}
});
}
test("K14: a unit with the recorded name but another invocation ID: evidence unavailable, no signals, uncertain", NEEDS_SCOPE, async () => {
const { fx, fakeEnv } = childFixture();
const a = spawnController({ fx, launcher: "scope", fakeEnv });
const ra = await a.next((m) => m.ready || m.error);
assert.ok(ra.ready, JSON.stringify(ra));
const rec = claimRecords(fx).map((r) => r.record).filter((r) => r?.invocationId).at(-1);
const unit = rec.unitName;
a.proc.kill("SIGKILL");
await a.exited;
// The shim ignores TERM by design, so the scope goes with SIGKILL.
spawnSync("systemctl", ["--user", "kill", "--signal=SIGKILL", `${unit}.scope`], { stdio: "ignore", timeout: 10000 });
await until(() => systemctlShow(unit)?.loadState === "not-found", 10000, "the original scope to go");
let impostor = null;
await until(() => {
if (impostor && systemctlShow(unit)?.activeState === "active") return true;
if (!impostor || impostor.exitCode !== null) {
impostor = spawn("systemd-run", ["--user", "--scope", `--unit=${unit}`, "--quiet", "--", "sleep", "300"], { stdio: "ignore", detached: true });
impostor.unref();
}
return false;
}, 10000, "the impostor unit");
strays.add(impostor.pid);
const theirs = systemctlShow(unit).invocationId;
assert.notEqual(theirs, rec.invocationId);
const b = spawnController({ fx, launcher: "scope", fakeEnv });
try {
const rb = await b.next((m) => m.ready || m.error, 15000);
assert.ok(rb.ready, JSON.stringify(rb));
assert.equal(rb.started.classified.state, "uncertain");
const c = await connectTo(rb.socketPath);
assert.equal((await c.confirmed("acquire-recovery-control")).outcome, "recovery-control-acquired");
const fs = await c.confirmed("force-stop");
assert.equal(fs.outcome, "force-stop-fenced", JSON.stringify(fs));
assert.ok(await c.waitFor(() => c.binding?.state === "uncertain" && c.pushes.some((m) => m.kind === "stop" && m.stop.id === fs.stop.id && m.stop.state === "uncertain"), 15000));
b.send("evidence");
const ev = (await b.next((m) => m.evidence !== undefined)).evidence;
assert.match(ev.stops.at(-1).reason, /invocation ID mismatch/);
assert.ok(alive(impostor.pid), "the other cohort got no signal");
assert.equal(systemctlShow(unit).invocationId, theirs);
c.close();
} finally {
b.send("close");
await b.exited;
spawnSync("systemctl", ["--user", "stop", `${unit}.scope`], { stdio: "ignore", timeout: 10000 });
reap(fx);
}
});
// ---- in-process fake: recovery, launch and the K9 fence ---------------------
async function provenStop(h, c = h.client) {
const fs = await c.confirmed("force-stop");
assert.equal(fs.outcome, "force-stop-fenced", JSON.stringify(fs));
await until(() => h.ctrl.binding.state === "stopped", 4000, "the proven stop");
return fs.stop.id;
}
function pinRootCopy(fx) {
const root = join(fx.base, "pins");
mkdirSync(join(root, "node_modules"), { recursive: true });
copyFileSync(join(REPO, "package-lock.json"), join(root, "package-lock.json"));
copyFileSync(join(REPO, "node_modules", ".package-lock.json"), join(root, "node_modules", ".package-lock.json"));
return root;
}
test("K6: recover without proof, without confirmation, or with changed pins is refused", async () => {
{
const h = await started();
try {
const notYet = await h.client.confirmed("recover", { stop: newId("stop") });
assert.equal(notYet.refusal, "stop-proof", "no stop at all");
} finally {
await h.close();
}
}
{
const h = await started({ launcher: new FakeLauncher({ stopOutcome: "unavailable" }) });
try {
const fs = await h.client.confirmed("force-stop");
await until(() => h.ctrl.binding.state === "uncertain", 4000, "the uncertain stop");
assert.equal((await h.client.confirmed("recover", { stop: fs.stop.id })).refusal, "stop-proof", "an uncertain stop is no proof");
} finally {
await h.close();
}
}
{
const fx = track(fixture());
const pinRoot = pinRootCopy(fx);
const h = await started({ fx, pinRoot });
try {
const stop = await provenStop(h);
const missing = await h.client.request("recover", { stop });
assert.equal(missing.refusal, "malformed", "no confirmation field");
const unknown = await h.client.request("recover", { stop, confirmation: newId("confirmation") });
assert.equal(unknown.refusal, "confirmation");
const issued = await h.client.request("issue-confirmation", { operationToConfirm: "force-stop" });
const id = issued.data.confirmation.id;
await h.client.request("answer-confirmation", { confirmation: id, answer: "confirm" });
assert.equal((await h.client.request("recover", { stop, confirmation: id })).refusal, "confirmation", "a confirmation for another operation");
const lock = join(pinRoot, "package-lock.json");
const original = readFileSync(lock, "utf8");
const changed = JSON.parse(original);
changed.packages["node_modules/@earendil-works/pi-coding-agent"].version = "0.0.0";
writeFileSync(lock, JSON.stringify(changed));
assert.equal((await h.client.confirmed("recover", { stop })).refusal, ENGINE_PIN_MISMATCH);
writeFileSync(lock, original);
const again = await h.client.request("issue-confirmation", { operationToConfirm: "recover" });
const once = again.data.confirmation.id;
await h.client.request("answer-confirmation", { confirmation: once, answer: "confirm" });
assert.equal((await h.client.request("recover", { stop, confirmation: once })).outcome, "recovery-eligible", "the same request with the pins restored");
assert.equal(h.ctrl.confirmations.get(once).state, "consumed");
assert.equal((await h.client.request("recover", { stop, confirmation: once })).refusal, "confirmation", "a confirmation is single-use");
} finally {
await h.close();
}
}
});
test("K7: recover after proof, then launch: new claim and execution, generation +1, same leaf; the cancelled prompt is not replayed", async () => {
const h = await started();
try {
h.engine.script([{ pause: "p1" }, { text: "never", stop: "stop" }]);
h.engine.arm("p1");
const p = await h.client.prompt("cancelled by the stop");
assert.equal(p.outcome, "admitted");
await receiptState(h.client, p.receipt.id, "working");
await h.engine.waitPaused("p1");
const before = { claim: h.ctrl.claim.claimId, execution: h.ctrl.binding.execution, generation: h.ctrl.binding.controllerGeneration };
const stop = await provenStop(h);
const leafAtProof = h.ctrl.claim.record.leafAtProof;
const rec = await h.client.confirmed("recover", { stop });
assert.equal(rec.outcome, "recovery-eligible", JSON.stringify(rec));
assert.equal(rec.data.generation, before.generation + 1);
const engines = h.launcher.engines.length;
const r = await h.ctrl.launch(rec.data.eligibility);
assert.equal(h.launcher.engines.length, engines + 1);
assert.notEqual(r.claim, before.claim);
assert.equal(r.claim, rec.data.claim);
assert.notEqual(h.ctrl.binding.execution, before.execution, "a new execution (K7's incarnation)");
assert.equal(h.ctrl.binding.controllerGeneration, before.generation + 1);
assert.equal(h.ctrl.binding.state, "active");
assert.equal(h.ctrl.claim.record.leaf, leafAtProof);
assert.equal(h.ctrl.claim.record.prior.stop, stop);
const fresh = h.launcher.last;
assert.equal(fresh.leaf, leafAtProof, "the new engine loaded the leaf at proof");
assert.ok(!fresh.commands.some((x) => x.type === "prompt"));
assert.ok(!fresh.bytes().toString().includes("cancelled by the stop"));
} finally {
await h.close();
}
});
test("K8: an engine that loads another leaf on resume is refused before admission; it stays claimed until a proven stop", async () => {
const h = await started();
try {
const stop = await provenStop(h);
const rec = await h.client.confirmed("recover", { stop });
assert.equal(rec.outcome, "recovery-eligible");
h.launcher.opts.leaf = "ffff0000";
await h.ctrl.launch(rec.data.eligibility);
assert.equal(h.ctrl.binding.state, "uncertain");
assert.ok(await h.client.waitFor(() => h.client.binding?.id === h.ctrl.binding.id && h.client.binding.state === "uncertain"), "the client sees the new binding");
assert.equal(h.ctrl.binding.admission, "closed");
assert.ok(h.ctrl.evidence.uncertain.some((u) => u.reason === "loaded-session" && /another leaf/.test(u.detail)));
assert.equal(h.ctrl.claim.claimId, rec.data.claim);
assert.notEqual(h.ctrl.claim.record.state, "active", "never promoted");
assert.ok(h.ctrl.claim.record.engine?.pid, "the engine stays recorded under the claim");
const stops = h.launcher.stops;
assert.equal(h.launcher.last.ended ?? false, false, "nothing killed it outside a force stop");
assert.equal((await h.client.confirmed("acquire-recovery-control")).outcome, "recovery-control-acquired");
assert.equal((await h.client.prompt("admitted?")).refusal, "preflight", "the loaded-session check never passed");
await provenStop(h);
assert.equal(h.launcher.stops, stops + 1);
assert.equal(h.ctrl.claim.record.state, "stopped");
} finally {
await h.close();
}
});
test("K9: an interrupt that never settles stays uncertain; force stop stays available; takeover is refused while fenced", async () => {
const h = await started({ clients: 2 });
try {
const [c1, c2] = h.clients;
h.engine.script([{ hang: true }]);
const p = await c1.prompt("hangs");
assert.equal(p.outcome, "admitted");
await receiptState(c1, p.receipt.id, "working");
const r = await c1.interrupt();
const stop = r.stop?.id ?? h.ctrl.binding.stop;
await until(() => h.ctrl.stops.get(stop)?.state === "uncertain", 10000, "the interrupt to end uncertain");
assert.equal(h.ctrl.binding.admission, "closed");
assert.ok(!h.ctrl.events.some((e) => e.type === "reconciled"));
assert.equal((await c1.prompt("again")).refusal, "fenced");
await until(() => c2.binding?.controllerGeneration === h.ctrl.binding.controllerGeneration, 2000, "c2 synced");
assert.equal((await c2.takeover()).refusal, "fenced");
await provenStop(h, c1);
assert.equal(h.ctrl.binding.state, "stopped");
} finally {
await h.close();
}
});
test("K16: a claim from another machine ID refuses foreign-host; no boot proof is issued", async () => {
const fx = track(fixture());
const verifier = new SpyVerifier();
const { ctrl } = controllerFor(fx, { verifier });
const foreign = new ClaimStore({ root: fx.claimRoot, host: { machineId: () => "f".repeat(32), bootId: () => "00000000-0000-4000-8000-000000000000" } });
await foreign.acquire(ctrl.seatK, ctrl.sessionK, {
bindingId: "binding-1", harness: "pi", conversation: ctrl.conversation, branch: "main", leaf: "b2c3d4e5",
pins: { engineVersion: "0.85.1", enginePin: "x", argvDigest: "y" },
owner: { pid: 1, start: "1", boot: "00000000-0000-4000-8000-000000000000", incarnation: "f".repeat(32) }, generation: 1,
});
await assert.rejects(ctrl.start(), { code: FOREIGN_HOST });
assert.deepEqual(verifier.posted, [], "no proof of any kind was posted");
});
test("K17: two launcher calls with one eligibility record: one launch, the other refuses, no second engine", async () => {
const h = await started();
try {
const stop = await provenStop(h);
const rec = await h.client.confirmed("recover", { stop });
const engines = h.launcher.engines.length;
const [x, y] = await Promise.allSettled([h.ctrl.launch(rec.data.eligibility), h.ctrl.launch(rec.data.eligibility)]);
const ok = [x, y].filter((v) => v.status === "fulfilled");
const no = [x, y].filter((v) => v.status === "rejected");
assert.equal(ok.length, 1);
assert.equal(no.length, 1);
assert.equal(no[0].reason.code, ELIGIBILITY);
assert.equal(h.launcher.engines.length, engines + 1);
await assert.rejects(h.ctrl.launch(rec.data.eligibility), { code: ELIGIBILITY });
assert.equal(h.launcher.engines.length, engines + 1);
} finally {
await h.close();
}
});
test("K18: the leaf changes after eligibility: launch refused; the reservation stays until released with proof", async () => {
const h = await started();
try {
const stop = await provenStop(h);
const rec = await h.client.confirmed("recover", { stop });
const leaf = h.ctrl.claim.record.leafAtProof;
appendFileSync(h.fx.sessionFile, JSON.stringify(assistantEntry("c3d4e5f6", leaf, "written after eligibility", 9)) + "\n");
const engines = h.launcher.engines.length;
await assert.rejects(h.ctrl.launch(rec.data.eligibility), { code: "target" });
assert.equal(h.launcher.engines.length, engines, "no engine started");
for (const key of [h.ctrl.seatK, h.ctrl.sessionK]) {
const head = h.ctrl.store.head(key);
assert.equal(head.record.claimId, rec.data.claim);
assert.equal(head.record.state, "reserved");
assert.equal(head.record.spawnMarker, false);
}
assert.equal((await h.ctrl.release(rec.data.eligibility)).released, rec.data.claim);
for (const key of [h.ctrl.seatK, h.ctrl.sessionK]) {
const head = h.ctrl.store.head(key);
assert.equal(head.record.claimId, rec.data.claim);
assert.equal(head.record.state, "stopped");
assert.equal(head.record.proof.kind, "no-unit");
}
await assert.rejects(h.ctrl.launch(rec.data.eligibility), { code: ELIGIBILITY });
} finally {
await h.close();
}
});