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/, '前置自检也必须还在'); });