Files
MailUI4Agents/client/electron/test/CRITERIA.md
JianFeeeee 2d8f5424b5 test(criteria): 规范补两条 + 还原纪律改写成"先固化基线再破坏"
pi 三条增量的第三条(纠正我的写法)与配套文档:

## 1 还原纪律:禁令 → 操作顺序(pi 纠正)

我原来写的是"变异后不要用 `git checkout` 还原"——**治症不治因**。真因是
**被还原到的那个状态还没提交**(我丢的是一个刚加、尚未提交的 marker)。
可执行的形状:

> **任何破坏性还原,都要求"将被还原到的那个状态已经在某个提交里"。**

所以:**变异前先把基线提交掉**;更稳就 `git worktree add` 一个干净副本去变异。
`cp` 备份仍然可用,但它依赖"人记得备份",顺序改对了则不依赖记性 ——
与"记得打 marker"改成"计数写在 `check()` 内部"是同一招。

本提交自身就是这条纪律的示范:先提交 `ec90cba`(helper + 移植 + 错误信息)作为基线,
再在已提交的基线上做 marker 变异验证。

## 2 共享 helper 与"失败信息自带修法"写进 §6.6

- `test/lib/checks.mjs` 的存在理由与用法(计数只可能在该模块内发生 →
  漏 marker / 计数写错位置在新判据上不可能发生);
- **失败信息要自带修法**:red 是那个人一定会看到的东西,文档不一定被打开。
  验证方式也记了:真删掉一条判据的 marker 行跑一遍,确认错误信息能照抄执行
  (已验:输出里给了 helper 用法与样板文件路径,且 `node:test` 的判据不用管)。

## 3 规范自检关键词 7 → 9

新增 '已经在某个提交里'、'自带修法',防止这两条被删掉还不报错。

## 验证

`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258)。
2026-09-14 15:12:19 +08:00

207 lines
12 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. 判据必须能红,而且红的地方要对
每条新判据配一次**变异验证**:把源码改成"错的样子",确认它红,并且红在那条上。
变异没红有两种可能,都要查清:一是判据没覆盖,二是**变异没真的生效**
(本仓真发生过:变异脚本的锚点不匹配、缩进不对,于是"变异后依然全绿"被当成判据有效)。
**还原纪律pi 2026-09-14 纠正了我的写法,这条更好用)**
我原来写的是"变异后不要用 `git checkout` 还原"——**那是治症不治因**。
真因是:**被还原到的那个状态还没提交**,于是 `checkout` 把未提交的改动一起抹掉了
(我丢的是一个刚加、尚未提交的 marker
可执行的形状是改**操作顺序**,而不是记一条禁令:
> **任何破坏性还原,都要求"将被还原到的那个状态已经在某个提交里"。**
- 所以:**变异前先把基线提交掉**(哪怕是个很小的提交)。之后 `git checkout -- <文件>`
只可能还原到已提交状态,不会连带抹掉未提交的东西;
- 要更稳就 `git worktree add` 一个干净副本,在副本里变异,主工作区完全不碰;
- 备份(`cp``/tmp/*.bak`)仍然可以,但它靠的是"人记得备份"——顺序改对了则不依赖记性。
这跟"记得打 marker"改成"计数写在 `check()` 内部"是同一招。
### 变异红了,还要看**红在哪**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一起抹掉了。**规矩写下来不等于会遵守** ——
> 所以 §3 已经把那条禁令改写成"**先固化基线、再破坏**"的操作顺序pi 指出真因是
> "被还原到的状态还没提交",不是 `checkout` 这个命令有罪)。
### 共享 helper`test/lib/checks.mjs`(新判据请用它)
```js
import { check, finish } from './lib/checks.mjs';
check('这条判据的名字', 条件, '失败时给人看的细节');
finish('标签'); // 打汇总 + `RESULT pass=N fail=M`,有失败则退出码 1
```
计数只可能发生在这个模块内部,所以用它以后:
**漏打 marker****计数写错位置**(导致 `check` 实现被改空也看不见)这两类
在新判据文件上**不可能发生** —— 不用再靠记性。
`markdown-xss` / `narrow-layout` 已改用它;其余 10 条保持原写法(`run-all` 的 marker 检查已覆盖)。
### 失败信息要**自带修法**pi 2026-09-14
受众不只是读过这份规范的人:并发写 WebUI 的 agent 新加判据时**不会打开这份文件**
看到套件红的第一反应很可能是"套件坏了" → 删自检或往清单里塞豁免。
所以 `run-all.mjs` 现在在"没找到自报条数""条数掉了"这两条错误里直接写出
**可照抄的修法**与样板文件路径。
> 原则:**red 是那个人一定会看到的东西,文档不一定被打开** —— 错误信息是最省成本的交接面。
> (验证方式:真删掉一条判据的 marker 行跑一遍,确认错误信息能照抄执行。)
## 7. 判据要钉用户真正会点的那一层
(移交信里交代的头号纪律)判据通过了但用户点不到,等于没做。
所以断言尽量落在"用户会触发的那个入口/那条路径"上:
例如"点同意/拒绝后列表要变"要钉到那条链路上,而不是钉"某函数存在"。