Files
MailUI4Agents/client/electron/test/CRITERIA.md
JianFeeeee a8ac2fc28b test(criteria): 闭环——判据自报条数 + 每文件期望条数(只增不减的棘轮)
pi 指出的残余缺口:我上一轮加的自检 4 是**文本证据**(文件里有 `test(` / `check(` /
`process.exit(1)`),只能证明"**有能红的路径**",不能证明"**它跑过**"。反例很短:

```js
const check = () => {};     // 实现被换空(现实形态:合并冲突改坏实现)
check('a', false);          // 存在、也执行了,但什么都不会红
console.log('主题:通过');   // 有输出
```

## 落地(pi 给的闭环形状)

1. 自定义 `check()` 的判据结尾打一行机器可读汇总 `RESULT pass=<条数> fail=<失败数>`
   (`node:test` 的判据不用改,已有 `# pass N`);
2. `run-all.mjs` **只解析这个固定 marker**(不猜口语汇总——「窄屏布局:全部通过」里没有数字,
   按数字猜会误报,这一点我上轮已经实测过);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。

关键细节:**计数写在 `check()` 内部**(theme/background 原本就在内部 ++;
narrow-layout 只有 failed 计数,补了 passed;markdown-xss 按 payload 条数算)。
写在调用点或靠扫源码的话,"实现被换空"就看不见了。

棘轮"只增不减":加判据**不用**改那个数,只有"条数掉了"才红。期望值按**实测**回填
(9/52/8/30/42/15/28/5/23/5/3/2)。

附带的可见性收益:这几轮我一直用"13→14""19→28""34→42"当信号,现在它成了判据 ——
某次改动顺手删掉两条判据、或某条被跳过,会立刻红。

## 变异

- pi 那个反例(`check` 换成空函数)→ 红(`自报 0 条 < 登记的 30 条`);
- 删掉 5 条 `check(` 调用 → 红(`自报 47 条 < 登记的 52 条`)。

## 规范

§6.5 新增"涉及运行时行为的结论必须实测过才能写进规范/判据"——同一个错这轮犯了两次
(我从"报告 0 个测试、退出 0"推断"退出码被吞",实测是照传;pi 拿我这个结论又建了一个洞)。
规则:**一次观察只支撑你看到的那一层**。
§6.6 记闭环形状与代价(故意删判据要同步改数字,属于一次可复核的显式编辑)。

⚠️ 并且如实记下一次**我自己违反规范**的事:写 §3 那条"变异后别用 `git checkout` 还原"的人
(就是我)在这次变异验证里又用了 `git checkout -- <文件>`,把刚加、尚未提交的 marker 抹掉了。
规矩写下来不等于会遵守 —— 已把这条实例写进规范,让人知道它是活人踩的坑。

## 验证

`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258);`run-all` 单独跑也 exit 0。
2026-09-14 15:06:14 +08:00

171 lines
10 KiB
Markdown
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.

# 判据规范(写判据前先读这份)
这份文件记的是**判据本身的写法**:什么样的判据能红、能红在对的地方、以及不会在"代码完全正确"时乱红。
不是"怎么用 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` 到备份,再从事先的备份还原。
### 变异红了,还要看**红在哪**pi 2026-09-14 补的一档)
只报"红了"不算证据。反过来的那一半同样成立:**变异后红了,也可能是假红** ——
比如变异把文件写坏了语法,判据红在"模块加载失败/解析不到源码",看起来像判据生效,
实际那条断言根本没执行。所以:
- 报结果要能**指名红的是哪几条**(本仓的变异表就是按这个写的:
"红 2 条B + 裸色值)"、"红 3 条(含品牌色防线)"
- **红在解析/加载失败上不算红** —— 先让变异"语法正确、语义错",再谈判据有没有生效;
- 同理,变异**作用于注释**(被剥掉的那部分)也不算:判据读的是剥注释后的源码,
文档里的"理由"断言才读原文(见 §4
### 判据自己不会跑:一个家族,六种宿主
这条家族在本仓已经露头六次,共同点都是"**看起来全绿**"
| # | 宿主 | 形态 |
|---|---|---|
| ① | `&&` 链 | 前面红一条,后面全部不跑("红"不可信) |
| ② | 判据文件 | 没接进 `SUITE`(写好了但隐身) |
| ③ | 清单名字 | 文件名写错 = 静默跳过一条判据 |
| ④ | 文件末尾 | 判据写在 `process.exit()` **之后**(并发写入总往末尾追加) |
| ⑤ | 验证手法 | 用 `node --test` 去验 runner里面的 `process.exit(1)` 被吞(见 §6 |
| ⑥ | runner 内部 | 清单 flag 与判据写法配错 |
`run-all.mjs` 现在对 ②③④⑥ 都有静态自检,⑥ 的落地方式见下(**不要**照抄"解析 pass 计数"
⚠️ **实测过两条真实样本,结论与直觉不同**
- `node --test <自定义 check() 的判据>`**退出码照样传出来**(文件 exit 1 → 命令行 exit 1
并没有被 runner 吞掉;
-`node --test <什么都不做的文件>` 会报 `# tests 1 / # pass 1` ——
**计数不是"检查跑过"的证据**。所以"数 pass、0 就判红"既抓不到空判据(它报 1
又会在汇总行没有数字的判据上误报。
改用**结构证据**:每条判据文件里必须存在"能红"的路径(`test(` / `check(` / `process.exit(1)`
外加"跑完必须有输出"。**先看真实输出再写规则**——这一条本身就是 §1 的推论。
## 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 真正跑的那一行。**
## 6.5 涉及**运行时行为**的结论,必须实测过才能写进规范/判据pi 2026-09-14
同一个错在本仓犯了两次,方向相反但错法相同:**从观察推断机制、没跑**。
- 我从"`node --test test/run-all.mjs` 报 0 个测试、退出 0"这个**观察**,推断出
"runner 吞掉了 `process.exit(1)`" —— 实测下来退出码照传(见 §6 末),我错了;
- pi 拿我那个结论直接往下建了一个洞("配错 flag = 绿")—— 他也错了。
规则:**一次观察只支撑你看到的那一层**。"报告里 0 个测试"是真的,"退出码被吞"是推断的。
凡涉及运行时行为的结论(退出码、计数、回调时机、事件触发),
**先贴真实样本再写规则** —— 这也是 §1 的推论。
## 6.6 闭环:自报条数 + 每文件期望条数(只增不减)
文本证据只能到"**存在**"为止:`test(` / `check(` / `process.exit(1)` 的存在性
证明"有能红的路径"**不证明它跑过**。反例(现实事故形态:合并冲突把 `check` 的实现改空):
```js
const check = () => {}; // 实现被换空
check('a', false); // 存在、也执行了,但什么都不会红
console.log('主题:通过'); // 有输出
```
所以 `run-all.mjs` 现在做三件事:
1. 自定义 `check()` 的判据**自报条数**:结尾打一行
`RESULT pass=<条数> fail=<失败数>``node:test` 的判据不用改,已有 `# pass N`
2. runner **只解析这个固定 marker**(不猜口语汇总 —— 「窄屏布局:全部通过」里没有数字,
靠猜数字会误报);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。
棘轮是"只增不减"**加判据不用改那个数**;只有"条数掉了"才红 ——
那正是要看见的事(顺手删两条判据、某条被跳过、`check` 实现被改坏)。
代价:**故意删判据时要同步改数字**(这是一次显式的、能被复核的编辑,可以接受)。
计数必须写在 **`check()` 内部**:写在调用点或靠扫源码,"实现被换空"就看不见了
—— 上面那个反例的 `pass` 会是 0正是靠这一条才有分辨力。
> ⚠️ 本节的作者在写完之后**又踩了一次 §3 那条**:变异验证时用 `git checkout -- <文件>`
> 还原,把尚未提交的改动(刚加的 marker一起抹掉了。**规矩写下来不等于会遵守**
> 变异前先 `cp` 备份,从备份还原。
## 7. 判据要钉用户真正会点的那一层
(移交信里交代的头号纪律)判据通过了但用户点不到,等于没做。
所以断言尽量落在"用户会触发的那个入口/那条路径"上:
例如"点同意/拒绝后列表要变"要钉到那条链路上,而不是钉"某函数存在"。