memory-body-readonly.test.js
4.92 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
/**
* Unit Tests — 运行期本体只读:反思沉淀不许写共享本体(28 号第二轮 blocker 1)
*
* 这条改动的失败模式是**跨公司串味且全程不报错**:`autoReflect` 收尾时把某一家公司的
* 对话内容(错误原文 + 任务标题)追加进 `system/agents/{aid}/.claude/skills/main_skill/SKILL.md`
* —— 批次 1 合并之后那是全系统唯一一份的共享本体,于是 A 公司的机密会出现在 B 公司调
* 同一个 agent 的提示词里。写入被 try/catch 包着、只打一行成功日志,且本体是 git 跟踪的,
* 顺带还会让 graph-engine 的边界守卫把别人正在跑的节点判成 boundary_escape。
*
* 所以断言落在「本体那份字节没动」+「内容确实落在了公司态」+「另一家读不到」三点上,
* 而不是「函数返回了什么」。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
const locator = require('../../services/agent-locator');
const { autoReflect } = require('../../services/memory-service');
// 常驻注册表条目(28 号裁决:测试建的本体不删),所以 id 固定。
// 本体内容每次 beforeAll 重写,跨次跑不漂;afterAll 只清公司态子树。
const AGENT_ID = 'zz-body-ro-agent';
const BODY_DIR = locator.bodyDir(AGENT_ID);
const SKILL_PATH = path.join(BODY_DIR, '.claude', 'skills', 'main_skill', 'SKILL.md');
const BODY_TEXT = '# 共享本体\n\n全系统唯一一份,运行期只读。\n';
const SECRET = '机密报价 1.2 亿';
const CO_A = 'zz-body-ro-co-alpha';
const CO_B = 'zz-body-ro-co-beta';
/** mtime 先按到一个固定的过去时刻:否则同一毫秒内的重写可能测不出来 */
const PAST = new Date('2020-01-01T00:00:00Z');
const META = {
status: 'failed',
title: 'ALPHA 收购 XYZ 的尽职调查',
createdAt: new Date(Date.now() - 600_000).toISOString(),
updatedAt: new Date().toISOString(),
stats: { toolCallCount: 9 },
};
const knowhowOf = (cid) => path.join(locator.companyDir(AGENT_ID, cid), 'memory', 'knowhow.md');
const readIfExists = (p) => { try { return fs.readFileSync(p, 'utf-8'); } catch { return ''; } };
describe('反思沉淀不写共享本体', () => {
// 建目录 / 跑反思必须在 beforeAll 里,不能摊在 describe 体上:describe 体在**收集阶段**
// 就会执行(`vitest list`、IDE 的用例发现、typecheck 都只收集不跑),而 afterAll 那时
// 不会触发 —— 于是每收集一次就往真实的 system/agents/ 里留一个 tmp-body-ro-* 目录,
// 它 untracked 且顶层没被 ignore,直接脏了 §5.5 验收 3 那条「git status 为空」。
let mtimeBefore;
let reflection;
beforeAll(() => {
// 上一次跑留下的公司态先清掉:最后一条用例断言「B 公司读不到 alpha 的内容」,
// 而 knowhow.md 是追加写的 —— 不清的话它验的是上一次的残留,而不是这一次的隔离。
for (const cid of [CO_A, CO_B]) {
fs.rmSync(locator.companyDir(AGENT_ID, cid), { recursive: true, force: true });
}
// 本体三份定义写齐:注册表里这份条目是完整本体,不是只有 SKILL.md 的半成品
fs.mkdirSync(BODY_DIR, { recursive: true });
fs.writeFileSync(path.join(BODY_DIR, 'agent.md'), '# zz-body-ro-agent\n\n单测用的常驻本体。\n', 'utf-8');
fs.writeFileSync(path.join(BODY_DIR, 'soul.md'), '# zz-body-ro-agent 的人格\n\n嘴严。\n', 'utf-8');
fs.writeFileSync(path.join(BODY_DIR, 'config.json'),
JSON.stringify({ id: AGENT_ID, name: AGENT_ID, type: 'skill', status: 'active' }, null, 2), 'utf-8');
fs.mkdirSync(path.dirname(SKILL_PATH), { recursive: true });
fs.writeFileSync(SKILL_PATH, BODY_TEXT, 'utf-8');
fs.utimesSync(SKILL_PATH, PAST, PAST);
mtimeBefore = fs.statSync(SKILL_PATH).mtimeMs;
reflection = autoReflect(`${CO_A}:${AGENT_ID}`, 'conv-a', META, `Error: ${SECRET}`);
});
afterAll(() => {
// 只清公司态,本体留在注册表里
for (const cid of [CO_A, CO_B]) {
fs.rmSync(locator.companyDir(AGENT_ID, cid), { recursive: true, force: true });
}
});
it('反思确实发生了(否则下面三条断言全是空跑)', () => {
expect(reflection?.lessons.length).toBeGreaterThan(0);
});
it('本体 SKILL.md 一个字节没动,mtime 也没动', () => {
expect(fs.readFileSync(SKILL_PATH, 'utf-8')).toBe(BODY_TEXT);
expect(fs.statSync(SKILL_PATH).mtimeMs).toBe(mtimeBefore);
});
it('公司态目录下能读到这次的教训', () => {
expect(readIfExists(knowhowOf(CO_A))).toContain(SECRET);
});
it('另一家公司读不到 alpha 的内容 —— 记忆归属 (agentId, companyId)', () => {
expect(readIfExists(knowhowOf(CO_B))).not.toContain(SECRET);
// 本体那一侧也不许有:B 公司的提示词是从本体 + 自己的公司态拼出来的
expect(fs.readFileSync(SKILL_PATH, 'utf-8')).not.toContain(SECRET);
});
});