test(criteria): runner 不再手写 --test(由内容推导)+ 自检 4「这条判据能不能红」;规范补两档

pi 提的三条,第一条落地前先按他的要求**贴真实样本实测**,结论与他的猜测不同(记在代码里)。

## 1 runner 内部那个"配错 flag = 绿" —— 实测后换了个形状

pi 的猜测:给自定义 `check()` 的判据传 `--test`,runner 会报"0 个测试"并以 0 退出。
实测(node v22.22.2,两条真实样本):

- `node --test <自定义 check() 判据>`:**退出码照样传出来**(文件 exit 1 → 命令行 exit 1),
  没有被吞;
- 但 `node --test <什么都不做的文件>` 报 `# tests 1 / # pass 1` ——
  **pass 计数不是"检查跑过"的证据**。

所以"解析 pass 计数、0 就判红"这条路两头不讨好:抓不到空判据(它报 1),
还会在 `narrow-layout` 上误报(它的汇总行是"窄屏布局:全部通过",里面没有数字)——
正是 pi 提醒的"别照抄我的正则,先贴样本"。

换成两条**结构证据**:

- **`--test` 不再手写**:由文件内容推导(源码里 `from 'node:test'` 就走 node:test),
  清单里出现手写 `--test` 直接红 —— 配对错误不再靠记性维护;
- **自检 4**:每条判据文件里必须存在"能红"的路径(`test(` / `check(` / `process.exit(1)`),
  外加"跑完必须有输出"。一个都没有 = 它永远不会红,与"全通过"长得一模一样
  (这是"判据自己不会跑"家族的第 6 个宿主,家族表和六种宿主都写进规范了)。

变异:清单手写 `--test` → 红;加一条"什么都不做、退出 0"的判据 → 红;
静默成功(有能红路径但一行不输出)→ 红。

## 2 规范 §3 补一档:变异红了还要看**红在哪**(pi)

"只报红了不算,要能指名红的是哪几条";**红在解析/加载失败上不算红**(先让变异
"语法正确、语义错");变异作用于被剥掉的注释也不算。

## 3 `CRITERIA.md` 的可见性(pi 提的位置问题)

它管两个客户端的判据,却躺在 electron 的测试目录里。已在
`docs/HARMONY-ALIGN-PLAN.md` §四(验收纪律)加指针,并顺手把 pi 点名过的两条口径写死在那儿:
**"未验"只能用于"步骤做过、结果没看",功能不存在必须写"没做"**;
**"机制上确定不同"要判、不许记成"未验"**(深色档预设那次)。

## 验证

`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258)。
This commit is contained in:
2026-09-14 15:00:26 +08:00
parent 22c9be7181
commit 4babc96f8b
3 changed files with 101 additions and 9 deletions

View File

@ -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);
}
/*
* 自检 4pi 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========== 判据汇总 ==========`);