Files
MailUI4Agents/plugins/pi-mail-bridge/test/env-preflight.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

100 lines
4.1 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.

#!/usr/bin/env node
/**
* 测试前置自检:**临时目录还有空间吗**。
*
* 名字**不带 `.test.`** —— 所以它不会被 `node --test 'test/*.test.mjs'` 收进去
* 当用例跑;它由 `package.json` 的 `test` 脚本**先**跑一次。
*
* # 它在挡什么
*
* 2026-09-14 实测:`/tmp` 是 tmpfs,被别的项目/别的 agent 的东西占满,
* `bavail` 只剩 0.70 MiB。此时 `npm test` 会红一条
* `★巨大的 message 行不进内存也不影响解析`(`ENOSPC`)—— 那条红**长得像内存缺陷**,
* 而真相是环境不足。这个脚本把"环境不足"在**跑测试之前**就大声说出来。
*
* # 退出码约定(与 `deploy/redeploy-plugin.sh` 同一套)
*
* 0 = 环境够用,继续跑测试
* 2 = **环境问题**(不是断言失败)
*
* 写清 `2` 的意义很关键:CI 或人看到 `1` 会去查代码,看到 `2` 才知道去查机器。
*/
import { tmpdir } from 'node:os';
import { judgeSpace, measureAvailBytes, MIN_FREE_BYTES } from '../lib/tmp-space.mjs';
/*
* 两个**只为测试存在**的开关(`src/pool.mjs` 的 `workerPath` 是同一手法)。
*
* 为什么需要:端到端那条判据要验"不足 ⇒ exit 2",而本机 `/tmp` 现在恰好是满的。
* 靠"机器恰好是满的"来验,等于把判据绑在一个**会变**的环境上 —— `/tmp` 一被清空,
* 那条判据就自动跳过、无声失效。开关让同一个行为在任何机器上都验得了。
*
* --measure=<dir> 只量并打印 JSON(`{"dir":…,"availBytes":…}`),总是 exit 0
* --inject-avail=<n> 绕过测量,直接按 `n` 字节判定(`null` 表示"没测到")
*/
const argOf = (name) => {
const hit = process.argv.find((a) => a.startsWith(`--${name}=`));
return hit ? hit.slice(name.length + 3) : undefined;
};
/** 未知参数一律拒绝(exit 2,与"环境/参数问题"同义)。
* 静默忽略未知参数会让 `--inject-avail=abc` 这类笔误退化成"没测到 ⇒ 放行" ——
* 在这条链上就等于**悄悄跳过守卫**。拼错参数必须炸,不能忍。 */
const KNOWN = ['--measure', '--inject-avail'];
for (const a of process.argv.slice(2)) {
if (!KNOWN.some((k) => a.startsWith(`${k}=`))) {
console.error(`[env-preflight] 不认识这个参数:${a}\n用法:--measure=<dir> | --inject-avail=<字节数|null>`);
process.exit(2);
}
}
const measureDir = argOf('measure');
if (measureDir !== undefined) {
console.log(JSON.stringify({ dir: measureDir, availBytes: measureAvailBytes(measureDir) }));
process.exit(0);
}
const injected = argOf('inject-avail');
const dir = tmpdir();
let availBytes;
if (injected === undefined) {
availBytes = measureAvailBytes(dir);
} else if (injected === 'null') {
availBytes = null;
} else {
availBytes = Number(injected);
// `Number('abc')` = NaN ⇒ 判据会当成"没测到"而**放行**。笔误在这条链上等于跳过守卫,
// 所以非法值按参数错误处理(exit 2),不给它静默放行的机会。
if (!Number.isFinite(availBytes) || availBytes < 0) {
console.error(`[env-preflight] --inject-avail 的值不合法:${injected}(要字节数或 null)`);
process.exit(2);
}
}
const verdict = judgeSpace({ availBytes });
if (verdict.ok) {
console.log(`[env-preflight] 临时目录 ${dir}:${verdict.note}`);
process.exit(0);
}
console.error(`
[env-preflight] 环境不足:临时目录写不下,**这是环境不足,不是断言失败**。
目录 ${dir}
可用 ${(availBytes / 1048576).toFixed(1)} MiB
需要 ${(MIN_FREE_BYTES / 1048576).toFixed(1)} MiB
判定 ${verdict.note}
测试用例会往临时目录写十几 MiB 的文件。空间不够时它报的是 ENOSPC,
而那看起来像代码缺陷 —— 曾经就是这么误导过一次。
药方(任选其一):
TMPDIR=<有空间的目录> npm test # 换临时目录,最省事
清理 ${dir} 下不属于你的东西之前先问主人 # 别误删别人的缓存
退出码 2 = 环境问题(与 deploy/redeploy-plugin.sh 的 2 同义),不是代码问题。
`);
process.exit(2);