boundary-guard-shared-worktree.test.js
3.87 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
/**
* 边界守卫在**多会话共用 worktree** 时不许乱判罚(27-P3C §15.5 第 1 条)。
*
* 守卫的前提是「本次调用窗口内新出现的仓库改动 = 这个节点写的」。
* 多会话共用同一个 worktree 时这个前提是假的 —— `git status` 的差集里
* **没有「谁写的」这一维**。实测已经发生过:批 3 的开发改动把一条 run 判成
* boundary_escape 卡住,而那些文件是并行的开发会话写的。
*
* 可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 →
* 有人正在这个 worktree 上开发 → 窗口内新增的源码改动无法归属 → 只记证据不判罚。
*/
import { describe, it, expect, afterEach } from 'vitest';
import { createRequire } from 'module';
const require_ = createRequire(import.meta.url);
const engine = require_('../../services/graph-engine');
const OLD = process.env.BOUNDARY_GUARD;
afterEach(() => {
if (OLD === undefined) delete process.env.BOUNDARY_GUARD;
else process.env.BOUNDARY_GUARD = OLD;
});
describe('边界守卫 · 多会话 worktree', () => {
it('开跑时仓库干净 → 照常判罚(CI / 干净环境不受影响)', () => {
delete process.env.BOUNDARY_GUARD;
expect(engine.sharedWorktreeReason('')).toBeNull();
// 只有 agent 运行态的脏,不算「有人在开发」
expect(engine.sharedWorktreeReason('?? system/agents/foo/\n M data/companies/x/company.json')).toBeNull();
});
it('开跑时源码目录已脏 → 认定有别的会话在写,降级为只记证据', () => {
delete process.env.BOUNDARY_GUARD;
const reason = engine.sharedWorktreeReason(' M server/services/task-scheduler.js\n M web/src/views/GraphConsole.vue');
expect(reason).toBeTruthy();
expect(reason).toContain('server/services/task-scheduler.js'); // 点名证据,不是一句空话
});
it('路径含空格/中文被 git 加了引号时也认得出来', () => {
delete process.env.BOUNDARY_GUARD;
expect(engine.sharedWorktreeReason(' M "server/services/新 文件.js"')).toBeTruthy();
});
it('未跟踪的新源码文件(?? 两字符状态位)同样算', () => {
delete process.env.BOUNDARY_GUARD;
expect(engine.sharedWorktreeReason('?? server/services/brand-new.js')).toBeTruthy();
});
it('BOUNDARY_GUARD=strict 强制判罚,脏仓库也不放过', () => {
process.env.BOUNDARY_GUARD = 'strict';
expect(engine.sharedWorktreeReason(' M server/services/task-scheduler.js')).toBeNull();
});
it('BOUNDARY_GUARD=record 一律只记不判,干净仓库也是', () => {
process.env.BOUNDARY_GUARD = 'record';
expect(engine.sharedWorktreeReason('')).toBeTruthy();
});
it('降级之后 outcome 不被改成 fail,且证据仍在 escapes 之外留档', () => {
delete process.env.BOUNDARY_GUARD;
const notes = [];
const ctx = {
nodeRunId: 'rn_1', attempt: 1, projectRoot: '/tmp/proj',
// timelineNote 走的是 sse,这里只验 outcome 不被降级;证据面由上面几条覆盖
};
const before = ' M server/services/task-scheduler.js'; // 开跑时就脏
const after = before + '\n M server/services/graph-engine.js'; // 窗口内又多一处
const out = engine.applyBoundaryGuard(ctx, before, { status: 'success', output: 'ok' }, after);
expect(out.status).toBe('success'); // ← 修之前这里会被判成 fail
expect(out.failReason).toBeFalsy();
expect(notes).toEqual([]);
});
it('对照:仓库开跑时是干净的 → 同样一处越界仍然判 fail', () => {
delete process.env.BOUNDARY_GUARD;
const out = engine.applyBoundaryGuard(
{ nodeRunId: 'rn_2', attempt: 1, projectRoot: '/tmp/proj' },
'', { status: 'success', output: 'ok' }, ' M server/services/graph-engine.js',
);
expect(out.status).toBe('fail');
expect(out.failReason).toBe('boundary_escape');
});
});