no-dead-path-references.test.js
6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* 元测试 —— 指路的文档 / 清单里不许出现「指向不存在的东西」的引用
*
* ── 来历 ──────────────────────────────────────────────────────────────────
* 手工扫了两轮,扫出的不是零星笔误,是三类会持续复发的失真:
*
* ① package.json 里 12 处引用指向**从未在 git 历史里存在过**的文件
* (`server/src/cli/auto-dev.js`、`server/src/utils/*`、`skills/whatsapp-bot/*` …)。
* `npm run example` 跑起来是 MODULE_NOT_FOUND —— 而 CLAUDE.md 把它们当可用命令列着。
* ② CLAUDE.md 的 `docs/` 那一行列了 5 个具体文件名,文档移进 `docs/done/` 之后全成死链。
* 根因是同一份清单被抄在两处(CLAUDE.md 一份、docs/README.md 一份),一搬家就烂一份。
* ③ 数据存储那一节把两个不同的 data 根当成一个写,`server/conversations/` 这个路径根本不存在。
*
* 三类的共性:**不报错、不影响测试、git status 干净**,只有照着做的人(或 agent)撞墙才发现。
* CLAUDE.md 是每个会话都会加载进上下文的文件,它写错等于把错误信息直接灌给每一个后来者 ——
* 所以这条防线守的是「指路牌不许指向不存在的地方」。
*
* ── 判据为什么窄 ──────────────────────────────────────────────────────────
* 路径只认「以仓库顶层目录开头 + 含 `/`」的那一种(`server/routes/devloop.js`、`docs/README.md`)。
* 排除掉:
* - 上下文里的相对片段(`_base/`、`todo/future/`、`agents/` —— 它们在各自句子里有前缀,
* 单独拎出来当路径判必然全红)
* - 含 `*` 的 glob、含空格的短语、`<repo>/...` 这类示意写法
* - **gitignored 的路径**:那是运行态产物(`web/dist/`、`data/companies/`、`skills/`),
* 它在一台干净机器上本来就不存在,缺席不是文档错误。这条判据与
* `no-stale-templates-path.test.js` 用的是同一条(跟踪的才是定义,未跟踪的是运行态)。
* 宽一档就会把上面这些一起判红,防线于是被人整条注释掉 —— 那比没有更糟。
*
* ── 「样本非空」前置断言 ───────────────────────────────────────────────────
* 四条扫描各带一条下限断言。少了它,正则一写错 / 文件一改名,就会以「零违例」的形式全绿 ——
* 一条永远通过的不变式测试和没有这条测试是一回事。下限取当前值的一半左右,
* 留出正常增删的余量,但挡得住「扫了个寂寞」。
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { execFileSync } from 'child_process';
const ROOT = path.resolve(__dirname, '../../..');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf-8');
const exists = (p) => fs.existsSync(path.join(ROOT, p));
/** 仓库顶层目录名,用来认「这是一条仓库相对路径」而不是句子里的片段 */
const TOP_DIRS = new Set(
fs.readdirSync(ROOT, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
);
/**
* 一次 git check-ignore 批量问完(逐条 spawn 会慢一个数量级)。
* 没有任何一条被忽略时 git 以 exit 1 结束,那不是错误,要吞掉。
*/
function ignoredSet(paths) {
if (paths.length === 0) return new Set();
try {
const out = execFileSync('git', ['check-ignore', '--stdin'], {
cwd: ROOT, input: paths.join('\n'), encoding: 'utf-8',
});
return new Set(out.split('\n').filter(Boolean));
} catch (err) {
if (err.status === 1) return new Set(String(err.stdout || '').split('\n').filter(Boolean));
throw err;
}
}
/** 反引号里那些长得像仓库相对路径的串 */
function repoPathsIn(md) {
const out = new Set();
for (const m of md.matchAll(/`([^`\n]+)`/g)) {
const t = m[1].replace(/\/$/, '');
if (!t.includes('/') || t.includes('*') || t.includes(' ') || t.startsWith('<')) continue;
if (!TOP_DIRS.has(t.split('/')[0])) continue;
out.add(t);
}
return [...out];
}
const pkg = JSON.parse(read('package.json'));
describe('指路牌不许指向不存在的地方', () => {
it('CLAUDE.md 里的仓库路径都真实存在', () => {
const cands = repoPathsIn(read('CLAUDE.md'));
const ignored = ignoredSet(cands);
const checked = cands.filter((p) => !ignored.has(p));
expect(checked.length).toBeGreaterThan(12); // 样本非空:当前 26 条
expect(checked.filter((p) => !exists(p))).toEqual([]);
});
it('CLAUDE.md 里出现的每条 npm run 都在 package.json 里', () => {
// 兼容 `npm run web / web:build` 这种一行写两个的形式
const names = new Set();
for (const m of read('CLAUDE.md').matchAll(/npm run ([\w:-]+(?:\s*\/\s*[\w:-]+)*)/g)) {
m[1].split('/').forEach((n) => names.add(n.trim()));
}
expect(names.size).toBeGreaterThan(6); // 样本非空:当前 16 个
expect([...names].filter((n) => !pkg.scripts?.[n])).toEqual([]);
});
it('package.json 的 scripts / bin / main 指向的文件都存在', () => {
const refs = [];
for (const [k, v] of Object.entries(pkg.scripts || {})) {
for (const m of String(v).matchAll(/(?:^|&&|\|\||;)\s*(?:node|bash|sh)\s+(\S+\.(?:js|sh))/g)) {
refs.push([`scripts.${k}`, m[1]]);
}
}
for (const [k, v] of Object.entries(pkg.bin || {})) refs.push([`bin.${k}`, String(v).replace(/^\.\//, '')]);
if (pkg.main) refs.push(['main', pkg.main]);
expect(refs.length).toBeGreaterThan(3); // 样本非空:当前 6 条
const ignored = ignoredSet(refs.map(([, p]) => p));
const bad = refs.filter(([, p]) => !ignored.has(p) && !exists(p)).map(([k, p]) => `${k} → ${p}`);
expect(bad).toEqual([]);
});
it('docs/README.md 的相对链接都指向真实文件', () => {
const links = [...read('docs/README.md').matchAll(/\]\((\.\/[^)#]+)\)/g)].map((m) => 'docs/' + m[1].slice(2));
expect(links.length).toBeGreaterThan(10); // 样本非空:当前 22 条
expect(links.filter((p) => !exists(p))).toEqual([]);
});
});