Files
MailUI4Agents/plugins/pi-mail-bridge/test/layout-boundaries.test.mjs
JianFeeeee d616582e96 fix: 回滚 user-question.js 那一搬(它把 check-shared-libs 打红两处),并把 drift 的非运行时差异摘出来
pi 逐处对文件后指出:我按"本平台不可达 ⇒ 搬去 test/lib/"把 `lib/user-question.js`
搬走,打红了 `deploy/check-shared-libs.sh` 两处(实测确认,脚本真退出码 1):

    共用模块缺失:plugins/pi-mail-bridge/lib/user-question.js
    共用测试已分叉:test/user-question.test.mjs(opencode vs pi)

根因不是取舍而是口径:**`lib/` 上挂着两条方向相反的不变量** ——
① 共用模块四方逐字节同源(`check-shared-libs.sh`,连相对路径一起钉);
② 本平台生产可达(我新加的规则)。而 `user-question.js` **是 dsh 桥的生产代码**
(`plugins/dsh-mail-bridge/src/index.ts` 引用它)⇒ 两条必然冲突。
**`lib/` 首先是四桥共用命名空间,其次才是"本平台可达"**;可达性只能当**报告**,
不能当搬家判据。教训的形状:**一条新判据上线时,先找它可能与哪些既有不变量冲突** ——
我只看⻅了自己那条。

改动:
- `user-question.js` 与它的测试回到 `lib/`、`test/`(路径也与 dsh 侧一致),
  两边逐字节相同已复验;`check-shared-libs.sh` 退出码 0。
- `reach.mjs` 增加 `sharedLibNames()`:直接从 `check-shared-libs.sh` 的 `ALL_LIBS`
  读共用清单做豁免(不手抄常量),并把"进快照但本平台不可达"降级为**报告**。
- `layout-boundaries.test.mjs` 增加回归判据:共用模块必须留在 `lib/`、
  测试相对路径与 dsh 一致、两侧逐字节相同。
- 删掉 `reach.mjs` / `docs/DEV-TOOLING.md` 里那句**无据的机制说明**
  ("user-question 走前缀动态 import"):`localRefs` 的三条正则只认引号字面量,
  对模板字面量形状是**盲的** ⇒ 那句若为真,搬走的就是生产代码而两条判据都会绿。
  pi 读了 `src/` 下九个文件都找不到引用,我也确认是记忆偏差;理由改用 `addressing.js`
  (传递可达、`src` 直接引用数为 0)—— 它已足够证明"直接引用数不是可达性"。

顺带按 pi 的第二条建议:`deploy/check-deploy-drift.mjs` 判据 ① 把
**非运行时差异**摘出来(`jsonTestOnlyChange`,只豁免 `scripts.test` 一类字段,
只对"两边都在、仅内容不同"的文件生效)。理由:一条**永远黄、没人打算为它动手**的判据
唯一的下场是被学会忽略,那时真正的运行时漂移会被一起忽略。
⚠️ 摘的条件很窄 —— **把运行时差异误判成非运行时比恒黄更坏(那是假绿)**,
所以 `main`/`start`/`dependencies` 变了、或解析不了,一律仍算运行时;
纯函数加了六个反/正样本的判据(含三个"必须算运行时"的)。

(该文件同时有另一条会话的改动,未提交、我未触碰;本次只加了我这一段。)

验证:`npm test` 463/463;`check-shared-libs.sh` 退出码 0;`--self-check` 18 条全过。
2026-09-14 20:11:58 +08:00

115 lines
6.5 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.

import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative, resolve } from 'node:path';
import { reachable, productionSeeds, sharedLibNames } from './lib/reach.mjs';
const HERE = dirname(fileURLToPath(import.meta.url)); // …/test
const ROOT = resolve(HERE, '..');
test('★生产代码不得引用 test/ 下的文件(`lib/` 与 `test/lib/` 的边界)', () => {
// 规则写成**可判定的**形式:从生产入口(`src/index.mjs` + 按路径 fork 的子进程
// 入口)出发的 import 闭包里,不许出现 `test/` 下的任何文件。
//
// 为什么需要:`lib/tmp-space.mjs` 等三个模块只有测试用,却住在 `lib/` 里
// ⇒ `cp -a "$SRC/." "$STAGING/"`(只排掉 `test/`)把它们**打进生产快照**。
// 后果不是几 KB,而是"漂移 N 处"这个数字**虚高**、哈希清单变长 ——
// 而我们刚把"手抄哈希清单"定性为会过期的东西。
const reach = reachable(productionSeeds());
const offenders = [...reach].filter((p) => p.startsWith('test/'));
assert.deepEqual(offenders, [], `生产可达了测试代码:${offenders.join('、')}`);
// 反向:闭包必须真的走起来了(空集合也能让上面那条绿 —— 反面样本只挡一半)。
assert.ok(reach.has('src/index.mjs'), '入口必须在闭包里');
assert.ok(
reach.has('src/worker.mjs'),
'worker 是按路径 fork 的子进程入口(不是 import 进来的),漏掉它就是漏掉生产代码'
);
assert.ok([...reach].some((p) => p.startsWith('lib/')), '至少有一个 lib/ 模块可达');
});
test('★`test/lib/` 里的模块只被测试引用(别把运行时模块藏进去)', () => {
// 反向的边界:`test/lib/` 是给测试用的,若某个**生产可达**的模块住在里面,
// 那它部署后就会缺失 —— 这是个会等到重启才炸的错(白名单失败的经典形状)。
const reach = reachable(productionSeeds());
const inTestLib = readdirSync(join(HERE, 'lib'))
.filter((f) => /\.(mjs|js)$/.test(f))
.map((f) => `test/lib/${f}`);
const wrong = inTestLib.filter((p) => reach.has(p));
assert.deepEqual(wrong, [], `这些模块生产可达,不该放在 test/lib/:${wrong.join('、')}`);
});
test('★共用模块必须留在 `lib/`:我把它搬走会打红两条不变量', () => {
// 回归判据。2026-09-14 我按"本平台不可达 ⇒ 搬去 test/lib/"把
// `lib/user-question.js` 搬走了,结果:
// 共用模块缺失:plugins/pi-mail-bridge/lib/user-question.js
// 共用测试已分叉:test/user-question.test.mjs(opencode vs pi)
// 因为它在**四桥共用清单**上,而且是 **dsh 桥的生产代码**
// (`plugins/dsh-mail-bridge/src/index.ts` 引用它)。
//
// 教训的形状值得单独记:**`lib/` 上挂着两条方向相反的不变量** ——
// ① 共用模块四方逐字节同源(连相对路径一起钉);② 本平台生产可达。
// 我只看见了自己那条,于是"按我的规则推断出的正确动作"恰好破坏了另一条。
// 判据所以长成这样:对**共用清单上的**模块,"本平台不可达"不构成搬家理由。
const shared = sharedLibNames();
assert.ok(shared.has('user-question'), 'ALL_LIBS 里应当有 user-question');
// 本平台侧:文件必须在 lib/,且测试的相对路径必须与它同源。
assert.ok(
existsSync(join(ROOT, 'lib', 'user-question.js')),
'lib/user-question.js 必须留在 lib/(它是 dsh 桥的生产代码,且要四方同源)'
);
assert.ok(
!existsSync(join(HERE, 'lib', 'user-question.js')),
'不该出现在 test/lib/'
);
assert.match(
readFileSync(join(HERE, 'user-question.test.mjs'), 'utf8'),
/from '\.\.\/lib\/user-question\.js'/,
'测试的相对路径也要跟 dsh 侧一致(同源是连路径一起钉的)'
);
// 逐字节同源(对 dsh 那一侧;这是被打破的那条不变量的直接判据)。
const peer = join(ROOT, '..', 'dsh-mail-bridge');
if (existsSync(join(peer, 'lib', 'user-question.js'))) {
assert.equal(
readFileSync(join(ROOT, 'lib', 'user-question.js'), 'utf8'),
readFileSync(join(peer, 'lib', 'user-question.js'), 'utf8'),
'pi 与 dsh 的 user-question.js 必须逐字节一致'
);
}
});
test('★测试文件之间不许互相 import(会二次注册整套用例)', () => {
// `node --test` 默认每个文件一个子进程,而模块导入是**进程内**的 ⇒
// 从另一个 `.test.mjs` 取东西,会把那个文件的用例在引用者进程里**再注册一遍**。
// 实测:巨行用例(单条往临时目录写 ~12 MiB)跑了两次,测试总数 475(修完 459)。
//
// 判据形状:解析**真引用**(静态 `from` / 动态 `import()` / `require()`),
// 注释里提到文件名不算(注释不会注册用例)。
// 两版都踩过,写在注释里免得重踩:只匹配静态 from ⇒ 漏动态导入(过窄);
// "文件里出现别的测试文件名" ⇒ 把散文引用也算违规、还被自己注释里的示例点亮(过宽)。
const refRe = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*)['"]\.\/([\w.-]+\.test\.mjs)['"]/g;
const offenders = [];
for (const f of readdirSync(HERE)) {
if (!f.endsWith('.test.mjs')) continue;
const src = readFileSync(join(HERE, f), 'utf8');
for (const m of src.matchAll(refRe)) offenders.push(`${f} → ${m[1]}`);
}
assert.deepEqual(offenders, [], `测试文件之间不许互相 import:${offenders.join('、')}`);
});
test('★`npm test` 必须跑"从同一次运行的 TAP 里数重名"那一层', () => {
// 因果**无关**的判据:跨文件重名不会被 runner 拦(实测两个文件各写一个同名用例
// ⇒ `# tests 2 / # pass 2 / # fail 0`,两句 `ok`,没有任何警告)。
// 静态那条只能发现"已知成因"(互相 import),这条不挑成因。
//
// 这里只验**接线**(脚本在 package.json 里被调到、且它自带自检),
// 因为真跑一遍套件就是 `npm test` 本身在做的事 —— 在套件里再跑一次套件会递归。
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
assert.match(pkg.scripts.test, /test\/lib\/run-suite\.mjs/, 'npm test 必须走 run-suite');
assert.match(pkg.scripts.test, /test\/lib\/env-preflight\.mjs/, '前置自检也必须还在');
});