Files
MailUI4Agents/plugins/pi-mail-bridge/test/lib/env-error.mjs
JianFeeeee fb85a8728d refactor(pi-bridge): 定下 lib/ 与 test/lib/ 的边界 —— 三个测试侧模块原来会随部署进 /opt
pi 复核后指出:`lib/` 会被 `cp -a "$SRC/." "$STAGING/"` **整份打进生产快照**
(排除清单只有 `test/`、`.git`、`node_modules/.cache`),而我们那三个测试侧模块
(`tmp-space.mjs`、`env-error.mjs`、`session-fixtures.mjs`)都住在 `lib/` 里。
后果不是几 KB,而是"漂移 N 处"这个数字**虚高**、哈希清单变长 ——
而"手抄哈希清单"正是我们刚定性为会过期的东西。

## 规则写成**可判定的**,不写成约定

    lib/      = 从生产入口可达的模块(会进快照)
    test/lib/ = 只被测试引用的模块(test/ 不部署、也不被注册进套件)

`test/lib/reach.mjs` 真去走一遍 import 闭包(种子 = `src/index.mjs` +
源码里 `new URL('./x.mjs', import.meta.url)` 这类**按路径 fork 的子进程入口**)。

★ 顺带纠正 pi 的规则表述:他写的是"被 `src/` import",但实测 22 个 `lib/` 模块里
有 4 个 `src` **直接**引用数是 0 —— `addressing.js`(被 `lib/inbox-format.js` 引)、
`user-question.js`(走前缀动态 import)、`mail-session-id.js`、`crash-notify.mjs`。
**直接引用数不是可达性**,所以判据真走图而不是 grep。
★ 也纠正他的排除清单名字:脚本里没有 `EXCLUDE_DIRS` 这个变量,就是一条 `rm -rf`。

## 本规则多抓到一个 pi 没发现的

`lib/user-question.js` 也是**只被测试引用**(只有 `test/user-question.test.mjs` 用它)
⇒ 同样会进快照。已一并移到 `test/lib/`。剩下 `mail-session-id.js` 与
`crash-notify.mjs` 是**谁都不用**(生产与测试都不可达)—— 那是遗留物,
不动它们(不属本次范围),但记录在此。

## 新增:因果**无关**的运行期判据

`test/lib/run-suite.mjs`:跑套件并从**同一次运行的 TAP**里数结果行,任何用例名
出现两次就红。为什么需要:静态那条(测试文件不许互相 import)只能发现**已知成因**。
实测跨文件重名**不会被 runner 拦**:两个文件各写一个同名用例 ⇒
`# tests 2 / # pass 2 / # fail 0`,两句 `ok`,零警告。

判据锚在 `^(ok|not ok) <n> - <名字>`(**结果行**),不是"名字出现过"——
pi 先前那条 `grep -c '<名字>'` 给 4 是因为 TAP 里名字既出现在 `# Subtest:` 头、
又出现在结果行,**2 倍效应 + 2 倍噪声恰好同值**,若行种类是 3 就会把两次读成三次。
本脚本自带 `--self-check`(干净样本放行 / 重复样本点名 / 只出现在头里的不算重复 /
名字含 `#` 不被截断)。

`package.json` 的 `test` 改为:
    node test/lib/env-preflight.mjs && node test/lib/run-suite.mjs

## 判据全进套件

`test/layout-boundaries.test.mjs`(新):生产可达性不碰 `test/`、`test/lib/` 里不许藏
运行时模块、测试文件不许互相 import、`npm test` 必须接上 run-suite 那一层。
原来放在 `env-guard.test.mjs` 里那条"夹具不在测试文件里"已移到这里(集中边界判据)。

## 变异自检(两条都实测红了才留下)

- 造一个与巨行用例**同名**的探针文件 ⇒ `npm test` exit 1 并点名
  `2× ★巨大的 message 行不进内存也不影响解析`;
- 往 `src/gateway.mjs` 加一行指向 `test/lib/run-suite.mjs` 的真 import ⇒
  边界判据红并指出 `生产可达了测试代码:test/lib/run-suite.mjs`。
  两条探针均已删除、`src/gateway.mjs` 用 `git checkout` 还原并 `cmp` 校验一致。

顺带修一处路径:`env-guard.test.mjs` 里 `PREFLIGHT` 仍指向旧的 `test/env-preflight.mjs`
(前置脚本已移入 `test/lib/`)。

验证:`npm test` **462/462**、结果行重复检查 0 个重名、set 全绿。
2026-09-14 20:00:16 +08:00

56 lines
3.0 KiB
JavaScript
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.

/**
* 「临时目录写不进去」⇒ 人话。**纯函数,可被反面样本喂**。
*
* # 为什么它必须是一个函数,而不是散在写点里的 try/catch
*
* 2026-09-14 实测:`/tmp` 是满的 tmpfs,`bavail` 一度真是 **0**。此时这条
* `npm test` 红的是
*
* not ok 323 - ★巨大的 message 行不进内存也不影响解析
* error: 'ENOSPC: no space left on device, write'
*
* 那条红的**形状指向内存**(用例名里就写着"不进内存",而它恰好是往临时目录
* 写文件的用例)⇒ 下一个踩到的人会去 `session-scan.mjs` 找一个**不存在**的
* 内存缺陷。翻译成人话("这是环境问题,不是内存缺陷")就治这个。
*
* # 为什么抽出来(只有一个调用点也值得抽)
*
* **不是为了复用,是为了可被反面样本喂**:`translateEnvError` 能被直接喂一个
* 构造出来的 ENOSPC 错误,验"该翻译的翻译了、不该翻译的原样返回"。
* 反例:第一版把这个判断留在 `session-scan.test.mjs` 里,判据只能靠**读源码文本**
* (断言文件里出现 `/ENOSPC/`)—— 而那段解释性注释里本来就有 "ENOSPC" 这个词,
* 于是**删掉整个翻译逻辑、只留注释,判据照样绿**。这正是
* `permission-note.test.mjs` 警告过的"钉装饰不钉机制"。
*
* # 为什么不去和 `deploy/check-deploy-drift.mjs` 的 `describeEnvError` 合并
*
* 判据相同、措辞不同,看起来该合并 —— 但不合:**`deploy/` 的独立性比去重值钱**
* (那份文件头整段在讲"服务不该依赖仓库是否存在",`deploy/` 下的工具同理,
* 让它 import 插件目录里的模块等于把它绑死在插件的目录结构上)。
* **第三份拷贝出现时再考虑共用。**
*
* # 变异自检的纪律(这条是踩出来的)
*
* 验证本函数"真的被接上"要注入变异(删掉调用、看判据红不红)。做这件事时:
* **先证明你能撤回来,再注入**;而且**还原路径不能依赖被测对象** ——
* 2026-09-14 那次把备份写进 `/tmp`(正是当时被占满的那个资源),备份没写成,
* 变异却已经覆盖了源文件。现在只对"已在 HEAD 里干净提交"的文件做变异,
* 还原一律走 `git checkout HEAD -- <file>`。
*
* @param {unknown} e 捕获到的错误
* @returns {{ translated: boolean, error: Error }} 翻译过的新错误,或原样返回
*/
export function translateEnvError(e) {
const message = e && typeof e.message === 'string' ? e.message : String(e ?? '');
const isEnospc = (e && e.code === 'ENOSPC') || /no space left on device/i.test(message);
if (!isEnospc) return { translated: false, error: e };
const err = new Error(
'环境不足:临时目录写不进去(ENOSPC)—— 这是环境问题,不是内存缺陷。' +
'药方:TMPDIR=<有空间的目录> npm test'
);
err.code = 'ENOSPC';
err.cause = e;
return { translated: true, error: err };
}