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` 未受影响。
131 lines
6.2 KiB
JavaScript
131 lines
6.2 KiB
JavaScript
/**
|
||
* 判据总入口 —— **全部跑完再算退出码**。
|
||
*
|
||
* 为什么不再用 `&&` 串起来:
|
||
*
|
||
* 原先 `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)));
|
||
// 自检 2:test/ 下每个 *.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);
|