docs(dev-tooling): 写点三处**不同的入口假设**、过宽过窄、因果无关判据、锚点、lib/ 边界 + 一条被自己纠正的观察

按 pi 的两条建议补:

1. "漏过三次"改成**列出三次的位置与各自不同的入口假设**(他指出的问题:原写法
   会让读者以为"同一处错了三次"):① `writeSession` 只包 `writeFileSync`;
   ② `mk()` 的 `mkdtempSync` 在 try 之外;③ 兜底层放在 `main()` 而 `selfCheck()` 是导出的。
   三次都概括成一句:**"我以为的入口/哪一行"决定了覆盖范围**。
2. "判据只能发现已知成因"作为实例加进同节,并配对写出**因果无关**的运行期判据
   (`test/lib/run-suite.mjs` 数结果行 / 跨文件重名实测不被拦)。

新补四条(本轮新踩出来的):
- **判据过宽和过窄都是坏的**(那条 import 判据自己同时踩过两边);
- **判据的锚点必须与结论一一对应**(重名检查锚结果行 vs 锚"名字出现过",
  噪声与效应恰好同阶 ⇒ 4 看起来还能解释);
- **`lib/` 与 `test/lib/` 的边界**(部署脚本是 `cp -a` + `rm -rf test`,
  没有 `EXCLUDE_DIRS` 这种变量;★ 别写成"被 src/ 直接 import"—— 直接引用数不是可达性);
- **附一节"被自己的结论半路纠正的观察"**:那条 `result IS NULL` 的挂起请求看着像
  授权链缺口,实际是网关按设计处理的(`PermissionWaitWindow = 10min` + 推导出的
  "失效时刻")。教训写成"先查完'是否已由另一层按设计处理掉'再下结论"——
  这类误判会以"我发现了新问题"的语气传播,比沉默更贵。
This commit is contained in:
2026-09-14 20:00:52 +08:00
parent fb85a8728d
commit e49f0a8f6a

View File

@ -171,20 +171,57 @@ bash deploy/prune-deploy-artifacts.sh --self-check # 判据自检(16 项,
实测:从测试文件取夹具,让巨行用例(单条往临时目录写 ~12 MiB)跑了**两次**
(测试总数 475;修完 459)。夹具放**非测试模块**(`lib/session-fixtures.mjs`),
判据也进套件(`env-guard.test.mjs` 里那条扫描)。
- **写点要"一处覆盖全部",且覆盖范围不取决于入口。** 同一个"ENOSPC 被翻译成环境问题"
在这套代码里被漏掉过三次:只包了 5 个写点里的 1 个;`mkdtempSync` 在 try 之外;
兜底层放在 `main()` 里而**被兜的函数是导出的**。判据过宽过窄都是坏的 ——
上面"测试文件互相引用"那条判据自己就同时踩了过窄(漏动态导入)和过宽
(把注释里的散文引用也算违规),最后只能收成"解析真引用"。
- **写点要"一处覆盖全部",且覆盖范围不取决于入口。** 同一条"ENOSPC 被翻译成环境问题"
在这套代码里被漏过**三次**,而且是**三个不同的入口假设**(不是同一处错了三次):
① `test/lib/session-fixtures.mjs` 的 `writeSession`(第一版只包 `writeFileSync`,
`mkdirSync` 在 try 之外);
② `deploy/check-deploy-drift.mjs` 的 `mk()`(`mkdtempSync` 也**是**一个写点,却在 try 之外);
③ 同一个文件的 `main()` —— 兜底层放在调用方,而**被兜的 `selfCheck()` 是导出的**
⇒ 任何绕过 `main()` 的调用者拿不到翻译。
三次都可以概括成一句话:**"我以为的入口/哪一行"决定了覆盖范围**。
- **判据过宽和过窄都是坏的。** "测试文件不许互相 import"那条判据自己同时踩过两边:
过窄(只匹配静态 from,漏掉动态 `import()`)、过宽("文件里出现别的测试文件名"
把**注释里的散文引用**也算违规,还被自己注释里的示例字面量点亮)。
最终形状只能是"解析真引用"。
- **因果特定的判据 + 因果无关的判据要配对。** "测试文件互相 import"只能发现**已知成因**;
同族的另一种成因它看不见 —— 实测**跨文件同名用例不会被 runner 拦**
(两个文件各写一个同名用例 ⇒ `# tests 2 / # pass 2 / # fail 0`,零警告)。
所以补一条不挑成因的运行期判据:`test/lib/run-suite.mjs` 从**同一次运行的 TAP**
里数结果行,重名即红。
- **判据的锚点必须与结论一一对应。** 同一条重名检查,锚 `^(ok|not ok) <n> - <名字>`
(结果行)是对的;锚"名字出现过"是错的 —— TAP 里名字既出现在 `# Subtest:` 头、
又出现在结果行,**效应 2 倍、噪声也 2 倍且恰好同值**,于是"4"看起来还能解释;
若行种类是 3,就会把"两次"读成"三次"。**别让噪声与效应同阶。**
- **`lib/` 与 `test/lib/` 的边界**(2026-09-14):部署脚本是 `cp -a "$SRC/." "$STAGING/"`
加一条 `rm -rf "$STAGING/test"`(**没有** `EXCLUDE_DIRS` 这种变量)⇒ **`lib/` 整份进快照**。
于是规则必须是可判定的:`lib/` = 从**生产入口**可达的模块;只被测试引用的放 `test/lib/`。
判据在 `test/lib/reach.mjs`(真走 import 闭包,含按路径 fork 的子进程入口)
与 `test/layout-boundaries.test.mjs`。
★ 别写成"被 `src/` **直接** import":实测 22 个 `lib/` 模块里 4 个 `src` 直接引用数为 0
(`addressing.js` 被 `lib/inbox-format.js` 引、`user-question.js` 走前缀动态 import、
另两个谁都不用)—— **直接引用数不是可达性**。
### 索引(实证在各自文件头注释里,此处不复述)
- `plugins/pi-mail-bridge/lib/tmp-space.mjs` —— 测量层:`bavail × bsize`、
- `plugins/pi-mail-bridge/test/lib/tmp-space.mjs` —— 测量层:`bavail × bsize`、
**`0` 是"真的没有"而不是"不知道"**(只有 `null` 才是不知道)。
- `plugins/pi-mail-bridge/lib/env-error.mjs` —— ENOSPC ⇒ 人话;为什么它是纯函数、
- `plugins/pi-mail-bridge/test/lib/env-error.mjs` —— ENOSPC ⇒ 人话;为什么它是纯函数、
为什么**不与** `deploy/` 的实现合并(`deploy/` 的独立性比去重值钱)。
- `plugins/pi-mail-bridge/test/lib/reach.mjs` —— `lib/` 与 `test/lib/` 的边界判据。
- `deploy/check-deploy-drift.mjs` —— 现场比树(判据自己算,不靠手抄常量)+
判据自检的两侧验证 + 为什么它自带兜底层。
- `deploy/check-deploy-drift.mjs` 的运行输出本身**就是**部署状态的判据来源;
**不要**把它抄成一份哈希清单往外发 —— 抄出来的那一刻就开始过期
(2026-09-14 发给 `jianf` 的清单在两次提交后就作废了)。
### 附:一条被自己的结论"半路纠正"的观察(留作提醒)
2026-09-14 排查时看到 `permission_requests` 里有一条 `result IS NULL` 的挂起请求
(11:56 创建,已挂 3h54m),当时的推断是"worker 被重启/超时杀掉 ⇒ 停在授权上没人解除
⇒ 这是那个 bug 的同族缺口"。
**实际是正常状态**:网关的等待窗口是 `PermissionWaitWindow = 10 * time.Minute`
(`server/internal/models/permission_mode.go`),请求 12:06 就失效了,
界面靠 `AttachPermissionDeadline` 推导出的**时刻**(不是布尔快照)自己判断过期。
**教训**:看到与已知 bug 形状相同的现象时,先把"它是否已经由另一层按设计处理掉了"
查完再下结论 —— 否则会把一个正常状态写成缺陷,而这类误判会以"我发现了新问题"的
语气传播出去,比沉默更贵。