Files
MailUI4Agents/plugins/pi-mail-bridge/test/lib/run-suite.mjs
JianFeeeee cc8beb79db fix(pi-bridge): --self-check 此前**一次都没在真实路径上跑过** —— 自检改进程内、默认路径先跑、量纲分三档
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 次。)
2026-09-18 07:19:26 +08:00

219 lines
9.9 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.

#!/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] 套件全绿,且没有重名。');