test(criteria): 抽出共享 check/finish(marker 不再靠记性)+ 失败信息自带修法

pi 的三条增量,前两条落地:

1. **错误信息自带修法**:受众不只是读过规范的人 —— 并发写 WebUI 的 agent 新加判据时不会打开
   CRITERIA.md,看到红的第一反应可能是"套件坏了"。所以把可照抄的修法写进那条错误本身
   (共享 helper 的用法 + 样板文件路径),并说明 node:test 的判据不用管。
   **red 是 ta 一定会看到的,文档不一定被打开。**

2. **marker 由共享 helper 打印**:新增 test/lib/checks.mjs(导出 check/finish),
   计数只可能在该模块内发生 → "漏打 marker"与"计数写错位置"这两类在新文件上不可能发生。
   为避免"写了没人用"(本仓踩过的坑),同时把两个手工计数的判据改用它:
   narrow-layout(原来只有 failed 计数)与 markdown-xss(原来根本没有计数器)——
   条数不变(52 / 9),套件仍全绿。

未回改其余 10 个文件:run-all 的 marker 检查已经覆盖它们。
This commit is contained in:
2026-09-14 15:11:47 +08:00
parent d1e0ba32f9
commit ec90cba129
4 changed files with 73 additions and 27 deletions

View File

@ -0,0 +1,44 @@
/*
* 判据的共享 check/finish —— 让"漏打 marker"和"计数写错位置"这两类
* **在新文件上不可能发生**pi 2026-09-14 的建议)。
*
* 背景:`run-all.mjs` 用"自报条数 + 每文件期望条数"当棘轮(只增不减),
* 但之前那是两条靠记性的约定 —— "记得在结尾打 `RESULT pass=N`"、
* "计数必须写在 `check()` 内部"。这两条各自都踩空过:
* `narrow-layout` 当时只有 `failed` 计数、`markdown-xss` 根本没有计数器。
*
* 用法(判据文件):
*
* import { check, finish } from './lib/checks.mjs';
* check('这条判据的名字', cond, '失败时给人看的细节');
* finish('主题'); // 打完汇总就退出(有失败则退出码 1
*
* 计数只可能在这个模块里发生,所以只要用了它:
* - marker 一定会有(`finish()` 打);
* - `check()` 的实现不可能被"合并冲突改空"而看不见(空实现 → pass=0 → 棘轮红)。
*
* ⚠️ 反过来也成立:**判据文件里不要自己再写一份 check/计数**(那正是本模块要消灭的东西)。
* 已有的 12 条判据不用回改(`run-all.mjs` 的 marker 检查已经覆盖它们)。
*/
let pass = 0;
let fail = 0;
/** 一条判据。`ok` 为真记通过,否则记失败并打印细节 */
export function check(name, ok, detail = '') {
if (ok) {
pass++;
console.log(` 通过 ${name}`);
return true;
}
fail++;
console.error(` 失败 ${name}${detail ? ' — ' + detail : ''}`);
return false;
}
/** 汇总并退出。`label` 是给人看的名字(例:「主题」) */
export function finish(label) {
console.log(`\n${label}${pass} 通过${fail ? `${fail} 失败` : ''}`);
// 机器可读的契约行run-all.mjs 只认这一行来判"这条判据到底跑了几条"
console.log(`RESULT pass=${pass} fail=${fail}`);
process.exit(fail ? 1 : 0);
}

View File

@ -9,6 +9,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { check, finish } from './lib/checks.mjs';
// 只有【真实标签】里的危险内容才算漏。
// 注意不能直接搜 onerror=raw HTML 被转义成 <img … onerror=" 后,
@ -27,19 +28,14 @@ const payloads = [
'**bold** `code`',
];
let leaks = 0;
// 每条 payload 就是一条判据(换来的是"跑了几条"可核对,而不是只报一句总括)
console.log('Markdown 注入回归:');
for (const p of payloads) {
const html = renderToStaticMarkup(
React.createElement(Markdown, { remarkPlugins: [remarkGfm] }, p)
);
const bad = dangerous.test(html);
if (bad) leaks++;
console.log((bad ? 'LEAK ' : 'safe '), JSON.stringify(p), '->', html.slice(0, 80));
check(`危险内容被清除:${JSON.stringify(p)}`, !bad, `渲染结果里出现了真实可执行的危险标记:${html.slice(0, 120)}`);
}
if (leaks > 0) {
console.error(`\n失败:${leaks} 处 XSS 泄漏`);
process.exit(1);
}
console.log('\n通过raw HTML 被转义javascript: URL 被清空');
// 机器可读汇总:每条 payload 算一条leaks 就是 fail
console.log(`RESULT pass=${payloads.length - leaks} fail=${leaks}`);
finish('Markdown 注入');

View File

@ -3,21 +3,9 @@
// 不做视觉快照:那需要 headless 浏览器,且像素级比对在字体差异下极脆。
// 这里守住几条真正会坏掉的不变量。
import { readFileSync } from 'node:fs';
import { check, finish } from './lib/checks.mjs';
const read = p => readFileSync(new URL(p, import.meta.url), 'utf8');
let failed = 0;
let passed = 0; // 机器可读汇总用(在 check 内部自增:实现被换空 → 0
const check = (name, cond, detail = '') => {
if (cond) {
passed++;
}
if (cond) {
console.log(` 通过 ${name}`);
} else {
console.error(` 失败 ${name}${detail ? ' — ' + detail : ''}`);
failed++;
}
};
console.log('窄屏布局回归:');
@ -238,6 +226,5 @@ check(
/\{lead\}\s*<button/.test(mv)
);
console.log(failed === 0 ? '\n窄屏布局全部通过' : `\n窄屏布局:${failed} 项失败`);
console.log(`RESULT pass=${passed} fail=${failed}`);
process.exit(failed === 0 ? 0 : 1);
// 共享 helper 打汇总与 marker计数在 check() 内部,见 lib/checks.mjs 的说明)
finish('窄屏布局');

View File

@ -176,9 +176,28 @@ for (const [file, flags, expected] of SUITE) {
const nodeTest = /^# pass (\d+)/m.exec(out);
const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null);
if (ran === null) {
reds.push(`${file}(没找到自报条数 —— 判据要打 RESULT pass=N 或走 node:test 的 # pass N`);
/*
* 报错**自带修法**pi 2026-09-14这条契约的受众不只是读过规范的人 ——
* 并发写 WebUI 的 agent 新加判据时不会打开 CRITERIA.md看到红的第一反应
* 很可能是"套件坏了"(删自检、往清单里塞豁免)。**red 是 ta 一定会看到的东西,
* 文档不一定会被打开** —— 所以把修法直接写进这条错误里,并给出可抄的样板。
*/
reds.push(`${file}
↳ 没找到自报条数。修法(二选一):
1) 用共享 helper新判据推荐
import { check, finish } from './lib/checks.mjs';
check('判据名', 条件, '失败时给人看的细节');
finish('标签'); // 它负责打 RESULT pass=N fail=M
样板test/markdown-xss.test.mjs、test/narrow-layout.test.mjs
2) 自己打一行(老写法,计数必须写在 check() 内部,否则"实现被换空"看不见):
console.log(\`RESULT pass=\${pass} fail=\${fail}\`);
样板test/theme.test.mjs、test/background.test.mjs
(用 node:test 写的判据不用管runner 认 \`# pass N\`。)`);
} else if (expected > 0 && ran < expected) {
reds.push(`${file}(自报 ${ran} 条 < 登记的 ${expected} 条 —— 条数掉了:判据被删/被跳过/check 实现被改坏?)`);
reds.push(`${file}
↳ 自报 ${ran} 条 < 清单里登记的 ${expected} 条。常见成因:判据被删/被跳过(写在 process.exit() 之后、
条件里提前 return、check() 的实现被改坏合并冲突、marker 打在了汇总之前但计数没接上。
确认确实该减少条数时,把清单里那个数字一起改掉(那是一次显式、可复核的编辑)。`);
}
}
}