From 187a8d1fbc1821b0556b1c61ad2a75664832e34b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 10:01:01 +0000 Subject: [PATCH] fix(pm): stream dispatch-gates self-test verdicts, document detached run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit selfTest()'s t() collector now prints each case's ✓/✗ line the moment the case is decided, instead of buffering all cases into an array and printing them only after the last one in the tail loop. The tail is reduced to the failed/length summary, unchanged in wording and exit codes. On an agent container the battery cannot finish in the foreground (the container's ~10-minute cap SIGTERMs it), and a killed run used to leave zero diagnostic lines. Streaming means a killed run now leaves every case decided before the kill readable in the log. Both files' headers gain a "run this detached on an agent container" section with the exact command. The triage's "record it in package.json's script comment" instruction lands in check-dispatch-gates.mjs's header instead — package.json is JSON and holds no comments, and that file is what check:pm-dispatch-gates actually runs. Option 2 from the triage (share one derivation across the end-to-end cases) is deferred per the ruling — no case is merged, no spawn removed, no case's name, condition or order moved. --- scripts/pm/check-dispatch-gates.mjs | 25 ++++++++++++++++++ scripts/pm/dispatch-gates.mjs | 40 ++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/scripts/pm/check-dispatch-gates.mjs b/scripts/pm/check-dispatch-gates.mjs index 35b41c5b05..050865425b 100644 --- a/scripts/pm/check-dispatch-gates.mjs +++ b/scripts/pm/check-dispatch-gates.mjs @@ -6,6 +6,31 @@ * * node scripts/pm/check-dispatch-gates.mjs # runs the tool's --self-test * + * ## On an agent container, run this DETACHED (#14281) + * + * `package.json`'s `check:pm-dispatch-gates` script is exactly this file — + * `node scripts/pm/check-dispatch-gates.mjs` — and JSON holds no comments, so + * the instruction a script comment would carry lands here instead, in the + * header of the file that script runs. The battery this file spawns re-runs + * `dispatch-gates.mjs`'s own CLI as a child process many times (see the "Why + * the self-test ONLY" section below), and on an agent container that makes a + * full run longer than the container's foreground command cap, which SIGTERMs + * a run past it — this file's `result.signal` branch further down reports + * exactly that kill, but only once the process has already been cut off. Do + * not run `pnpm check:pm-dispatch-gates` (or `dispatch-gates.mjs --self-test` + * directly) in the foreground there. Detach it and poll the log instead: + * + * nohup pnpm check:pm-dispatch-gates > /tmp/pm-dispatch-gates.log 2>&1 & + * + * then tail the log file until it stops growing. `dispatch-gates.mjs`'s + * `selfTest()` streams each case's `✓`/`✗` line as that case is decided, so a + * run killed mid-battery — by this cap, or by anything else — leaves every + * case decided before the kill in the log, readable as a partial result. The + * measured runtime and case count are not repeated here — a reading belongs + * to a named commit, not to a header (the convention the next section states + * for this same file) — see #14281 for the reading that motivated this + * section. + * * ## Why the gate exists * * scripts/pm/dispatch-gates.mjs derives the "local gates for this card" line of diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 9743275c28..d7c9429189 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -17,6 +17,26 @@ * node scripts/pm/dispatch-gates.mjs --repo / ... # refuse unless this checkout IS that repo * node scripts/pm/dispatch-gates.mjs --self-test * + * ## Run --self-test DETACHED on an agent container (#14281) + * + * The battery re-spawns this tool's own CLI as a child process many times — + * deliberate (see `check-dispatch-gates.mjs`'s header for why a self-test this + * size is not fixture-only) — and on an agent container that makes the full + * run longer than the container's foreground command cap, which SIGTERMs a + * run past it. Do not run `--self-test` (or `pnpm check:pm-dispatch-gates`, + * which is exactly that flag) in the foreground there. Detach it and poll the + * log instead: + * + * nohup pnpm check:pm-dispatch-gates > /tmp/pm-dispatch-gates.log 2>&1 & + * + * then tail the log file until it stops growing. Each case's `✓`/`✗` line + * prints the moment that case is decided, so a run killed mid-battery — by + * this cap, or by anything else — still leaves every case decided before the + * kill in the log, readable as a partial result rather than a silent zero. The + * measured runtime and case count are not repeated here — they move with the + * tree and belong to a named commit, not to this header (see #14281 for the + * reading that motivated this section). + * * ## Harvest the machine-readable modes, never this prose (#13462) * * The matched block renders in TWO spellings — `pnpm check:NAME` and @@ -10032,7 +10052,19 @@ export function bannerLines({ identity, paths = [], drift = null }) { function selfTest() { const cases = []; - const t = (name, cond) => cases.push([name, cond]); + // Stream the verdict the moment it is decided (#14281) rather than only at + // the tail: every `t()` call evaluates `cond` eagerly at the call site, so + // the line below is not a preview of the tail loop's output — it prints the + // SAME verdict, just however many calls earlier than a buffered run did. A + // run that dies mid-battery (the container's foreground cap SIGTERMs a run + // past ~10 minutes; see check-dispatch-gates.mjs's header for the detached + // workaround) used to leave zero case lines; now the log already carries + // every case decided before the kill. `cases` still collects every entry — + // the tail's `failed`/`length` summary reads it unchanged. + const t = (name, cond) => { + cases.push([name, cond]); + console.log(` ${cond ? '✓' : '✗'} ${name}`); + }; const wf = [ 'jobs:', @@ -16829,10 +16861,12 @@ function selfTest() { } } + // The per-case line already printed inside `t()`, streamed as each verdict + // was decided (#14281) — this tail is the summary only, unchanged in shape + // and wording from the pre-streaming version. let failed = 0; - for (const [name, cond] of cases) { + for (const [, cond] of cases) { if (!cond) failed++; - console.log(` ${cond ? '✓' : '✗'} ${name}`); } if (failed) { console.error(`✗ dispatch-gates self-test: ${failed} of ${cases.length} case(s) failed.`);