repo-hygiene.test.js
5.07 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
/**
* Unit Tests — 仓库卫生:tracked 与 ignored 不重叠(28 二轮修复,原 blocker 6)
*
* 锁的不是某段代码,而是**索引与 .gitignore 的一致性**这个不变式:
* `system/agents/` 下深度 >2 的路径里,被 gitignore 命中的 tracked 文件必须是 0 条。
*
* 为什么这条不变式非锁不可(三条后果都不长成「报错」的样子):
* 1. gitignore 对**已 tracked** 的文件完全无效。批次 1 用 `git mv` 把 system/templates/
* 整体搬进 system/agents/ 时,把 143 个本该被 §5.5 新规则 ignore 的运行态文件
* 一起带进了 index —— 新规则写得再对也拦不住它们。
* 2. graph-engine 的 beginBoundaryGuard / applyBoundaryGuard 每次 agent 调用前后各拍一次
* 整仓 git status,diff 非空就把节点判 `boundary_escape` fail。agent 按相对路径写进
* 自己的 workspace/ 就会改动这些 tracked 文件 → **本来成功的节点被判失败,
* 而且归因指向当时正在跑的那个节点**(28 风险表第 1 行)。
* 3. 于是 §5.5 验收 3「跑一次真 agent 调用后 git status 增量为空」结构性不可能达成。
*
* 判据用 `--no-index`:不加这个参数时 git check-ignore 会跳过已在索引里的路径,
* 而「已在索引里」恰恰是本测试唯一关心的那批 —— 不加就永远绿,是最典型的「隔离是表演」。
*/
import { describe, it, expect } from 'vitest';
import { execSync, spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
const REPO_ROOT = path.resolve(__dirname, '../../..');
const trackedUnderAgents = () =>
execSync('git ls-files system/agents', { cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 })
.split('\n')
.filter(Boolean);
/** 命中 gitignore 的那些(空列表进空列表出,check-ignore 退出码 1 不当异常) */
function ignoredAmong(paths) {
if (!paths.length) return [];
const r = spawnSync('git', ['check-ignore', '--no-index', '--stdin'], {
cwd: REPO_ROOT, input: paths.join('\n'), encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024,
});
return (r.stdout || '').split('\n').filter(Boolean);
}
describe('仓库卫生(28 §5.5)', () => {
it('system/agents 下深度 >2 的 tracked 路径,没有一条命中 gitignore', () => {
const tracked = trackedUnderAgents();
// 先证明样本非空:本体目录下的 .claude/skills、train/、docs/ 都是深度 >2 的合法 tracked 路径。
// 少了这一步,哪天 system/agents 整个被误 ignore,下面那条断言反而会变绿。
const deep = tracked.filter((p) => p.split('/').length > 3);
expect(deep.length).toBeGreaterThan(0);
expect(ignoredAmong(deep)).toEqual([]);
});
it('反向:磁盘上每一份 system/agents/*/agent.md 都必须可被 git 跟踪', () => {
// 上一条锁的是「ignore 规则没漏」,这条锁的是**反方向**:ignore 规则没误伤本体。
// 两条都必要,因为 §5.5 的白名单写法是「先 `system/agents/*/*/` 全排除、再 ! 回几处」——
// 任何一条新加的排除规则(哪怕只是想挡掉某个运行态目录)都可能顺手把本体根盖住。
//
// 后果同样不长成报错的样子:本体被 ignore → 它不进版本库 → 别的机器 / 下一次 clone
// 拿到的是「目录在、agent.md 不在」的空壳,promptBuilder 读不到定义时**不抛异常**,
// agent 静默退化成通用 Base Agent 文本(28 风险表第 3 行的同款静默降级)。
//
// 判据取**磁盘**而不是 git ls-files:新建出来还没 add 的本体(G4 create-agent 的产物、
// 测试跑出来的注册表条目)正是最容易被规则误伤的一批,它们不在 index 里,
// 用 ls-files 取样就恰好把该测的样本漏掉了。
// 仍要 `--no-index`:已 tracked 的本体会被 check-ignore 直接跳过,不加就只测到一半。
const agentsRoot = path.join(REPO_ROOT, 'system/agents');
const bodies = fs.readdirSync(agentsRoot, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => `system/agents/${e.name}/agent.md`)
.filter((rel) => fs.existsSync(path.join(REPO_ROOT, rel)));
// 样本非空自证:本仓有 50+ 个本体,取样为 0 说明扫描路径写错了,否则下面那条恒绿。
expect(bodies.length).toBeGreaterThan(10);
expect(ignoredAmong(bodies)).toEqual([]);
});
it('两个迁移备份根都被 ignore(178MB 的快照不进版本库)', () => {
// 通配 `data/_migration-backup-*/` 写的是**目录**规则,只对目录名生效;
// 逐个列名的老写法漏过了 _migration-backup-2026-08-17,那份 178MB 一直挂在 git status 里。
const roots = ['data/_migration-backup-agentdirs/x', 'data/_migration-backup-2026-08-17/x'];
expect(ignoredAmong(roots).sort()).toEqual(roots.sort());
expect(execSync('git ls-files data/_migration-backup-agentdirs data/_migration-backup-2026-08-17',
{ cwd: REPO_ROOT, encoding: 'utf-8' }).trim()).toBe('');
});
});