/** * Unit Tests — 记忆 / 会话的落点下沉(28 号设计 §6 批次 3,第 13 条) * * 这条改动的失败模式是**「记忆是空的」而不是报错**:写侧(memory-service 存摘要)与 * 读侧(prompt-builder 拿 agent-manager 算出的 companyDir 去读)只要算出的字符串差一点, * 目录照建、文件照写、prompt 照拼,只是 agent 从此失忆。所以断言全部落在 * 「写进去的那份,读侧照它自己的算法能不能读回来」上,而不是「函数返回了什么字符串」。 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import fs from 'fs'; import path from 'path'; const locator = require('../../services/agent-locator'); const { saveSessionSummary, loadRecentMemory } = require('../../services/memory-service'); // 本体是真建在注册表里的常驻条目(28 号裁决:测试建出来的 agent 不删), // 所以 id 固定;afterAll 只清公司态子树 —— 记忆归属 (agentId, companyId), // 那才是这次跑写出来的东西,本体目录本身留着。 const AGENT_ID = 'zz-mem-agent'; const BODY_DIR = locator.bodyDir(AGENT_ID); const CO_A = 'zz-mem-co-a'; const CO_B = 'zz-mem-co-b'; /** 清掉本次跑写出来的公司态(含无公司调用落的 `_global/`);本体目录本身不动 */ const clearCompanyState = () => { for (const cid of ['_global', CO_A, CO_B]) { fs.rmSync(locator.companyDir(AGENT_ID, cid), { recursive: true, force: true }); } }; const META = { status: 'completed', title: '写一份测试用的战略蓝图', createdAt: new Date(Date.now() - 60_000).toISOString(), updatedAt: new Date().toISOString(), stats: { toolCallCount: 3 }, }; describe('记忆落点 = 公司态目录', () => { beforeAll(() => { // 常驻本体:内容每次重写(跨次跑不漂),三份定义写齐 = 注册表里是一份完整本体。 // 这也是第一条用例的前提:本体根上不许有 memory/,所以上一次的公司态得先清干净。 fs.mkdirSync(BODY_DIR, { recursive: true }); fs.writeFileSync(path.join(BODY_DIR, 'agent.md'), '# zz-mem-agent\n\n单测用的常驻本体。\n', 'utf-8'); fs.writeFileSync(path.join(BODY_DIR, 'soul.md'), '# zz-mem-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'); clearCompanyState(); }); afterAll(clearCompanyState); it('全局 agent(无公司)落 _global/,不再落本体根 —— 本体根只放只读本体', () => { saveSessionSummary(AGENT_ID, 'conv-global', META, '全局那次的输出'); expect(fs.existsSync(path.join(locator.companyDir(AGENT_ID), 'memory', 'short-term'))).toBe(true); expect(fs.existsSync(path.join(BODY_DIR, 'memory'))).toBe(false); expect(loadRecentMemory(AGENT_ID)).toContain('全局那次的输出'); }); it('两家公司各记各的:A 的摘要不会出现在 B 的记忆里', () => { saveSessionSummary(`${CO_A}:${AGENT_ID}`, 'conv-a', META, 'A 公司那次的输出'); saveSessionSummary(`${CO_B}:${AGENT_ID}`, 'conv-b', META, 'B 公司那次的输出'); const memA = loadRecentMemory(`${CO_A}:${AGENT_ID}`); const memB = loadRecentMemory(`${CO_B}:${AGENT_ID}`); expect(memA).toContain('A 公司那次的输出'); expect(memA).not.toContain('B 公司那次的输出'); expect(memB).toContain('B 公司那次的输出'); }); it('写侧的落点 = 读侧(prompt-builder)拿到的 companyDir —— 两边算出同一个字符串', () => { // prompt-builder 走的是 agent-manager 挂在 agentConfig 上的 companyDir, // 也就是 locator.companyDir(aid, cid);它和 memory-service 的落点必须重合。 const readSide = locator.companyDir(AGENT_ID, CO_A); expect(fs.existsSync(path.join(readSide, 'memory', 'short-term'))).toBe(true); expect(loadRecentMemory(`${CO_A}:${AGENT_ID}`)).toBe(loadRecentMemory(AGENT_ID, readSide)); }); it('companyStateDir:公司私有目录还在的老公司走老地方(过渡期,批次 4 删)', () => { // 挑一个真实存在的存量私有目录,证明过渡期分支没被绕过 —— // 绕过的表现是这些公司的历史记忆一次性全部读不到。 const legacy = locator.legacyCompanyAgentDir('mycompany', 'brd-planning'); if (fs.existsSync(legacy)) { expect(locator.companyStateDir('mycompany:brd-planning')).toBe(legacy); } // 没有私有目录的(新布局)一律落公司态目录 expect(locator.companyStateDir(`${CO_A}:${AGENT_ID}`)).toBe(locator.companyDir(AGENT_ID, CO_A)); expect(locator.companyStateDir(AGENT_ID)).toBe(locator.companyDir(AGENT_ID, '_global')); }); });