diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 51f0303..32c0fcc 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -779,6 +779,44 @@ if (process.argv.includes('--probe-selftest')) { process.exit(bad ? 1 : 0); } +/* + * ★★ **上游退出码的唯一权威**:`diag` 码 → `summary.py` 真跑出来的退出码。 + * + * pi 2026-09-18 报的那条:`--verdict-selftest` 第 ⑤ 条**注释**说 + * "把 `summary.py` 的退出码分类与 `run-all` 的表**绑在一起**验(**不在一边写死另一边的值**)", + * 而它的**实现**只遍历 `DIAG` 表 + 跳过三个**硬编码**的码名: + * `if (code === 'none' || code === 'baseline-absent' || code === 'baseline-stale') continue;` + * —— **一个字节都没读 `summary.py`**,它断言的其实是"除了我手列的这三个,其它码都必须红"。 + * 即:**注释说"不写死",实现恰恰写死了一份码名单。** + * + * 两个方向都实测失真(我按 pi 的构造各做了一次): + * · **假红**:加一个**上游退 0 的提示码**(与 `baseline-stale` 同类、完全合法) + * ⇒ 报 `上游退非零,下游却不红 ⇒ 两侧口径漂移`。**那个码的上游根本不存在**, + * `summary.py` 永远不会打出它 —— ⑤ 在这里**编造了一个上游事实**并据此判红。 + * · **假绿**:加一个 `blocksGreen: true` 的新码 ⇒ **rc=0,一条 RED 都没有**。 + * 同一句"上游退非零"也是编的,⑤ 没验也验不了 ⇒ 漂移从这个方向完全看不见。 + * + * ⇒ 修法(照 pi 的建议):把 `(diag, rc)` 抽成**唯一权威**本表,然后 + * ① `--exitcode-selftest` **真跑** `summary.py`,逐条断言 `rc === UPSTREAM_RC[diag]` + * ⇒ 本表的**每一个值都被真实脚本验过**(不是我在表里写的希望值); + * ② `--verdict-selftest` 第 ⑤ 条拿本表与 `DIAG.blocksGreen` **双向**比对 + * ⇒ 两个方向都真绑上,**白名单消失**(数据说话)。 + * 链条:**真脚本 → UPSTREAM_RC → DIAG.blocksGreen**,每一跳都有自检守着。 + * + * ★ 加一个新码 ⇒ 必须在 `UPSTREAM_RC` 里登记"它退几", + * 否则 ⑤ 报"没登记上游"(与 `blocksGreen` 那条"缺字段即红"是同一句纪律的另一半)。 + */ +const UPSTREAM_RC = { + 'none': 0, + 'counts-unusable': 2, + 'manifest-mismatch': 1, + 'baseline-residue': 1, + 'baseline-stale': 0, + 'baseline-unrunnable': 2, + 'baseline-unknown': 2, + 'baseline-absent': 0, +}; + /* * summary.py 退出码契约自检(`--exitcode-selftest`):**跑真的那个脚本**。 * @@ -1010,6 +1048,7 @@ if (process.argv.includes('--exitcode-selftest')) { ['jobs-one.json'], ['jobs-one.json'], 0, 'diag=baseline-absent', 'nobaseline'], ]; let bad = 0; + const seenDiags = []; // 反向覆盖用:本组案例**真跑出来**的码(不是声明的) // 前提构造不出来 ⇒ 明说 + 计红(不许静默跳过) if (!dropTo) { console.log('RED 降权工具(runuser/setpriv)不可用 ⇒ **rc=2 的两条分支本机覆盖不到** —— ' + @@ -1034,11 +1073,61 @@ if (process.argv.includes('--exitcode-selftest')) { } const okStatus = got.status === want; const okOut = wantOut === null || got.out.includes(wantOut); - const ok = okStatus && okOut; + /* + * ★★ 顺带把 `UPSTREAM_RC` **逐个真跑验一遍**(pi 2026-09-18 本封)。 + * + * 这一跳是整条链的地基:`UPSTREAM_RC` 名义上是"上游退出码的唯一权威", + * 但若它只是我**手抄**的一张表,那 ⑤ 拿它去比 `blocksGreen` 仍然可能两边一起错 + * (而且会**自恰**地绿 —— 本仓消过很多次的那种)。 + * ⇒ 每条案例若声明了 `diag=<码>`,就用**真脚本真跑出来的 rc** 去核对那张表。 + * 于是表里**每一个值都是被真脚本验过的**,不是我希望它等于几。 + * (案例没带 `diag=` 的,只判上面的 rc/输出 —— 它们守的是别的性质,如 ghosts/unlisted。) + */ + /* + * ⚠️ 第一版我按**声明**的 `wantOut` 去找码 —— 于是 `wantOut` 是 `null` 的那四条 + * (一致 / 说明条目 / 未列入清单 / 清单有磁盘无)**一个码都没验到**, + * 反向覆盖当场报出 `none`、`counts-unusable`、`manifest-mismatch` 三个码"没人验过"。 + * 而这三个码真跑出来是 0 / 2 / 1(我单独量过)—— 表里写的是对的,**是验法漏了**。 + * ⇒ 改成从**脚本真打出来的 stdout** 里取码:这样"验的是它真说的",而不是"我以为它会说的", + * 且对 `wantOut=null` 的案例同样有效。 + */ + let rcCheck = ''; + const seenDiag = (got.out.match(/diag=([\w-]+)/) || [])[1]; + if (seenDiag) seenDiags.push(seenDiag); + if (seenDiag) { + if (!(seenDiag in UPSTREAM_RC)) { + rcCheck = `;**新码 \`${seenDiag}\` 没在 UPSTREAM_RC 里登记上游退出码**`; + bad++; + } else if (UPSTREAM_RC[seenDiag] !== got.status) { + rcCheck = `;**UPSTREAM_RC[${seenDiag}]=${UPSTREAM_RC[seenDiag]} 与真跑出来的 rc=${got.status} 不符**`; + bad++; + } else { + rcCheck = `(真打出 diag=${seenDiag},UPSTREAM_RC=${got.status} ✓)`; + } + } else if (!/Traceback/.test(got.err)) { + // 打得出来却没有 `diag=` ⇒ 契约被破坏(诊断码是判据的一部分) + rcCheck = ';**脚本打出了 RESULT 却没带 `diag=`**(诊断码是判据的一部分,不许漏)'; + bad++; + } + const ok = okStatus && okOut && !rcCheck.includes('**'); console.log(`${ok ? 'ok ' : 'RED '} ${what}:rc=${got.status}` + - `${ok ? '' : `(期望 rc=${want}${wantOut ? ` 且输出含 "${wantOut}"` : ''})`}`); + `${ok ? rcCheck : `(期望 rc=${want}${wantOut ? ` 且输出含 "${wantOut}"` : ''}${rcCheck})`}`); if (!ok) bad++; } + /* + * ★ 反向覆盖:`UPSTREAM_RC` 里**每个码**都得有案例真跑过 —— + * 只登记不验,就等于表里有个"我希望它是几"的格子(正是上面那段要防的)。 + */ + { + const covered = new Set(seenDiags); + const uncovered = Object.keys(UPSTREAM_RC).filter(c => !covered.has(c)); + if (uncovered.length) { + console.log(`RED UPSTREAM_RC 里这些码**没有案例真跑过** ⇒ 表里的值是没人验过的:${uncovered.join('、')}`); + bad++; + } else { + console.log(`ok UPSTREAM_RC 的 ${Object.keys(UPSTREAM_RC).length} 个码都有案例真跑验过`); + } + } process.exit(bad ? 1 : 0); } @@ -1793,16 +1882,62 @@ if (process.argv.includes('--verdict-selftest')) { } console.log(`ok 码表每个码都有 blocksGreen(${Object.keys(DIAG).length} 个码)`); /* - * ⑤ 口径一致性:`blocksGreen === true` 的码,其**上游退出码必须非零**。 - * 两边漂移(上游退 0、下游却红)会让人去查一个"其实没被报错"的东西。 - * 这条把 `summary.py` 的退出码分类与 `run-all` 的表**绑在一起**验(不在一边写死另一边的值)。 + * ⑤★ 口径一致性(pi 2026-09-18 本封报的那条)。 + * + * ⚠️ **上一版这条是坏的,两个方向都坏**,pi 报了、我复现了: + * 注释写"把 `summary.py` 的退出码分类与 `run-all` 的表**绑在一起**验 + * (**不在一边写死另一边的值**)",而实现只遍历 `DIAG` 表 + 跳过三个**硬编码**码名 —— + * **一个字节都没读 `summary.py`**。它其实在断言"**除了我手列的这三个,其它码都必须红**": + * **注释说"不写死",实现恰恰写死了一份码名单。** + * + * 实测两个方向: + * · **假红**:加一个上游退 0 的**提示码**(与 `baseline-stale` 同类、完全合法) + * ⇒ 报"上游退非零,下游却不红 ⇒ 两侧口径漂移"。 + * 而那个码的上游**根本不存在**(`summary.py` 永远不会打出它)—— + * ⑤ 在这里**编造了一个上游事实**并据此判红。 + * · **假绿**:加一个 `blocksGreen: true` 的新码 ⇒ **rc=0,一条 RED 都没有**。 + * 同一句"上游退非零"也是编的 ⇒ 漂移从这个方向**完全看不见**。 + * + * ⇒ 现在拿**唯一权威** `UPSTREAM_RC`(它的每个值都在 `--exitcode-selftest` 里被**真脚本** + * 逐条验过,见那一段的反向覆盖检查),与 `DIAG.blocksGreen` **双向**比对: + * · 上游退非零、下游却不红 ⇒ 漂移(该红的没红); + * · 上游退 0、下游却标阻断 ⇒ 漂移(反之亦然,上一版根本查不到这个方向); + * · 码没在 `UPSTREAM_RC` 里登记 ⇒ 红("加码要同时登记它退几")。 + * **白名单随之消失**(数据说话)—— 这也是 `DIAG` 那条"一个码一行、一次定完"的延伸: + * 上游退出码也该是那一行的一部分。 */ - for (const [code, e] of Object.entries(DIAG)) { - if (code === 'none' || code === 'baseline-absent' || code === 'baseline-stale') continue; - if (!e.blocksGreen) { - console.log(`RED \`${code}\` 上游退非零,下游却不红 ⇒ 两侧口径漂移`); + { + const missing = Object.keys(DIAG).filter(c => !(c in UPSTREAM_RC)); + const extra = Object.keys(UPSTREAM_RC).filter(c => !(c in DIAG)); + if (missing.length) { + console.log(`RED DIAG 里这些码**没在 UPSTREAM_RC 登记上游退出码**:${missing.join('、')}` + + ' ⇒ 加一个码必须同时登记"它退几",否则⑤无从比对'); bad++; } + if (extra.length) { + console.log(`RED UPSTREAM_RC 里这些码在 DIAG 里不存在:${extra.join('、')} ⇒ 两张表漂移`); + bad++; + } + const drift = []; + for (const [code, e] of Object.entries(DIAG)) { + const rc = UPSTREAM_RC[code]; + if (rc === undefined) continue; + if (rc !== 0 && !e.blocksGreen) drift.push(`${code}(上游 rc=${rc},却 blocksGreen=false)`); + if (rc === 0 && e.blocksGreen) drift.push(`${code}(上游 rc=0,却 blocksGreen=true)`); + } + if (drift.length) { + console.log(`RED 两侧口径漂移(**双向**比对 UPSTREAM_RC ↔ blocksGreen):${drift.join(';')}`); + bad++; + } + /* + * ⚠️ `ok` 只在**这一格完全没红**时打。我第一版无条件打 ⇒ 变异 (a) 的输出长成 + * `RED DIAG 里这些码没在 UPSTREAM_RC 登记…` 紧跟 `ok 口径一致(双向):9 个码…` —— + * **同一格的报文自相矛盾**(读者会以为没红)。与结构锚点那次是同一个错,我又犯了一遍。 + */ + if (!missing.length && !extra.length && !drift.length) { + console.log(`ok 口径一致(双向):${Object.keys(DIAG).length} 个码的` + + '`UPSTREAM_RC ↔ blocksGreen` 逐条相符,且无未登记/多余码'); + } } /* * ⑥★ **结构锚点:生产路径必须真的经过 `applyMutants`**。