Files
MailUI4Agents/plugins/pi-mail-bridge/test/lib/session-fixtures.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

62 lines
2.8 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.

/**
* 会话文件的测试夹具(**不是测试模块**)。
*
* # 一、为什么放在 `lib/` 而不是 `test/` 里
*
* `node --test` 默认**每个文件一个子进程**,但 `import` 是进程内的。所以
* `env-guard.test.mjs` 里 `import { writeSession } from './session-scan.test.mjs'`
* 会把**整个 `session-scan.test.mjs` 的用例在那个进程里再注册一遍** ——
* 2026-09-14 实测确认:巨行用例(单条写 ~12 MiB)在 TAP 里出现**两次**
* (`ok 87` 与 `ok 353`),也就是那条用例被跑了两次、往临时目录写了两次。
* 这台机器的 `/tmp` 正是被这次事件的主角(tmpfs 满)—— 判据自己加倍压它是自伤。
*
* 这跟 `translateEnvError` 被抽到 `test/lib/env-error.mjs` 是同一个手法、同一个理由:
* **可被别的模块引用,而不被注册进套件。**
*
* # 二、写点必须全部被兜住
*
* `session-scan.test.mjs` 的第一版只把 `writeFileSync` 包在 try 里,
* 而 `mkdirSync` 在 try 之外 —— 目录建不出来时(ENOSPC 也可能从这里出来)
* 抛的仍是原始英文堆栈。所以这里**整段**包一层,与 `selfCheck()` 那条同一个形状:
* **覆盖范围不能取决于"我以为的入口/哪一行"**。
*
* ENOSPC 一律翻译成"环境不足"(`test/lib/env-error.mjs`)—— 不管套件是怎么被调起来的
* (`npm test` 会先跑 `test/env-preflight.mjs`,但 `node --test 'test/*.test.mjs'`
* 会绕过它,那就只剩这一道)。
*/
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { translateEnvError } from './env-error.mjs';
/**
* 写一条会话文件。`lines` 是 header 之后的行(对象,会被 JSON 化)。
*
* `write` 只为测试存在(默认 `writeFileSync`):让"ENOSPC 被翻译"这件事
* 能被**行为**判据验证 —— 喂一个必然 ENOSPC 的假写,而不是去 grep 源码文本
* (注释里本来就有 "ENOSPC" 这个词,文本判据会被注释满足)。
*
* @param {string} root 根目录
* @param {string} cwdSlug 该 cwd 的子目录名
* @param {string} fileName 文件名
* @param {object} header 会话 header
* @param {object[]} lines 后续行
* @param {(file: string, body: string) => void} [write] 写实现(测试用接缝)
* @returns {string} 写出的文件路径
*/
export function writeSession(root, cwdSlug, fileName, header, lines = [], write = writeFileSync) {
const dir = join(root, cwdSlug);
const file = join(dir, fileName);
const body = [JSON.stringify({ type: 'session', version: 3, ...header })]
.concat(lines.map((l) => JSON.stringify(l)))
.join('\n');
try {
mkdirSync(dir, { recursive: true });
write(file, `${body}\n`);
} catch (e) {
throw translateEnvError(e).error;
}
return file;
}