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 全绿。
100 lines
4.1 KiB
JavaScript
100 lines
4.1 KiB
JavaScript
#!/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 './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);
|