diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index b1da1db..055916c 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -60,6 +60,43 @@ const ALLOW = [ /* { name, replacement, why } */ ]; 顺带:变异后**不要用 `git checkout` 还原**(会连同未提交的改动一起抹掉)。 先 `cp` 到备份,再从事先的备份还原。 +### 变异红了,还要看**红在哪**(pi 2026-09-14 补的一档) + +只报"红了"不算证据。反过来的那一半同样成立:**变异后红了,也可能是假红** —— +比如变异把文件写坏了语法,判据红在"模块加载失败/解析不到源码",看起来像判据生效, +实际那条断言根本没执行。所以: + +- 报结果要能**指名红的是哪几条**(本仓的变异表就是按这个写的: + "红 2 条(B + 裸色值)"、"红 3 条(含品牌色防线)"); +- **红在解析/加载失败上不算红** —— 先让变异"语法正确、语义错",再谈判据有没有生效; +- 同理,变异**作用于注释**(被剥掉的那部分)也不算:判据读的是剥注释后的源码, + 文档里的"理由"断言才读原文(见 §4)。 + +### 判据自己不会跑:一个家族,六种宿主 + +这条家族在本仓已经露头六次,共同点都是"**看起来全绿**": + +| # | 宿主 | 形态 | +|---|---|---| +| ① | `&&` 链 | 前面红一条,后面全部不跑("红"不可信) | +| ② | 判据文件 | 没接进 `SUITE`(写好了但隐身) | +| ③ | 清单名字 | 文件名写错 = 静默跳过一条判据 | +| ④ | 文件末尾 | 判据写在 `process.exit()` **之后**(并发写入总往末尾追加) | +| ⑤ | 验证手法 | 用 `node --test` 去验 runner,里面的 `process.exit(1)` 被吞(见 §6) | +| ⑥ | runner 内部 | 清单 flag 与判据写法配错 | + +`run-all.mjs` 现在对 ②③④⑥ 都有静态自检,⑥ 的落地方式见下(**不要**照抄"解析 pass 计数"): + +⚠️ **实测过两条真实样本,结论与直觉不同**: +- `node --test <自定义 check() 的判据>`:**退出码照样传出来**(文件 exit 1 → 命令行 exit 1), + 并没有被 runner 吞掉; +- 但 `node --test <什么都不做的文件>` 会报 `# tests 1 / # pass 1` —— + **计数不是"检查跑过"的证据**。所以"数 pass、0 就判红"既抓不到空判据(它报 1), + 又会在汇总行没有数字的判据上误报。 + +改用**结构证据**:每条判据文件里必须存在"能红"的路径(`test(` / `check(` / `process.exit(1)`), +外加"跑完必须有输出"。**先看真实输出再写规则**——这一条本身就是 §1 的推论。 + ## 4. 读源码断言的两种模式,别混用 - 断**代码行为**:读**剥掉注释**的源码(注释里出现的调用不是调用); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index ada1524..3e274b7 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -59,17 +59,17 @@ for (const must of ['配对/解析', 'allow-list', '变异验证', '剥掉注释 const SUITE = [ ['test/markdown-xss.test.mjs', []], ['test/narrow-layout.test.mjs', []], - ['test/nav-merge.test.mjs', ['--test']], + ['test/nav-merge.test.mjs', []], ['test/theme.test.mjs', []], ['test/background.test.mjs', []], - ['test/cross-client-theme.test.mjs', ['--test']], - ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']], - ['test/harmony-system-api.test.mjs', ['--test']], + ['test/cross-client-theme.test.mjs', []], + ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings']], + ['test/harmony-system-api.test.mjs', []], // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types - ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']], - ['test/build-stamp.test.mjs', ['--test']], + ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings']], + ['test/build-stamp.test.mjs', []], ['test/packaging.test.mjs', []], - ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']] + ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings']] ]; // 自检 1:清单里的文件必须真的存在(写错名字 = 那条判据永远不跑) @@ -113,12 +113,59 @@ if (buried.length) { process.exit(1); } +/* + * 自检 4(pi 2026-09-14 提的家族,第 6 例):**"判据自己不会跑"**。 + * 第 6 例的宿主是 runner 自己:清单里的 flag 与判据写法如果配错,症状是"看起来全绿"。 + * + * ⚠️ 落地前先实测了两条真实样本,结论与 pi 的猜测**不同**,记在这里免得后人重猜: + * - `node --test <自定义 check() 的判据>`:**退出码照样传出来**(文件 exit 1 → 命令行 exit 1), + * 并没有被 runner 吞掉; + * - 但 `node --test <什么都不做的文件>` 会报 `# tests 1 / # pass 1` —— + * **计数不是"检查跑过"的证据**。所以"解析 pass 计数、0 就判红"这条路既 + * 抓不到空判据(它报 1),又会在 `narrow-layout`(汇总行"全部通过"里没有数字)上误报。 + * + * 换成**结构证据**:每条判据文件里必须存在"能红"的路径 —— + * node:test 的 `test(`、自定义 `check(`、或显式 `process.exit(1)`。 + * 一个都没有 = 它永远不会红,与"全通过"长得一模一样。 + * 再加一条"跑完必须有输出"(12 条判据现在都有输出),静默成功同样可疑。 + */ +function shapeOf(file) { + const src = readFileSync(join(ROOT, file), 'utf8'); + const usesNodeTest = /from 'node:test'/.test(src); + const canFail = usesNodeTest + || /(^|[^.\w])check\(/.test(src) + || /process\.exit\(\s*1\s*\)/.test(src); + return { usesNodeTest, canFail }; +} + +const shapeless = []; +for (const [file, flags] of SUITE) { + if (flags.includes('--test')) { + console.error(`清单里不要手写 --test(它由文件内容推导):${file}`); + process.exit(1); + } + if (!existsSync(join(ROOT, file))) continue; // 自检 1 已经报过了 + if (!shapeOf(file).canFail) shapeless.push(file); +} +if (shapeless.length) { + console.error('这些判据文件里找不到任何"能红"的路径(test( / check( / process.exit(1)):' + + `${shapeless.join('、')} —— 它们永远不会红,与"全通过"看起来一样`); + process.exit(1); +} + const reds = []; for (const [file, flags] of SUITE) { console.log(`\n========== ${file} ==========`); - const r = spawnSync(process.execPath, [...flags, join(ROOT, file)], { stdio: 'inherit' }); + const shape = shapeOf(file); + const all = shape.usesNodeTest ? [...flags, '--test'] : flags; + // 收集输出再自己打回去:观感不变(stdio:'inherit' 的等价物),但能顺手做"跑了吗"的检查 + const r = spawnSync(process.execPath, [...all, join(ROOT, file)], { encoding: 'utf8' }); + const out = (r.stdout || '') + (r.stderr || ''); + process.stdout.write(r.stdout || ''); + process.stderr.write(r.stderr || ''); // 不 break:后面每条都要跑出来,否则"红了几条"这个信息本身是假的 if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`); + else if (out.trim().length === 0) reds.push(`${file}(跑完没有任何输出 —— 判据没跑起来的典型症状)`); } console.log(`\n========== 判据汇总 ==========`); diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index dd23e4f..728afd5 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -88,7 +88,15 @@ WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递 1. 每个页面都要有**点它**的判据(WebUI 侧就是因为只验结构没验点击, 漏掉了"侧栏点了不翻页")。 2. 判据不许只看截图:要量几何/对比度/命中区。 -3. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成"。 +3. 无法验证的要**如实标注**(例如"编译通过、视觉未验"),不能写成"已完成")。 + ⚠️ 两条口径(这轮各踩过一次,pi 复核时点名): + - **"未验"只能用于"步骤做过、结果没看"**;功能不存在必须写"**没做**" + (把"没做"写成"没验"会让人以为只剩观感风险,实际那里什么都没有); + - **"机制上确定不同"要判,不许记成"未验"**(深色档预设那次就是)。 +4. **判据怎么写**:见 `client/electron/test/CRITERIA.md` —— 那份规范管**两个客户端**的判据 + (判结构与行为、不判字面与邻接;清单与 allow-list 的形状;变异要红在预期位置; + "判据自己不会跑"那个家族的六种宿主)。**写在 electron 的测试目录下只是因为 + `run-all.mjs` 在那儿强制它存在**,鸿蒙侧改判据前先读它。 ---