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.`);