Files
Literature-Management/tests/unit/main/lint/stale-test-reference.test.ts
2026-10-21 14:42:59 +08:00

170 lines
6.7 KiB
TypeScript

// tests/unit/main/lint/stale-test-reference.test.ts
//
// 守卫(r180):源码注释里引用的**测试文件路径必须真实存在**,否则要么
// 明确标注为「守卫缺失」。
//
// 为什么需要这条:2026-10-19 的 docs 大下线把整棵 tests/ 目录删掉了
// (CHANGELOG 记录曾达 2501 例),但源码注释里 45 处「行为锁:xxx.test.ts」
// 的指针还指着已经不存在的文件。后果不是编译错误,而是**更坏的**:
// - 后来的人 grep 那个测试名 → 找不到 → 以为「这行为没被测过」→
// 重写一遍(白干),或者
// - 以为「有测试兜着」→ 不写测试就改行为 → 回归无人拦。
// 注释里的测试指针是**契约的一部分**,指针死了必须显式说「死了」。
//
// 约定:引用一个已不存在的测试文件时,同一行必须带 🔒 标记。
// 现状:45 处已按约定标注(逐条改成 `路径 🔒(守卫缺失)`)。
// 这条测试只拦**未标注**的死指针 —— 标注过的允许存在(它们是历史记录,
// 补回测试时把 🔒 去掉即可,这条测试会继续守着)。
//
// audit-r203 补上**反向**方向:🔒 标记会**过期** —— 守卫测试补齐后,
// 源码里的 `路径 🔒(守卫缺失)` 就不再成立,读者仍会以为「这块没测试兜着」
// 而重复造轮子(或反过来,以为「🔒 = 有问题」而不敢动)。约定是
// 「补回测试就把 🔒 去掉」,那就让机器来查有没有人忘了去掉。
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
import { join } from 'path';
import { describe, expect, it } from 'vitest';
const ROOT = process.cwd();
const MARKER = '🔒';
// 只在源码目录里扫(测试文件之间互相引用少见,且会让本文件自指)。
const SCAN_DIRS = ['src'];
function walkTs(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) out.push(...walkTs(full));
else if (/\.tsx?$/.test(entry) && !entry.endsWith('.d.ts')) out.push(full);
}
return out;
}
// 匹配注释 / 字符串里出现的「测试文件路径」字面量。
// 写成拼接形式,避免本文件自身被自己的正则命中。
const TEST_PATH_RE = new RegExp('tests[/\\\\][A-Za-z0-9_\\\\/-]+?\\.test\\.tsx?', 'g');
interface Violation {
file: string;
line: number;
target: string;
text: string;
}
/** 取出该行引用的全部测试路径(去重) */
function targetsIn(text: string): string[] {
TEST_PATH_RE.lastIndex = 0;
const out: string[] = [];
let m: RegExpExecArray | null;
while ((m = TEST_PATH_RE.exec(text)) !== null) {
const t = m[0].replaceAll('\\', '/');
if (!out.includes(t)) out.push(t);
}
return out;
}
function collectViolations(): Violation[] {
const violations: Violation[] = [];
for (const dir of SCAN_DIRS) {
for (const file of walkTs(join(ROOT, dir))) {
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((text, i) => {
TEST_PATH_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = TEST_PATH_RE.exec(text)) !== null) {
const target = m[0].replaceAll('\\', '/');
if (existsSync(join(ROOT, target))) continue;
if (text.includes(MARKER)) continue; // 已显式标注「守卫缺失」
violations.push({
file: file.slice(ROOT.length + 1),
line: i + 1,
target,
text: text.trim().slice(0, 120),
});
}
});
}
}
return violations;
}
/**
* 收集**过期**的 🔒 标记:某行带 🔒,且它引用的测试文件**全部**都已存在
* ⇒ 这条 🔒 已经过期,应当去掉。
*
* 判据用「全部存在」而不是「任一存在」:一行里可能同时引用多个测试
* (有的存在、有的仍缺),此时 🔒 仍然必要(还指着缺失的那个)。
*/
function collectStaleMarkers(): Violation[] {
const violations: Violation[] = [];
for (const dir of SCAN_DIRS) {
for (const file of walkTs(join(ROOT, dir))) {
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((text, i) => {
if (!text.includes(MARKER)) return;
const targets = targetsIn(text);
if (targets.length === 0) return;
if (!targets.every((t) => existsSync(join(ROOT, t)))) return;
violations.push({
file: file.slice(ROOT.length + 1),
line: i + 1,
target: targets.join(', '),
text: text.trim().slice(0, 120),
});
});
}
}
return violations;
}
describe('源码注释里的测试指针(r180)', () => {
it('引用的测试文件必须存在,或已在同一行标注 🔒 守卫缺失', () => {
const violations = collectViolations();
const detail = violations
.map((v) => ` ${v.file}:${v.line} → ${v.target}\n ${v.text}`)
.join('\n');
expect(
violations,
`以下注释引用了不存在的测试文件,且没有 🔒(守卫缺失)标注:\n${detail}`,
).toEqual([]);
});
it('扫描器本身能认出「已标注」与「未标注」两种状态', () => {
// 探针:确认正则与标注约定真的生效(否则上面那条可能因为正则失配而空过)
const sample = [
'// 行为锁:tests/unit/definitely/absent.test.ts',
'// 行为锁:tests/unit/definitely/absent.test.ts 🔒(守卫缺失)',
];
const hits = sample.filter((line) => {
TEST_PATH_RE.lastIndex = 0;
const m = TEST_PATH_RE.exec(line);
return m !== null && !line.includes(MARKER);
});
expect(hits).toHaveLength(1);
});
});
describe('🔒(守卫缺失)标记的时效性(r203)', () => {
it('守卫测试已补齐的行,不许还留着 🔒 标记', () => {
const stale = collectStaleMarkers();
const detail = stale.map((v) => ` ${v.file}:${v.line} → ${v.target}\n ${v.text}`).join('\n');
expect(
stale,
`以下行的测试已存在,🔒(守卫缺失)标记已过期,请去掉:\n${detail}`,
).toEqual([]);
});
it('扫描器能区分「过期标记」与「仍然有效的标记」', () => {
// 探针:一个引用真实存在文件的 🔒 行 → 过期;一个引用不存在文件的 🔒
// 行 → 仍然有效。两条判据都跑一遍,防止上面那条因为逻辑写反而空过。
const realFile = 'tests/unit/main/lint/stale-test-reference.test.ts';
const absentFile = 'tests/unit/definitely/absent.test.ts';
const realTargets = targetsIn(`// ${realFile} ${MARKER}`);
const absentTargets = targetsIn(`// ${absentFile} ${MARKER}`);
expect(realTargets).toEqual([realFile]);
expect(existsSync(join(ROOT, realTargets[0]))).toBe(true);
expect(existsSync(join(ROOT, absentTargets[0]))).toBe(false);
});
});