Files
MailUI4Agents/client/electron/test/run-all.mjs
JianFeeeee 0ce8b29546 test(criteria): 判据规范 CRITERIA.md(邻接不是结构·第三次露头)+ 废弃 API allow-list 结构化 + 扫描范围自检
pi 的两条增量,都不需要他再确认。

## 1(pi 建议):生成清单 + 空 allow-list 的结构性风险

他的推演:SDK 升版会往清单里加新条目 → 某天早上套件**突然红**,且红在与本次改动无关的
代码上;这时人的第一反应是把名字塞进 allow-list —— 而 allow-list 一旦这么用,
就不再是"研究过的例外",只是"红的止痛药"。所以条目结构化:

```js
const ALLOW = [ /* { name, replacement, why } */ ];
```

`replacement` 非空是硬断言("暂时不想改"不是放行理由,"替代品要求的 API level 高于基线"才是)。
**刻意不断言"名单必须为空"** —— 那会挡住合理放行;断言的是"有名字、没替代品 → 红",
于是侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
变异:塞一条 `{ name:'px2vp', replacement:'', why:'暂时不想改' }` → 红。

## 2(pi 建议):扫目录的判据要防"空判据"

他问废弃 API 判据扫哪些目录(怕只扫 `pages/`,`common/` 里的旧写法逃掉)。
答案:扫的是**整个 ets 目录递归**(实测 25 个文件,含 `pages/ common/ model/ api/ entryability/`)。
顺手加了防退化的自检:文件数 ≥ 20,且 `pages/ common/ model/ api/` 四个目录都必须扫到
—— 目录改名/只扫一个子目录会让这条变成空判据而依然全绿。
变异:把扫描范围改成只扫 `pages/` → 红。

## 3(pi 建议):把"邻接不是结构"写进判据规范

同一个坑在本仓露头三次:① 窗口式正则被一行注释挤爆(原注释自嘲过);
② 括号配对取代窗口;③ 链式修饰符让"看前一个字符是不是 `}`"静默失效。
共同形式值得升格成规则,于是新建 `client/electron/test/CRITERIA.md`(七条),
并在 `run-all.mjs` 加**自检 3**:规范文件必须在、且必须点到关键条目。
WebUI 侧 `background.test.mjs` 的窗口式存量按 pi 的说明**记着不动**(那是他的地盘)。

规范里另外两条是本仓自己踩出来的:剥注释读代码 vs 读原文读理由(混用必红);
以及**验证要按真实入口跑** —— 我用 `node --test test/run-all.mjs` 验自检 3 时它"依然绿",
其实是 runner 把内部的 `process.exit(1)` 吞了;换成 `npm test` 走的那一行就红对了。
变异:删掉规范里的一条关键规则 → `npm test` 那条路 exit 1。

## 验证

`npm test` 退出码 0(11 个判据文件全绿 + vitest 258/258);`hvigorw assembleHap` 未受影响。
2026-09-14 14:51:30 +08:00

131 lines
6.2 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', []],
['test/narrow-layout.test.mjs', []],
['test/nav-merge.test.mjs', ['--test']],
['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']],
// P4 外观同步:跑 model/Appearance.ts纯逻辑所以也要 strip-types
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']],
['test/build-stamp.test.mjs', ['--test']],
['test/packaging.test.mjs', []]
];
// 自检 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);
}
const reds = [];
for (const [file, flags] of SUITE) {
console.log(`\n========== ${file} ==========`);
const r = spawnSync(process.execPath, [...flags, join(ROOT, file)], { stdio: 'inherit' });
// 不 break后面每条都要跑出来否则"红了几条"这个信息本身是假的
if (r.status !== 0) reds.push(`${file}(退出码 ${r.status}`);
}
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);