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 条全过。
115 lines
6.5 KiB
JavaScript
115 lines
6.5 KiB
JavaScript
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/, '前置自检也必须还在');
|
||
});
|