From e49f0a8f6a7e522b3f21e19e5724f51e64386d37 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 20:00:52 +0800 Subject: [PATCH] =?UTF-8?q?docs(dev-tooling):=20=E5=86=99=E7=82=B9?= =?UTF-8?q?=E4=B8=89=E5=A4=84**=E4=B8=8D=E5=90=8C=E7=9A=84=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E5=81=87=E8=AE=BE**=E3=80=81=E8=BF=87=E5=AE=BD?= =?UTF-8?q?=E8=BF=87=E7=AA=84=E3=80=81=E5=9B=A0=E6=9E=9C=E6=97=A0=E5=85=B3?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E3=80=81=E9=94=9A=E7=82=B9=E3=80=81lib/=20?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=20+=20=E4=B8=80=E6=9D=A1=E8=A2=AB=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E7=BA=A0=E6=AD=A3=E7=9A=84=E8=A7=82=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 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` + 推导出的 "失效时刻")。教训写成"先查完'是否已由另一层按设计处理掉'再下结论"—— 这类误判会以"我发现了新问题"的语气传播,比沉默更贵。 --- docs/DEV-TOOLING.md | 51 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/docs/DEV-TOOLING.md b/docs/DEV-TOOLING.md index cd23bbe..94d76e5 100644 --- a/docs/DEV-TOOLING.md +++ b/docs/DEV-TOOLING.md @@ -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) - <名字>` + (结果行)是对的;锚"名字出现过"是错的 —— 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 形状相同的现象时,先把"它是否已经由另一层按设计处理掉了" +查完再下结论 —— 否则会把一个正常状态写成缺陷,而这类误判会以"我发现了新问题"的 +语气传播出去,比沉默更贵。