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], + ]); +});