From d23ef69c88f42c4d8193756658002e7a5fd8cc6e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 12:44:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E4=BF=AE=20`ran`/`checks?= =?UTF-8?q?`=20=E8=A2=AB=E7=AE=97=E4=B8=A4=E9=81=8D=EF=BC=88=E6=90=AC?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E5=8F=AA=E5=8A=A0=E6=B2=A1=E5=88=A0=EF=BC=89?= =?UTF-8?q?+=20=E8=A1=A5=20`red=3D`=20=E6=A0=BC=20+=20=E8=87=AA=E6=A3=80?= =?UTF-8?q?=205=20=E9=92=89=E4=BD=8F=E8=BF=99=E7=B1=BB=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-15 把我上封 §⑦ 提的"口径对不上"**查成了真 bug**,不是已知口径。 他要的"对齐"我去做了,结果是**我错、他对**:总数行不只是"缺一格",其中**两格是虚高的**。 ## 一、`ran` / `checks` / `pass` 被**算了两遍** 累加块(`totalTests += ran` / `ranFilesCount += 1` / `totalFail += …`)**存在两份**: - 一份在红绿分支**之外**(`if (ran !== null) { … }`,注释还写着"必须在红/绿分支之外")—— 这是我**修另一半 bug 时搬过去**的:原先它在 `else` 里 ⇒ 红文件一条都不计入,`checks` 静默少算; - **另一份还留在绿色 `else` 分支里**,搬动那一步**只加了新的、没删旧的**。 后果:**每个 exit 0 的文件都被计两遍**,只有退出码非 0 的(当时是 `build-stamp`)计一遍。 **修前 → 修后**(同一棵树、同一批判据): ``` 修前 RESULT files=25 ran=49 checks=792 pass=791 fail=1 red=? broken=0 unreported=0 修后 RESULT files=25 ran=25 checks=399 pass=398 fail=1 red=4 broken=0 unreported=0 ``` `ran: 49 → 25`(**每个文件恰好一次**,与 `files=25` 相等);`checks: 792 → 399`。 验算与 pi 的预测吻合:`24 个 exit 0 文件各多算一次` ⇒ `790 = 399 + 391`, `ran = 24×2 + 1 = 49`。 ★ **它是自洽的,所以读数检查不出来**:`pass + fail == checks`(791+1=792) **两边一起虚高,恒等式照样成立** —— "自洽的读数不等于正确的读数"。 而 `RESULT files=…` 这行当初**就是为"总数要有权威来源"才加的**,结果**它自己就是错的**: 新机制进来时把旧路径留在原地。**与我们这一路在消的形状同族,只是这次长在总数行上。** 修法:`else` 分支里**只留 `if (ran === null) { … }`**("没找到自报条数"那条红), 累加删掉。顺带修掉一个还没露面的同类:`unreportedFiles += 1` 原来也在两处, 在"绿且没自报"时会加两次(现在 `unreported=0`,所以没人看见)。 ## 二、`fail=1` vs 红 4 个:不是 bug,但**行里缺一格** `fail` 累加的是**文件自报的失败条数**;那 3 条"自报条数 ≠ 登记数"的红是**运行器自己**推的 (`reds.push`),**不经过文件** ⇒ 进不了 `fail`。两格单位不同却只有一格,于是 `fail=1` 被读成"只有一处坏了"。**已补 `red=${reds.length}`**,并在那行上方写清三格的区别。 ## 三、★ 自检 5:这类回归原来**没有任何东西拦着** 这个 bug 的形状是"**搬动一段代码时只加不删**",两处都跑得通、都报得出数, 而**没有任何东西会红**。所以加了一条结构性判据: ``` ran ≤ files —— 一个文件最多贡献一次 ranFilesCount fail ≤ checks —— fail 是 checks 的子集 ``` **为什么只判这两条**:`ran ≤ files` 是**结构性的上限**,不需要知道任何自报条数就能判。 而 `checks` **没有**这种上限(条数是文件自己报的)—— 给它编一个上限就是**猜数字的判据**, 所以不编;`checks` 的正确值只能逐文件核(这正是这个 bug 难发现的原因)。 **变异验证**(不是只跑绿):把双计重新注入 `else` 分支 ⇒ `✗ 套件自检 5:ran=49 > files=25 —— 每个文件只该计一次,超出来说明累加块有不止一份`; 还原 ⇒ `ok 5 - 总数行自洽(ran=25 ≤ files=25)`。 ## 四、格式改动的影响面我核了 `RESULT files=…` 加了 `red=` 一节 ⇒ 先查**有没有别的解析者**: 全仓(`*.mjs`/`*.sh`/`*.md`/`*.go`)搜 `RESULT files=` 与 `checks=`/`ran=`, **除 `run-all.mjs` 自己外没有任何消费者**;`mutants/` 那套工具**不共用**这一行 (它自己打 `RESULT mutants=48 ran=48 …`,口径 A = 36 不变)。所以加格是安全的。 ## 五、未做 - 到期那 7 条(`STATIC_ONLY`)仍未动。 - 红仍 4 条、**都不是我的**(`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`build-stamp`)。 - `mutants` 的 `ran=48`/`mutants=48` 与本次无关(另一套计数),**没动**。 --- client/electron/test/run-all.mjs | 87 +++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index c2eae02..8e0964e 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -305,20 +305,31 @@ for (const [file, flags, expected] of SUITE) { } else if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`); else { /* - * 自报条数(闭环):自定义 check() 打 `RESULT pass=N fail=M`,node:test 打 `# pass N`。 - * 只解析**固定 marker**,不去猜口语汇总(「窄屏布局:全部通过」里没有数字, - * 靠猜数字的写法会误报 —— pi 提过,我也先贴过真实样本)。 + * ★ 这里**只保留"没自报条数"的那条红**;累加**不能**在这里做。 + * + * 这段曾经是累加块(`totalTests += ran` / `ranFilesCount += 1` / `totalFail += …`)**唯一的** + * 位置,而它在 `else` 里 ⇒ **红文件一条都不计入**,`checks` 静默少算 + * (实测:`checks=379` 而逐文件手算 387,差的正是红文件里的)。 + * 后来把累加搬到了红绿**分支之外**(上面 `if (ran !== null) { … }`), + * **但没删掉这一份** —— 于是变成: + * + * `totalTests`/`ranFilesCount`/`totalFail` 对**每个 exit 0 的文件计两遍**, + * 只有退出码非 0 的文件(当时是 `build-stamp`)计一遍。 + * 实测吻合:25 个 section、24 个 exit 0、1 个非 0 + * ⇒ `ran = 24×2 + 1 = 49`(汇总行正是 `ran=49`) + * ⇒ `checks ≈ 逐文件单次合计 × 2`(`checks=792`) + * + * ★ 它是**自洽**的,所以读数检查不出来:`pass + fail == checks` + * (791+1=792)**两边一起虚高,恒等式照样成立** —— + * "自洽的读数不等于正确的读数"。而 `RESULT files=…` 这行本是为 + * "总数要有权威来源"才加的,结果**它自己就是错的**: + * 新机制进来时把旧路径留在原地。这和我们这一路在消的形状同族, + * 只是这次长在总数行上(pi 2026-09-15 找到的)。 + * + * ⇒ 教训写在这儿:**搬动一段代码时,删旧的那一步要和加新的那一步一起提交**; + * 只加不删的话,两处都跑得通、都报得出数,而**没有任何东西会红**。 */ - const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out); - const nodeTest = /^# pass (\d+)/m.exec(out); - const nodeFail = /^# fail (\d+)/m.exec(out); - const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null); - // 累计总数(见汇总末尾的 `RESULT files=…`)—— 解析成功就加,无论该文件红绿 - if (ran !== null) { - totalTests += ran; - ranFilesCount += 1; - totalFail += marker ? Number(marker[2]) : (nodeFail ? Number(nodeFail[1]) : 0); - } if (ran === null) { + if (ran === null) { unreportedFiles += 1; /* * 报错**自带修法**(pi 2026-09-14):这条契约的受众不只是读过规范的人 —— @@ -602,6 +613,43 @@ try { selfCheckFailed = true; } +/* + * 自检 5(pi 2026-09-15 找到的总数行 bug):**`ran` 不许超过 `files`** —— 每个文件只该计一次。 + * + * 起因(真发生过):累加块在红绿分支**之外**和绿分支 `else` 里**各有一份**, + * 于是每个 exit 0 的文件被计**两遍** ⇒ `ran=49`(而 files=25)、`checks` 约 2 倍。 + * 它**自洽**,所以读数检查不出来:`pass + fail == checks`(791+1=792)两边一起虚高,恒等式照样成立。 + * 而 `RESULT files=…` 这行本是为"总数要有权威来源"才加的 —— **它自己就是错的**。 + * + * 判据形状:`ran <= files` 是**结构性的上限**(一个文件最多贡献一个 `ranFilesCount`), + * 不需要知道任何自报条数就能判。`checks` 没有这种上限(条数是文件自己报的), + * 所以别在这里给 `checks` 编一个 —— 那会变成"用猜数字的判据"。 + * + * ⚠️ 这条自检管的是**总数的自洽性**,不是"条数对不对":`checks` 的正确值只有 + * 逐文件核得出(这正是这个 bug 难发现的原因)。它拦的是"又一次搬动只加不删"。 + */ +try { + const probs = []; + if (ranFilesCount > SUITE.length) { + probs.push(`ran=${ranFilesCount} > files=${SUITE.length} —— 每个文件只该计一次,` + + `超出来说明**累加块有不止一份**(搬动时只加没删)。` + + `修法:全文件搜 \`totalTests +=\`,累加只许出现在一处(红绿分支之外那一处)。`); + } + if (totalTests < totalFail) { + probs.push(`checks=${totalTests} < fail=${totalFail} —— fail 是 checks 的子集,这不可能成立`); + } + if (probs.length) { + console.error('✗ 套件自检 5(总数行的自洽性):'); + for (const x of probs) console.error(` - ${x}`); + selfCheckFailed = true; + } else { + console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},fail 是 checks 的子集)`); + } +} catch (e) { + console.error(`✗ 套件自检 5 跑不了:${e.message || e}`); + selfCheckFailed = true; +} + /* * pi 2026-09-15:这块原先在报文末尾 `process.exit(1)`,而**它在汇总之前** ⇒ * 只要到期闸红了,`RESULT` 行、欠账余额、条数不符、以及**所有其它红**都不会打出来。 @@ -738,9 +786,20 @@ console.log(`\n========== 判据汇总 ==========`); * 那个口径只跑标准 `test/*.test.mjs`,**不跑套件里的构建相位**,也不含本次相位跳过。 * `pass + fail == checks` 恒成立(fail 是那些文件里的红条数)。 */ +/* + * ★ 这一行的**三个"坏"计数单位不同**,别混着读(pi 2026-09-15 指出缺一格): + * + * · `fail=N` —— **文件自报的失败条数**(`# fail N` / `RESULT fail=N` 累加)。 + * · `red=N` —— **红的判据文件数**(`reds`,含**运行器自己**推的那些,见下)。 + * · `broken=N` —— 跑不起来(exit 非 0 / 无输出)的文件数。 + * + * 为什么必须把 `red` 显式打出来:那 3 条"自报条数 ≠ 登记数"的判据是**运行器自己**推的 + * (`reds.push`),根本不经过文件 ⇒ **进不了 `fail`**。实测过 `fail=1` 而"红的判据(4/25)", + * 于是 `fail=1` 被读成"只有一处坏了"。两格都在,读的人才不用自己猜单位。 + */ const totalsLine = (verdict) => `RESULT files=${SUITE.length} ran=${ranFilesCount} ` + `checks=${totalTests} pass=${totalTests - totalFail} fail=${totalFail} ` + - `broken=${brokens.length} unreported=${unreportedFiles} verdict=${verdict}`; + `red=${reds.length} broken=${brokens.length} unreported=${unreportedFiles} verdict=${verdict}`; if (reds.length === 0 && brokens.length === 0 && !dueFailed && !selfCheckFailed) { // 报**本相位实际跑了**几条,不报清单长度 —— 上面刚写完"不能声称自己做过的事", // 这里如果打 SUITE.length 就是在同一行里犯同一个错(2 条构建相位判据被跳过了)。