From 2f8cee35e15522f42aacc9783194c78e771944c8 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 16:04:30 -0500 Subject: [PATCH] row 271 (264d, #1776): r3, T6 per sage-3001. New cli test: a bus unavailable (broker-exited) at the exit proof, then at retirement, holds the clear with no retirement, successor launch or slot handed on, logs once, and resumes when the bus answers. BUILD.md maps the halt case to session-launch.test.mjs:372 and launcher.mjs:824, binds the Q198 hold to row 257 with duckworth-0210's carry, and names M14 (drop clearPass's try/catch) and its kill. No src change. Not run (no CLEAR). --- agents/gizmoduck/work/queue-271/BUILD.md | 35 ++++++++++++ packages/cli/tests/session-clear.test.mjs | 67 ++++++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/agents/gizmoduck/work/queue-271/BUILD.md b/agents/gizmoduck/work/queue-271/BUILD.md index fde74be41..d6d26ff80 100644 --- a/agents/gizmoduck/work/queue-271/BUILD.md +++ b/agents/gizmoduck/work/queue-271/BUILD.md @@ -729,3 +729,38 @@ covered: clear under a kill or restart (cli D7 Q2 survivor, D7 uncertain), grant limits (cli D8 capacity-full held) and a halt at retirement (bus D7 N4 halt). These are not: a Q198 hold, a halt between reservation, spawn and bind, and a manager configured but unavailable. That goes to Sage before the in-review move. + +## Round 3: T6 (sage-3001, duckworth-0209, duckworth-0210) + +Sage ruled the three T6 items left open above. Written with no runs; the r3 gate runs on a CLEAR. + +- Halt between reservation, spawn and bind: covered by citation. session-launch.test.mjs:372 (the + halt fence, C2) halts once after check() and once inside its recheck, and neither attempt spawns. + A successor goes through the same path: clearStart at launcher.mjs:824 calls `check()` and + `start()` unchanged, so the fence that test pins is the successor's fence too. +- Q198 hold: binds row 257 (Morgana). Row 271 has no Q198 code to test: 257 hasn't landed, and + packages/*/src holds no pressure or Q198 code on this branch or on origin/refactor f2735bb6a. + Carry for 257 (duckworth-0210): 257 either puts its hold code in CLEAR_HELD (launcher.mjs:815) or + rules that a Q198 hold counts as a refusal, and either way it adds a clear-path test. +- Manager configured but unavailable: a new cli test, "T4 T6: a bus unavailable at the exit proof + and then at retirement" (packages/cli/tests/session-clear.test.mjs). The test's host op rejects + with `broker-exited`, the code host.mjs:303 gives when the broker exits before it replies. It is + down first at `sessionClearExited`, then only at `sessionClearRetire`. While it is down the test + expects no retired or failed event, no launch but D's, nothing in the session registry (the + successor takes no slot) and D still the thread's only and default session. The stage holds at + drained, then at exited. The failure is logged once in all, and no `thread tick:` line appears. + Once the bus answers, the clear retires and the successor starts, in the same event order as the + running-session test. + +Reading of "capacity release": the launcher counts capacity from its registry of running sessions +(`limitHit`, launcher.mjs:403). D's run really ends, so its slot is free on the host either way; +what an unproven stop must not do is hand that slot to the successor. The test pins that. + +| # | Where | Mutant | Killed by | +| --- | --- | --- | --- | +| M14 | launcher.mjs:755-759 | drop clearPass's per-clear try/catch, so a failed op leaves the pass and tick() | cli T4 T6 bus unavailable: the first `lines()` assertion. The clear's line is never logged; the ticker's catch logs `thread tick:` instead | + +r3 gate, on a CLEAR (sage-3001): the touched files run whole, the cli suite runs whole, and M14. +No src file changed in r3, so the full gate isn't due again. A first-run red in the new test is +placed under mos-1272 and reported after; a fix that needs a product change goes to Sage and Mos +before any edit. diff --git a/packages/cli/tests/session-clear.test.mjs b/packages/cli/tests/session-clear.test.mjs index c883f43fd..92a55b5b5 100644 --- a/packages/cli/tests/session-clear.test.mjs +++ b/packages/cli/tests/session-clear.test.mjs @@ -2,7 +2,8 @@ // session stops its drained run once, retires it on the run's own end, and starts the successor // through the same check()/start() as any session; a held start waits for capacity; an earlier // host's live run of the cleared session holds the clear until it exits; and a session runs in its -// own pid namespace (N6). Real runners in the bwrap sandbox, with clear-adapter.mjs as the turn. +// own pid namespace (N6); a bus that can't answer holds a clear where it is (T6). Real runners in +// the bwrap sandbox, with clear-adapter.mjs as the turn. // The bus half is packages/bus/tests/session-clear.test.mjs. // // The host is session-launch.test.mjs's, with the five clear ops the broker child forwards. @@ -330,3 +331,67 @@ test("REQ-THREAD-2 T4 D7: a drained run that outlives the stop timeout makes the assert.equal(events(f, "session-clear-stop").length, 1); assert.equal(events(f, "session-clear-uncertain").length, 1); }); + +// T6 (sage-3001): the bus that proves the stop is configured but unavailable. Its host's op +// rejects as host.mjs does when the broker exits before it replies (broker-exited), first at the +// exit proof, then at retirement. While it is down nothing retires, no successor launches and the +// cleared session's slot isn't handed on; each failure is logged once, and the clear resumes where +// it stopped once the bus answers again. +test("REQ-THREAD-2 T4 T6: a bus unavailable at the exit proof and then at retirement holds the clear with no retirement, successor or launch, and it resumes when the bus is back", { skip, timeout: 90000 }, async (t) => { + const prepared = prepare(t); + const { f } = prepared; + const host = await start(t, prepared); + const down = new Set(); + const op = host.host.op; + host.host.op = (m) => (down.has(m.op) ? Promise.reject(Object.assign(new Error("broker exited before it replied"), { code: "broker-exited" })) : op(m)); + const { A, D } = thread(host.call); + await send(host.call, A, D, "before the clear").reply(); + const stage = () => host.runtime.sessionClears("acme").map((c) => c.stage); + const lines = () => host.logs.filter((l) => l.startsWith("clear clear-1") || l.startsWith("thread tick:")); + const line = `clear clear-1 (session ${D}): broker-exited`; + const held = () => { + assert.deepEqual(events(f, "session-clear-retired"), []); + assert.deepEqual(events(f, "session-clear-failed"), []); + assert.deepEqual(events(f, "launch").map((e) => e.session), [D]); + assert.deepEqual(readSessions(f.dataRoot), [], "nothing runs in the cleared session's slot"); + assert.deepEqual(host.call("session.list", { thread: A }).map((r) => [r.id, r.default]), [[D, true]]); + }; + + down.add("sessionClearExited"); + const S = host.call("session.clear", { thread: A, session: D, revision: 0, confirm: true, op: "clear-1" }).successor; + // The drained run is stopped and ends; the exit proof is what fails. + await settle(() => events(f, "end").length === 1 && host.logs.includes(line)); + await new Promise((r) => setTimeout(r, 1000)); + assert.deepEqual(lines(), [line]); + assert.deepEqual(stage(), ["drained"]); + assert.equal(events(f, "session-clear-stop").length, 1); + held(); + + down.clear(); + down.add("sessionClearRetire"); + await settle(() => stage()[0] === "exited"); + await new Promise((r) => setTimeout(r, 1000)); + assert.deepEqual(stage(), ["exited"]); + assert.deepEqual(lines(), [line], "the same code isn't logged again"); + held(); + + down.clear(); + const done = await settle(() => { + const e = host.call("session.operation", { thread: A, op: "clear-1" }); + return e.start === "started" && e; + }, 30000); + assert.deepEqual([done.state, done.start, done.refusal], ["retired", "started", null]); + const kinds = launchLog(f).map((e) => [e.event, e.session ?? null]).filter(([e]) => ["session-clear-stop", "end", "session-clear-retired", "launch"].includes(e)); + assert.deepEqual(kinds, [ + ["launch", D], + ["session-clear-stop", D], + ["end", D], + ["session-clear-retired", D], + ["launch", S], + ]); + assert.deepEqual(lines(), [line]); + assert.deepEqual(host.call("session.list", { thread: A }).map((r) => [r.id, r.state, r.default]), [ + [D, "retired", false], + [S, "active", true], + ]); +});