/** * 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); }); });