definition-write-gate.test.js
4.53 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
/**
* 改本体定义文件之前的 git 历史闸门(29-DREAM 决策 B)。
*
* ── 守什么 ────────────────────────────────────────────────────────────
* dream 直接改 `agent.md` / `soul.md` / `SKILL.md`,而「本体只有一份」——
* 改一次影响**所有雇了这个本体的公司**。覆盖一份尚未进入 git 历史的内容,
* 等于让上一版永久消失,且**不报错**。
*
* 两条判据都要满足才放行:① 被 git 跟踪;② 没有未提交改动。
* 判不出来(不在仓库 / git 不可用)一律拒绝 —— fail-closed。
*
* ── 为什么不是「脏就先替它提交一把」 ──────────────────────────────────
* 那正是本仓库记过的一类事故:多会话共用同一个 worktree 时,一份未提交的改动
* 很可能是**另一个会话正在写的东西**(27-P3C §15.4)。替它提交 = 把别人的半成品
* 写进 main 且署错名。所以拒绝并报告,把「要不要提交」留给人。
*
* ── 隔离 ──────────────────────────────────────────────────────────────
* 在临时目录里 `git init` 一个真仓库来验,不碰本仓库的 git 状态。
* 闸门的 REPO_ROOT 是模块级常量,所以用 `cwd` 覆盖不了 —— 改成对**真仓库里的真文件**
* 做正向断言,对负向分支用临时仓库 + 直接调内部判据。
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { execFileSync } from 'child_process';
const gate = require('../../services/definition-write-gate');
const git = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
describe('本体定义写入闸门 · git 历史', () => {
it('已跟踪且工作区干净 → 放行(拿真仓库里的真文件验,样本非空)', () => {
const f = path.join(gate.REPO_ROOT, 'system/agents/ceo/agent.md');
expect(fs.existsSync(f)).toBe(true); // 样本非空:文件真的在,否则下面这条是空跑
const v = gate.canOverwriteDefinition(f);
// 本仓库跑测试时这个文件应当是干净的;万一有人正在改它,这条会红 —— 那不是误报,
// 正是闸门要表达的意思(有未提交改动时就不该让 dream 覆盖)
expect(v.allowed).toBe(true);
});
it('没被跟踪 → 拒绝,且理由里说清「覆盖即永久丢失」和下一步', () => {
const f = path.join(gate.REPO_ROOT, 'system/agents/ceo/zz-not-tracked.md');
const v = gate.canOverwriteDefinition(f);
expect(v.allowed).toBe(false);
expect(v.reason).toContain('没有被 git 跟踪');
expect(v.reason).toContain('覆盖即永久丢失');
expect(v.reason).toContain('commit'); // 拒绝必须给出下一步,否则撞上的人只能猜
});
it('仓库之外的路径 → 拒绝(没有历史可依托)', () => {
const v = gate.canOverwriteDefinition('/tmp/somewhere/agent.md');
expect(v.allowed).toBe(false);
expect(v.reason).toContain('不在仓库内');
});
it('有未提交改动 → 拒绝(这一条用临时真仓库验,不碰本仓库状态)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'defgate-'));
try {
git(['init', '-q'], dir);
git(['config', 'user.email', 't@t'], dir);
git(['config', 'user.name', 't'], dir);
const f = path.join(dir, 'agent.md');
fs.writeFileSync(f, '# v1\n');
git(['add', 'agent.md'], dir);
git(['commit', '-qm', 'v1'], dir);
// 干净时:git status 为空
expect(git(['status', '--porcelain', '--', 'agent.md'], dir).trim()).toBe('');
// 改脏之后:非空 —— 这正是闸门判「拒绝」的那个信号
fs.writeFileSync(f, '# v2 未提交\n');
expect(git(['status', '--porcelain', '--', 'agent.md'], dir).trim()).not.toBe('');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('assertOverwritable 在不满足时抛,且带可识别的 code', () => {
expect(() => gate.assertOverwritable('/tmp/nope/agent.md')).toThrow(/拒绝改写本体定义/);
try {
gate.assertOverwritable('/tmp/nope/agent.md');
} catch (e) {
expect(e.code).toBe('DEFINITION_NOT_IN_GIT_HISTORY');
}
});
});