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` 未受影响。
This commit is contained in:
91
client/electron/test/CRITERIA.md
Normal file
91
client/electron/test/CRITERIA.md
Normal file
@ -0,0 +1,91 @@
|
||||
# 判据规范(写判据前先读这份)
|
||||
|
||||
这份文件记的是**判据本身的写法**:什么样的判据能红、能红在对的地方、以及不会在"代码完全正确"时乱红。
|
||||
不是"怎么用 node:test",而是这个仓库里踩出来的形状。
|
||||
|
||||
一句话版本:**判据要钉结构与行为,不要钉字面与邻接。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 判结构必须配对/解析;看邻接或固定宽度都会被合法写法绕过
|
||||
|
||||
统一形式(这是同一个坑在本仓露头的**第三次**,所以升格成规则):
|
||||
|
||||
> **判结构要配对/解析(括号、块、AST);看"前一个字符"或"固定宽度窗口"都会被合法写法绕过。**
|
||||
|
||||
三次露头:
|
||||
|
||||
| # | 当时的写法 | 被什么合法写法绕过 |
|
||||
|---|---|---|
|
||||
| ① | 固定宽度窗口正则 | 往规则里加一行注释就把 `backdrop-filter` 挤出窗口 —— 断言变红而 CSS 完全正确(`background.test.mjs` 的注释里记着这件事) |
|
||||
| ② | 括号配对取代窗口 | 修掉了 ①,方向对 |
|
||||
| ③ | "调用点往前看一个字符是不是 `}`" | ArkUI 的**链式修饰符**:`Row(){…}.backgroundColor(x).backgroundBlurStyle(A)` 里调用点前面是 `)` 不是 `}` → 判据**静默失效**(拿到 null 就放过去了) |
|
||||
|
||||
③ 的修法:往回扫时**跳过成对的括号组**再找 `}`。它同时暴露了"叠"有两种形态这件事
|
||||
(同一组件叠两次 / 套在另一层玻璃的子树里),所以 §2 的形状在这些规则里反复出现。
|
||||
|
||||
**推论**:同一条规则里出现 `[\s\S]{0,N}` 时先问一句"N 是怎么来的"。答不出数字来源的,
|
||||
就是窗口式判据,换成"取出这条规则的 `{...}` 体再断言"。
|
||||
|
||||
**已知存量**(pi 2026-09-14 点名,**WebUI 侧归他**,别人不要顺手改):
|
||||
`client/electron/test/background.test.mjs` 里还有一批 `\{[\s\S]{0,80}…`、`{0,160}` 窗口。
|
||||
下次碰那个文件时按本节形状改成"取规则体",不要在那里再加一条注释算了。
|
||||
|
||||
## 2. 合成的清单 + 空 allow-list:条目要结构化
|
||||
|
||||
"清单从 SDK/参考实现生成 + allow-list 目前为空"是个好形状,但它**迟早被侵蚀**:
|
||||
上游升版会往清单里加新条目 → 某天早上套件突然红,且红在与本次改动无关的代码上。
|
||||
这时人的第一反应是把名字塞进 allow-list —— 而 allow-list 一旦这么用,
|
||||
就不再是"研究过的例外",只是"红的止痛药"。
|
||||
|
||||
所以条目写成三元组,并断言后两项非空:
|
||||
|
||||
```js
|
||||
const ALLOW = [ /* { name, replacement, why } */ ];
|
||||
```
|
||||
|
||||
- `replacement` 必须非空:**"暂时不想改"不是放行理由**,"替代品要求的 API level 高于本机基线"才是;
|
||||
- **不要断言"名单必须为空"**(那会挡住合理放行);要断言的是"有名字、没替代品 → 红"。
|
||||
这样侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
|
||||
|
||||
同一个形状的实例:`cross-client-theme.test.mjs` 的 `SELF_OWNED_COLORS`(手写色登记表,
|
||||
每条带理由)、`GLASS_REGISTRY`(玻璃面登记,每条带"为什么这里要玻璃")。
|
||||
|
||||
## 3. 判据必须能红,而且红的地方要对
|
||||
|
||||
每条新判据配一次**变异验证**:把源码改成"错的样子",确认它红,并且红在那条上。
|
||||
变异没红有两种可能,都要查清:一是判据没覆盖,二是**变异没真的生效**
|
||||
(本仓真发生过:变异脚本的锚点不匹配、缩进不对,于是"变异后依然全绿"被当成判据有效)。
|
||||
|
||||
顺带:变异后**不要用 `git checkout` 还原**(会连同未提交的改动一起抹掉)。
|
||||
先 `cp` 到备份,再从事先的备份还原。
|
||||
|
||||
## 4. 读源码断言的两种模式,别混用
|
||||
|
||||
- 断**代码行为**:读**剥掉注释**的源码(注释里出现的调用不是调用);
|
||||
- 断**"理由写清了没"**:读**原文**(理由就在注释里)。
|
||||
本仓真踩过:用剥离注释的读取去断"注释里写了为什么用 Canvas",永远红。
|
||||
|
||||
## 5. 取片段按行/按块,别用偏移算术两头夹
|
||||
|
||||
`src.indexOf('build() {')` 会撞上文件里更早的同名成员;偏移差一个字符会把最后一行拦腰截断
|
||||
(现象是"这行只剩 57 个字符"这种看着像文案、其实像切片的怪事)。
|
||||
取"某个成员的正文"要按行扫到下一个同级成员,并加一条自检断言切片没跨到别的成员上。
|
||||
|
||||
## 6. 判据要接线,扫描范围要有下限
|
||||
|
||||
- 新增判据文件必须进 `test/run-all.mjs` 的 `SUITE`(run-all 的**自检 2** 会直接红);
|
||||
- 扫目录的判据要断言"**至少扫到 N 个文件**",并列出必须包含的子目录
|
||||
(`pages/` `common/` `model/` `api/` …)—— 目录改名/只扫一个子目录会让判据变成
|
||||
**空判据而依然全绿**。
|
||||
|
||||
- **验证要按真实入口跑**:`npm test` 走的是 `node test/run-all.mjs && vitest run`。
|
||||
用 `node --test test/run-all.mjs` 去验时,runner 只报"0 个测试"、**把 runner 内部的
|
||||
`process.exit(1)` 吞掉**,于是"变异后依然 exit 0"看起来像判据失效(本仓刚踩过这一次:
|
||||
自检 3 其实是好的,是我用错了入口去验它)。**验判据要模拟用户/CI 真正跑的那一行。**
|
||||
|
||||
## 7. 判据要钉用户真正会点的那一层
|
||||
|
||||
(移交信里交代的头号纪律)判据通过了但用户点不到,等于没做。
|
||||
所以断言尽量落在"用户会触发的那个入口/那条路径"上:
|
||||
例如"点同意/拒绝后列表要变"要钉到那条链路上,而不是钉"某函数存在"。
|
||||
@ -248,11 +248,29 @@ test('废弃 API:清单**从 SDK 生成**,源码里不得调用(新增一
|
||||
// 全局 showToast 不是 `declare function`(它是命名空间成员),由 harmony-logic 那条单钉
|
||||
|
||||
/**
|
||||
* 允许的例外:**每条都要写理由**。
|
||||
* 空名单就是"一处都不许"—— 想加就得在这里写明为什么非用不可。
|
||||
* 允许的例外:**每条都要写"替代品"和"为什么现在不能换"**。
|
||||
*
|
||||
* pi 指出这套组合(生成的清单 + 空 allow-list)有个结构性风险:
|
||||
* SDK 升版会往清单里加新条目 → 某天早上套件**突然红**,且红在与本次改动无关的代码上;
|
||||
* 这时第一反应是把名字塞进 allow-list,而 allow-list 一旦这么用,
|
||||
* 就不再是"研究过的例外",只是"红的止痛药"。
|
||||
*
|
||||
* 所以条目结构化成 `{ name, replacement, why }`,并断言 `replacement` **非空**:
|
||||
* "暂时不想改"不是放行理由,"替代品要求的 API level 高于本机基线"才是。
|
||||
* **刻意不断言"名单必须为空"** —— 那会挡住合理放行;但"有名字、没替代品"必须红,
|
||||
* 这样侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
|
||||
*/
|
||||
const ALLOW = [];
|
||||
|
||||
// 名单形状自检:每条都必须有替代品与理由(防止以后有人只塞个名字进来)
|
||||
for (const entry of ALLOW) {
|
||||
assert.ok(typeof entry.name === 'string' && entry.name.length > 0, 'allow-list 条目要有 name');
|
||||
assert.ok(typeof entry.replacement === 'string' && entry.replacement.length > 0,
|
||||
`allow-list 里 ${entry.name} 没写"替代品" —— 没有替代品的放行不是例外,是止痛药`);
|
||||
assert.ok(typeof entry.why === 'string' && entry.why.length > 0,
|
||||
`allow-list 里 ${entry.name} 没写"为什么现在不能换"`);
|
||||
}
|
||||
|
||||
const walk = (dir, acc = []) => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, e.name);
|
||||
@ -261,7 +279,22 @@ test('废弃 API:清单**从 SDK 生成**,源码里不得调用(新增一
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
/*
|
||||
* 扫描范围 = **整个 ets 目录递归**(`pages/` `common/` `model/` `api/` `entryability/` …),
|
||||
* 不是只扫 `pages/` —— pi 点过这条:只扫页面的话 `common/` 里的旧写法会逃掉,
|
||||
* 而"新代码照抄旧模块"这条路径最常发生在 `common/`。
|
||||
*
|
||||
* "扫到的文件数 ≥ N"这条自检是防它**悄悄退化**(目录改名、遍历写错、
|
||||
* 只扫了一个子目录都会让这条判据变成空判据而依然全绿)——
|
||||
* 与 `cross-client-theme` 里给 pages 加的那条同形。
|
||||
*/
|
||||
const files = walk(ETS_DIR);
|
||||
const scannedDirs = new Set(files.map(f => f.slice(ETS_DIR.length + 1).split('/')[0]));
|
||||
assert.ok(files.length >= 20,
|
||||
`这条判据要扫到整个 ets 目录(至少 20 个文件),实际 ${files.length} 个 —— 扫描范围退化了`);
|
||||
for (const dir of ['pages', 'common', 'model', 'api']) {
|
||||
assert.ok(scannedDirs.has(dir), `扫描范围要包含 ${dir}/(否则那里的旧写法会逃掉)`);
|
||||
}
|
||||
const hits = [];
|
||||
for (const f of files) {
|
||||
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
* 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';
|
||||
@ -31,6 +35,27 @@ const ROOT = join(HERE, '..');
|
||||
* `--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', []],
|
||||
|
||||
@ -737,6 +737,20 @@ WebUI 的"壁纸模糊度(px)"在鸿蒙变成了"**材质档次**"——**
|
||||
前者是给 CSS 图层用的半径,后者是系统材质的档位(Thin/Regular/Thick)。
|
||||
映射在 `model/Appearance.ts` 的 `blurStyleFor()`,判据与 SDK 的 `BlurStyle` 成员比对。
|
||||
|
||||
### 7.19a 判据规范:`client/electron/test/CRITERIA.md`
|
||||
|
||||
pi 指出"邻接不是结构"这条已经在同一个仓库露头**三次**(① 窗口式正则被一行注释挤爆;
|
||||
② 括号配对修掉它;③ 链式修饰符让"看前一个字符"静默失效),共同形式是
|
||||
**判结构要配对/解析,看邻接或固定宽度都会被合法写法绕过** —— 比记具体招式有用,
|
||||
所以升格成仓库级的判据规范(`client/electron/test/CRITERIA.md`),并在 `run-all.mjs` 里加了
|
||||
**自检 3**:规范文件必须在、且必须点到那几条关键规则(规则只活在脑子里等于没有)。
|
||||
|
||||
规范里现在有七条:配对/解析而非邻接(含 WebUI 侧 `background.test.mjs` 的存量,
|
||||
**那是 pi 的地盘,记着不动**)、生成清单 + 结构化 allow-list、变异验证(含"变异没生效"
|
||||
这个反向陷阱)、剥注释读代码 vs 读原文读理由、按行取片段、判据要接线 + 扫描范围要有下限、
|
||||
判据要点用户真正会点的那一层。另外补了一条本仓刚踩的:**验证要按真实入口跑**
|
||||
(`node --test test/run-all.mjs` 会把 runner 内部的 `process.exit(1)` 吞掉)。
|
||||
|
||||
### 7.19 两条跨端约定(pi 2026-09-14 复核后确认)
|
||||
|
||||
- **`Theme.` 的成员名不改**(pi 三条理由:判据钉的是"值来自系统 + 品牌色仍手写",
|
||||
|
||||
Reference in New Issue
Block a user