Files
MailUI4Agents/plugins/pi-mail-bridge/lib/env-error.mjs
JianFeeeee 87359588eb fix(pi-bridge): 评审第二轮 —— 判据在健康机器上会退化、"一处覆盖"取决于入口、笔误参数静默放行
pi 读了 `5bc579f` 之后报了两条新的 + 三条小的,全部认下并落地。

## 一、"开关真的被认"那条判据在 /tmp 被清空后失去分辨力

上一版只在"真实测量不足"那个分支里断言(注入大数必须放行)。问题是:
**"不足"正是机器恢复健康后会消失的条件** —— 那天这条判据就退化成"只验
`--measure` 可用"的弱检查,而它守的恰恰是"开关别静默失效"。

两个方向是对偶的、各守一个机器状态,所以改成**按实测分叉、在两个分支里断言相反的方向**:

    真实不足 ⇒ 注入大数必须放行   (开关被忽略则回退测量 ⇒ 2 ≠ 0 ⇒ 红)
    真实充足 ⇒ 注入 0    必须 exit 2(开关被忽略则回退测量 ⇒ 0 ≠ 2 ⇒ 红)

量不到就 `assert.fail` 并说明"无法分叉"—— 不静默跳过(跳过会把"失去分辨力"
伪装成"验过了")。另把"端到端"那条的两个方向拆明白:只验"不足⇒2"时,
一个恒报不足的坏守卫也能绿。

## 二、"一处覆盖全部写点"成立的前提是"从 main() 进来"

`selfCheck()` 是**导出**的(用途就是被直接调),而兜住那三处裸写的 catch 在
`main()` 里 ⇒ 任何绕过 `main()` 的调用者撞上 ENOSPC 拿到的仍是原始英文堆栈。
**"覆盖范围取决于我以为的入口"正是这一串 bug 的共同病根**,所以把整段包一层
(`body()` + 统一 catch):与入口无关,`main()` 那个退化为冗余的第二道。
实测:`TMPDIR=/tmp node -e 'import("./deploy/check-deploy-drift.mjs").then(m=>m.selfCheck())'`
现在拿到的是「环境不足…这是环境问题,不是检查器的问题」。

## 三、`--inject-avail=abc` 静默放行(笔误 = 跳过守卫)

`Number('abc')` = NaN ⇒ 判据当"没测到" ⇒ 放行。现在按仓库约定处理:
**非法值 exit 2,未知参数也 exit 2**(`--measure` 少写 `=` 同样炸)。
`null` 仍是合法值("没测到 ⇒ 放行"是有意的),加了判据把这两个方向都钉住。

## 四、三条小的

- 两份实现(`lib/env-error.mjs` 的 `translateEnvError` 与 `deploy/` 的
  `describeEnvError`)**不去重**,但两边各写一句"为什么不复用":
  `deploy/` 的独立性比去重值钱(那份文件头整段在讲"服务不该依赖仓库是否存在")。
  并写明**第三份拷贝出现时再考虑共用**。
- 写点计数口径写进注释:本函数 **6 处写** = `mkdtempSync`×2 + `mk()` 内 ×2
  + 三处裸写。免得与别处"五处"的说法对不上(上一封信里两个实测数字就是这么被误读的)。
- 变异自检的纪律补进 `lib/env-error.mjs` 头注释:**先证明能撤回来再注入变异,
  且还原路径不能依赖被测对象**(那次把备份写进 `/tmp` —— 正是当时被占满的资源,
  备份没写成而变异已覆盖源文件)。现在只对"已在 HEAD 干净提交"的文件做变异,
  还原一律 `git checkout HEAD -- <file>`。

验证:`npm test` **475/475**;`--self-check` 18 条全过;
`TMPDIR=/tmp node deploy/check-deploy-drift.mjs --self-check` ⇒ exit 2 + 人话。
2026-09-14 19:42:58 +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 };
}