dsh 2026-09-18 在本插件里实测报的第二例(同一形状:判据存在但不在路径上)。
## 洞(实测,不是读代码)
· `package.json` 的 `npm test` = `env-preflight && run-suite`,**不带 `--self-check`**;
`deploy/install.sh` 也是 `npm test`,同样不带;全仓 `--self-check` 零引用,
本插件下**没有任何 .md** 提到它 ⇒ 它从没在真实路径上跑过。
· 实测:把 `findDuplicates` 改成恒返回 `[]` ⇒ **`npm test` 照样 `exit=0`**(带 flag 才红)。
· 后果不是小事:它是**跨文件重名**判据的引擎,而它**唯一**的分辨力判据就是这个自检
⇒ 引擎哪天退化成恒空,重名判据**安静地永远放行**,而整套测试全绿。
· ⚠️ 判"在不在路径上"**不能 `grep` 数命中**:`grep -c '判据自检'` 在默认跑里得 **16 次**,
全是若干 `.test.mjs` 的**用例名**恰好含这四个字,而 `selfCheck()` 自己的输出
(`^ (通过|失败) `、`干净样本`)**一次都没有**。**数命中 = 数到的是词,不是调用。**
## 修
1. `selfCheck()` 去掉 `process.exit`,改为**返回失败条数**(否则没法进程内调);
`--self-check` 那条 CLI 行为不变。
2. **默认路径先跑自检**:坏了就 `exit 3` 并明说"工具坏了、本次结论不可信"。
顺序排在跑套件**之前** —— 引擎坏了时套件那份结论本来就不可信,
而且省一整轮 509 条测试进程。
3. **量纲分三档**(本文件已约定的那套 + 新增一档):
`0`=全绿且无重名;`1`=断言失败/发现重名(**代码问题**);
`2`=环境;**`3`=判据引擎自己坏了(工具问题)** —— 不复用 `1`,
否则 `install.sh` 会把"引擎坏了"读成"代码有问题"。`install.sh` 本次不动
(它只判"插件装好没有",非零即中止,3 与 1 对它的行为一致;改它的退出码语义是另一件事)。
## 验证(都跑过)
· 干净树:`npm test` rc=**0**,默认路径出现 `判据自检:全部通过。`(此前 0 次)。
· **M**:`findDuplicates → []` ⇒ `npm test` rc=**3** + `判据自检 2 条失败 —— findDuplicates 的分辨力坏了…`。
· **反向对照**:真造一个跨文件重名 ⇒ rc=**1** + 点名 `2× 平台名字正常时派生别名并原样带标题`
⇒ 两个量纲**确实分开**,没把"代码问题"和"工具问题"混成一个码。
· `node --check` 通过;`--self-check` 单独跑 rc=0(行为不变)。
(补 dsh 交回的实测读数:`--self-check` 在干净树 4/4 通过、变异后 2/4 失败、默认跑 0 次。)
219 lines
9.9 KiB
JavaScript
219 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 跑套件,并从**同一次运行的 TAP 输出**里数结果行 —— 任何用例名出现两次就红。
|
||
*
|
||
* # 为什么需要它:这是一条**因果无关**的判据
|
||
*
|
||
* `test/env-guard.test.mjs` 里那条"测试文件之间不许互相 import"只能发现**已知的
|
||
* 那个成因**。同族的另一种成因它看不见,比如**两个文件里恰好同名**的用例 ——
|
||
* `node --test` 按文件注册,跨文件重名**不会被拦**。2026-09-14 实测确认:
|
||
*
|
||
* 两个文件各写一个 `test('同名用例')` ⇒ `# tests 2 / # pass 2 / # fail 0`
|
||
* (两条 `ok`,没有任何警告)
|
||
*
|
||
* 本脚本不挑成因:**只数结果行**。它同时也能抓住"二次注册"那类问题,
|
||
* 所以和那条静态判据是**互补**的(静态那条更早、能指认是哪个文件引了哪个;
|
||
* 这条更晚、但因果无关)。
|
||
*
|
||
* # 判据锚在哪一行(这条是被人纠正出来的)
|
||
*
|
||
* 锚 `^(ok|not ok) <n> - <name>` —— **结果行**。pi 先前给的一条验证命令是
|
||
* `grep -c '<用例名>'`,那个数的是**字符串出现次数**:TAP 里同一个用例名既出现在
|
||
* `# Subtest:` 头、又出现在结果行 ⇒ 2 倍效应 + 2 倍噪声,**两个因子恰好同值**,
|
||
* 于是"4"看起来还能解释。若行种类是 3,就会把"两次注册"读成"三次"。
|
||
* 规矩:**判据必须锚在与结论一一对应的那一行上**,而不是"出现过"。
|
||
*
|
||
* # 退出码(与 `env-preflight.mjs`、`redeploy-plugin.sh` 同一套)
|
||
*
|
||
* 0 = 套件全绿 **且**没有重名
|
||
* 1 = 断言失败(套件红了)**或**发现重名(这是真问题,要看代码)
|
||
* 2 = 环境/参数问题(临时目录不足等)
|
||
*
|
||
* `--self-check`:不跑套件,只用两段合成的 TAP 样本验本脚本自己
|
||
* (干净样本必须放行、重复样本必须点名)—— 这就是"判据自检",与
|
||
* `check-deploy-drift.mjs --self-check` 同一形状。
|
||
*/
|
||
|
||
import { spawnSync } from 'node:child_process';
|
||
import process from 'node:process';
|
||
|
||
/** TAP 结果行:`ok 12 - 名字` / `not ok 12 - 名字`(名字里可能有 `#` 之类,照收)。 */
|
||
const RESULT_RE = /^(not ok|ok) (\d+) - (.*)$/;
|
||
|
||
/** 只认"用例"行:跳过整份文件的汇总行(形如 `ok 1 - test/xxx.test.mjs` 也算结果行,
|
||
* 所以不能只靠这一条区分)。这里用"出现次数"来做判断,不区分文件级与用例级 —— */
|
||
export function findDuplicates(tapText) {
|
||
const counts = new Map();
|
||
for (const line of tapText.split('\n')) {
|
||
const m = RESULT_RE.exec(line);
|
||
if (!m) continue;
|
||
const name = m[3].trim();
|
||
counts.set(name, (counts.get(name) || 0) + 1);
|
||
}
|
||
return [...counts.entries()]
|
||
.filter(([, n]) => n > 1)
|
||
.map(([name, n]) => ({ name, count: n }))
|
||
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
||
}
|
||
|
||
/** 从 TAP 里取汇总:`# tests N` / `# pass N` / `# fail N`。取不到就是没跑完。 */
|
||
export function summaryOf(tapText) {
|
||
const grab = (k) => {
|
||
const m = new RegExp(`^# ${k} (\\d+)$`, 'm').exec(tapText);
|
||
return m ? Number(m[1]) : null;
|
||
};
|
||
return { tests: grab('tests'), pass: grab('pass'), fail: grab('fail') };
|
||
}
|
||
|
||
const SELF_CHECK_SAMPLES = [
|
||
{
|
||
what: '干净样本:每个名字只出现一次 → 必须放行',
|
||
tap: [
|
||
'# Subtest: 甲',
|
||
'ok 1 - 甲',
|
||
'# Subtest: 乙',
|
||
'ok 2 - 乙',
|
||
'1..2',
|
||
'# tests 2',
|
||
'# pass 2',
|
||
'# fail 0',
|
||
].join('\n'),
|
||
expect: [],
|
||
},
|
||
{
|
||
what: '★重复样本:同名两次(正是跨文件重名的形状)→ 必须点名',
|
||
tap: [
|
||
'# Subtest: 同名用例',
|
||
'ok 1 - 同名用例',
|
||
'# Subtest: 同名用例',
|
||
'ok 2 - 同名用例',
|
||
'1..2',
|
||
'# tests 2',
|
||
'# pass 2',
|
||
'# fail 0',
|
||
].join('\n'),
|
||
expect: ['同名用例'],
|
||
},
|
||
{
|
||
what: '★噪声样本:名字只出现在 `# Subtest:` 头里(没有结果行)→ 不算重复',
|
||
tap: ['# Subtest: 只在头里', '1..0', '# tests 0', '# pass 0', '# fail 0'].join('\n'),
|
||
expect: [],
|
||
},
|
||
{
|
||
what: '★名字里带 `#` 也要能取对(别把名字截断成前缀)',
|
||
tap: ['ok 1 - ★0 字节不是「不知道」', 'ok 2 - ★0 字节不是「不知道」', '1..2'].join('\n'),
|
||
expect: ['★0 字节不是「不知道」'],
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 判据自检:拿合成 TAP 样本验 `findDuplicates` 的**分辨力**。
|
||
*
|
||
* ★★ 返回失败条数,**不自己 `process.exit`** —— 这样它能在默认路径里被调用。
|
||
* 为什么必须能(pi 2026-09-18 实测报的洞):
|
||
* · `package.json` 的 `npm test` = `env-preflight && run-suite`,**不带 `--self-check`**;
|
||
* `deploy/install.sh` 也是 `npm test`,同样不带。全仓 `--self-check` 零引用,
|
||
* 且本插件下**没有任何 .md** 提到它。
|
||
* · ⇒ 它此前**一次都没在真实路径上跑过**。实测:把 `findDuplicates` 改成恒返回 `[]`
|
||
* ⇒ **`npm test` 照样 `exit=0`**(而带 `--self-check` 才红)。
|
||
* · 后果不是小事:`findDuplicates` 是**跨文件重名**那条判据的引擎,
|
||
* 而它**唯一**的分辨力判据就是这个自检 ⇒ 它哪天退化成恒空,
|
||
* 重名判据会**安静地永远放行**,而整套测试全绿。
|
||
* ⚠️ 判"自检在不在路径上"**不能靠 `grep` 数命中**:本仓库实测 `grep -c '判据自检'`
|
||
* 在默认跑里得到 **16 次**,全是若干 `.test.mjs` 的**用例名**恰好含这四个字,
|
||
* 而 `selfCheck()` 自己的输出(`^ (通过|失败) `、`干净样本`…)**一次都没有**。
|
||
* **数命中 = 数到的是词,不是调用。**
|
||
*/
|
||
function selfCheck() {
|
||
let bad = 0;
|
||
for (const s of SELF_CHECK_SAMPLES) {
|
||
const got = findDuplicates(s.tap).map((d) => d.name);
|
||
const ok = JSON.stringify(got) === JSON.stringify(s.expect);
|
||
console.log(` ${ok ? '通过' : '失败'} ${s.what}`);
|
||
if (!ok) {
|
||
bad += 1;
|
||
console.log(` 期望 ${JSON.stringify(s.expect)},实际 ${JSON.stringify(got)}`);
|
||
}
|
||
}
|
||
console.log(bad === 0 ? '\n判据自检:全部通过。' : `\n判据自检:${bad} 条失败。`);
|
||
return bad;
|
||
}
|
||
|
||
// `--self-check`:只跑自检、不跑套件(原有行为不变)。
|
||
if (process.argv.includes('--self-check')) process.exit(selfCheck() === 0 ? 0 : 1);
|
||
|
||
// ── 跑套件 ────────────────────────────────────────────────────────
|
||
/*
|
||
* ★★ **默认路径也要先跑自检**(pi 2026-09-18)。
|
||
*
|
||
* 顺序要紧:**自检排在跑套件之前**。理由是量纲 —— 自检判的是"引擎的分辨力",
|
||
* 套件判的是"代码对不对"。引擎坏了的时候,套件那份结论**本来就不可信**,
|
||
* 拿它当主结论是把两件事混在一起(`docs/DEV-TOOLING.md:218` 那条 = 退出码也有量纲)。
|
||
* 而先跑自检还省一整轮测试进程(引擎坏了就没必要跑 509 条)。
|
||
*
|
||
* 退出码用本文件已经约定的那一套,并**与套件红分开**:
|
||
* 0 = 套件全绿且无重名(且自检通过)
|
||
* 1 = 断言失败 / 发现重名(代码问题)
|
||
* 2 = 环境(env-preflight 那套)—— 自检失败**不走这一格**
|
||
* ★ 自检失败 = **判据本身坏了**,这是"工具问题"而不是"被测代码问题",
|
||
* 所以它**不复用 `1`**(那会让 `install.sh` 把"引擎坏了"读成"代码有问题")。
|
||
* 这里用 **3** 表达"这次结论不可信(工具坏了,不是代码坏了)",
|
||
* 并在 stderr 里**明说**是哪个量纲 —— 量纲说不清就退化成"又一个红"。
|
||
* (`install.sh` 那侧的退出码语义**本次不动**:它只判"插件装好没有",
|
||
* `set -e` 下非零即中止,3 与 1 对它的行为一致;改它的语义是另一件事,
|
||
* 且要先想清楚 `install.sh` 该不该为此新增一档。)
|
||
*/
|
||
const selfCheckBad = selfCheck();
|
||
if (selfCheckBad > 0) {
|
||
console.error(`[tap-dup] 判据自检 ${selfCheckBad} 条失败 —— \`findDuplicates\` 的分辨力坏了,`
|
||
+ '**本次重名判据的结论不可信**(工具坏了,不是被测代码坏了)。');
|
||
process.exit(3);
|
||
}
|
||
|
||
// `--test-reporter=tap` 让报告格式稳定(`--test` 在 TTY 下会自动换格式,
|
||
// 那样就锚不住结果行)。输出走管道,本进程负责原样转写到 stderr。
|
||
const args = ['--test', '--test-reporter=tap', 'test/*.test.mjs'];
|
||
const r = spawnSync(process.execPath, args, {
|
||
encoding: 'utf8',
|
||
env: process.env,
|
||
maxBuffer: 256 * 1024 * 1024,
|
||
});
|
||
|
||
if (r.error) {
|
||
console.error(`[tap-dup] 起不了测试进程:${r.error.message}`);
|
||
process.exit(2);
|
||
}
|
||
|
||
const tap = r.stdout || '';
|
||
process.stderr.write(tap);
|
||
|
||
// 环境不足(前置脚本那套约定的 2)原样透传 —— 这一层不替环境问题下结论。
|
||
if (r.status === 2) {
|
||
console.error('[tap-dup] 套件以 2 退出(环境问题),不据此判重名。');
|
||
process.exit(2);
|
||
}
|
||
|
||
const s = summaryOf(tap);
|
||
if (s.tests === null) {
|
||
console.error('[tap-dup] 拿不到 `# tests` 汇总 —— 套件没跑完(可能崩了)。');
|
||
process.exit(1);
|
||
}
|
||
|
||
const dups = findDuplicates(tap);
|
||
const failed = (s.fail ?? 0) > 0 || r.status !== 0;
|
||
|
||
if (dups.length > 0) {
|
||
console.error('\n[tap-dup] 有用例名在同一次运行里出现多次:');
|
||
for (const d of dups) console.error(` ${d.count}× ${d.name}`);
|
||
console.error(' 跨文件重名不会被 runner 拦下(按文件注册)⇒ 这多半是两个文件里有同名用例,');
|
||
console.error(' 也可能是某个测试文件被二次注册(例如别的测试文件 import 了它)。');
|
||
}
|
||
|
||
console.log(`\n# 结果行重复检查:${s.tests} 条结果,${dups.length} 个名字重复`);
|
||
if (failed) {
|
||
console.error(`[tap-dup] 套件红:pass ${s.pass} / fail ${s.fail}`);
|
||
process.exit(1);
|
||
}
|
||
if (dups.length > 0) process.exit(1);
|
||
console.log('[tap-dup] 套件全绿,且没有重名。');
|