Files
MailUI4Agents/client/electron/test/run-all.mjs
JianFeeeee ec90cba129 test(criteria): 抽出共享 check/finish(marker 不再靠记性)+ 失败信息自带修法
pi 的三条增量,前两条落地:

1. **错误信息自带修法**:受众不只是读过规范的人 —— 并发写 WebUI 的 agent 新加判据时不会打开
   CRITERIA.md,看到红的第一反应可能是"套件坏了"。所以把可照抄的修法写进那条错误本身
   (共享 helper 的用法 + 样板文件路径),并说明 node:test 的判据不用管。
   **red 是 ta 一定会看到的,文档不一定被打开。**

2. **marker 由共享 helper 打印**:新增 test/lib/checks.mjs(导出 check/finish),
   计数只可能在该模块内发生 → "漏打 marker"与"计数写错位置"这两类在新文件上不可能发生。
   为避免"写了没人用"(本仓踩过的坑),同时把两个手工计数的判据改用它:
   narrow-layout(原来只有 failed 计数)与 markdown-xss(原来根本没有计数器)——
   条数不变(52 / 9),套件仍全绿。

未回改其余 10 个文件:run-all 的 marker 检查已经覆盖它们。
2026-09-14 15:11:47 +08:00

213 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 判据总入口 —— **全部跑完再算退出码**。
*
* 为什么不再用 `&&` 串起来:
*
* 原先 `npm test` 是 `a && b && c …`。这种行为有个不起眼但很贵的后果 ——
* **前面红一条,后面全部不跑**。于是"只红了一条"看起来像"只有一个问题"
* 实际上后面那些判据连跑都没跑(这次就真发生了:`background` 红着,
* `packaging` 从来没跑到过,而它正是能发现"界面改了没重打包"的那条)。
* 换句话说:`&&` 链下的"全绿"是可信的,**"红"是不可信的**。
*
* 现在:每条判据都跑,红的收集起来,最后一起报、一起退出。
*
* 另外两条防"判据自己不会跑"的自检(与 process.exit 之后写判据是同一族问题):
* 1. 清单里的文件必须存在(名字写错 = 静默跳过一条判据);
* 2. `test/` 下的每个 `*.test.mjs` 都必须在清单里
* —— 这次 `cross-client-theme.test.mjs` 就是"写好了但没接进套件"
* 在它进套件之前一直是隐身状态。加了这条,**新增判据忘了接线会直接红**。
* 3. 判据规范 `test/CRITERIA.md` 要在、且要点到那几条规则
* —— 写判据的规矩本身也会被"忘了带"(形状记在某个人的脑子里等于没有)。
*
* 写判据之前先读 `test/CRITERIA.md`(判结构与行为,不判字面与邻接)。
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..');
/**
* 判据清单:[文件, 额外 node 参数]。
*
* `--test` 给用 node:test 写的判据;鸿蒙那条要 `--experimental-strip-types`
* 才能直接执行 `client/harmony/.../MailGrouping.ts`(判据跑的是客户端真正引用的那份逻辑)。
*/
/*
* 自检 3判据规范在不在、有没有写到那几条关键规则。
*
* 为什么把"文档"也判:`CRITERIA.md` 里的每条都是踩出来的(窗口式判据、邻接式判据、
* 生成的清单被侵蚀、剥注释读不到理由……)。规则只在某个人的脑子里时,下一个人会重踩一遍;
* 文件被删/被搬走却没人发现,等于规则也没了。这里只断"还在 + 关键条目还在"
* 不断它的措辞 —— 那是笔记,不是接口。
*/
const CRITERIA_DOC = join(HERE, 'CRITERIA.md');
if (!existsSync(CRITERIA_DOC)) {
console.error('✗ 判据规范 test/CRITERIA.md 不见了(写判据的规矩不能只活在脑子里)');
process.exit(1);
}
const criteriaDoc = readFileSync(CRITERIA_DOC, 'utf8');
for (const must of ['配对/解析', 'allow-list', '变异验证', '剥掉注释', '按行', '自报条数', '只支撑你看到的那一层']) {
if (!criteriaDoc.includes(must)) {
console.error(`✗ 判据规范里少了「${must}」这条 —— 规则被删掉了还是搬走了?`);
process.exit(1);
}
}
const SUITE = [
['test/markdown-xss.test.mjs', [], 9],
['test/narrow-layout.test.mjs', [], 52],
['test/nav-merge.test.mjs', [], 8],
['test/theme.test.mjs', [], 30],
['test/background.test.mjs', [], 42],
['test/cross-client-theme.test.mjs', [], 15],
['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28],
['test/harmony-system-api.test.mjs', [], 5],
// P4 外观同步:跑 model/Appearance.ts纯逻辑所以也要 strip-types
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 23],
['test/build-stamp.test.mjs', [], 5],
['test/packaging.test.mjs', [], 3],
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2]
];
// 自检 1清单里的文件必须真的存在写错名字 = 那条判据永远不跑)
const ghosts = SUITE.map(([f]) => f).filter((f) => !existsSync(join(ROOT, f)));
// 自检 2test/ 下每个 *.test.mjs 都要在清单里(防"写好了没接线"
const onDisk = readdirSync(join(ROOT, 'test'))
.filter((f) => f.endsWith('.test.mjs'))
.map((f) => `test/${f}`);
const unwired = onDisk.filter((f) => !SUITE.some(([s]) => s === f));
if (ghosts.length || unwired.length) {
if (ghosts.length) console.error(`清单里的判据文件不存在:${ghosts.join('、')}`);
if (unwired.length) {
console.error(`这些判据文件没接进套件(写了却不会跑):${unwired.join('、')}`);
}
process.exit(1);
}
/*
* 自检 3判据不得写在 `process.exit()` **之后**pi 提议2026-09-14
*
* 自检 1/2 管的是"文件没接线",管不到"检查写在了退出之后" —— 而那正是实际发生过的
* 第 4 例4 条玻璃判据被并发写入落到了文件末尾、`process.exit()` 后面,
* 于是**一条都不执行、也不计入通过/失败**,输出看起来完全正常。
* 这种事的成因是结构性的(并发写入总是往文件末尾追加),所以它一定会再发生,
* 而它下一次仍然不报错 —— 静态扫一遍最省事。
*/
const buried = [];
for (const [file, flags] of SUITE) {
if (flags.includes('--test')) {
continue; // node:test 那几条没有 process.exit结构上不会踩这个
}
const src = readFileSync(join(ROOT, file), 'utf8');
const exitAt = src.lastIndexOf('process.exit(');
if (exitAt >= 0 && /(^|\n)\s*check\(/.test(src.slice(exitAt))) {
buried.push(file);
}
}
if (buried.length) {
console.error(`判据写在 process.exit() 之后,永远不会跑(挪到汇总之前):${buried.join('、')}`);
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, expected] of SUITE) {
console.log(`\n========== ${file} ==========`);
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}(跑完没有任何输出 —— 判据没跑起来的典型症状)`);
else {
/*
* 自报条数(闭环):自定义 check() 打 `RESULT pass=N fail=M`node:test 打 `# pass N`。
* 只解析**固定 marker**,不去猜口语汇总(「窄屏布局:全部通过」里没有数字,
* 靠猜数字的写法会误报 —— pi 提过,我也先贴过真实样本)。
*/
const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out);
const nodeTest = /^# pass (\d+)/m.exec(out);
const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null);
if (ran === null) {
/*
* 报错**自带修法**pi 2026-09-14这条契约的受众不只是读过规范的人 ——
* 并发写 WebUI 的 agent 新加判据时不会打开 CRITERIA.md看到红的第一反应
* 很可能是"套件坏了"(删自检、往清单里塞豁免)。**red 是 ta 一定会看到的东西,
* 文档不一定会被打开** —— 所以把修法直接写进这条错误里,并给出可抄的样板。
*/
reds.push(`${file}
↳ 没找到自报条数。修法(二选一):
1) 用共享 helper新判据推荐
import { check, finish } from './lib/checks.mjs';
check('判据名', 条件, '失败时给人看的细节');
finish('标签'); // 它负责打 RESULT pass=N fail=M
样板test/markdown-xss.test.mjs、test/narrow-layout.test.mjs
2) 自己打一行(老写法,计数必须写在 check() 内部,否则"实现被换空"看不见):
console.log(\`RESULT pass=\${pass} fail=\${fail}\`);
样板test/theme.test.mjs、test/background.test.mjs
(用 node:test 写的判据不用管runner 认 \`# pass N\`。)`);
} else if (expected > 0 && ran < expected) {
reds.push(`${file}
↳ 自报 ${ran} 条 < 清单里登记的 ${expected} 条。常见成因:判据被删/被跳过(写在 process.exit() 之后、
条件里提前 return、check() 的实现被改坏合并冲突、marker 打在了汇总之前但计数没接上。
确认确实该减少条数时,把清单里那个数字一起改掉(那是一次显式、可复核的编辑)。`);
}
}
}
console.log(`\n========== 判据汇总 ==========`);
if (reds.length === 0) {
console.log(`全部通过(${SUITE.length} 个判据文件:${SUITE.map(([f]) => f.replace('test/', '').replace('.test.mjs', '')).join('、')}`);
process.exit(0);
}
console.error(`红的判据(${reds.length}/${SUITE.length}`);
for (const r of reds) console.error(` - ${r}`);
process.exit(1);