identity-context.test.js
7.42 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* Unit Tests — identity-context.js(本体装配静默坑修复)
*
* 锁的是「work 节点只有 agent.md,soul/identity/tools/heartbeat/OKR/hippocampus/skills
* 全部缺失」这条修复的具体行为:读了哪些文件、按什么条件跳过、不重复已经被
* PromptBuilder.buildCompanyStateParts 读过的那三样(user.md/memory.md/近期记忆)。
*
* OkrService / retrieveRelevantPlans 两个依赖没有走 vi.mock——本仓库服务层是纯
* CommonJS(require/module.exports),vi.mock 面向的是 Vite 处理过的 ESM 模块图,
* 对 identity-context.js 内部自己 require() 出来的这条链路不生效(实测:mock 完全不拦截,
* `okrMod.OkrService` 收到的还是真实的类,不是 vi.fn())。改用更直接的手法——CJS 的
* `require()` 在同一进程内对同一路径返回**同一个** `module.exports` 对象,所以直接在
* `beforeEach` 里把这两个依赖模块的导出属性**原地换成假实现**,`identity-context.js`
* 用 `require()` 拿到的就是换过的那份;`afterEach` 换回来,不留痕迹。
* 这两个依赖也没有任何 env 覆盖点能让它们落到隔离的临时目录(不像 GRAPH_DB_PATH 那套),
* 真去读会既不确定又污染仓库数据,所以只验证「调用了、传参对不对、返回值有没有被
* 正确拼进去」,不验证它们内部的真实检索逻辑(那是各自模块自己的测试该管的事)。
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const okrModule = require('../../services/okr-service');
const memoryModule = require('../../services/memory-service');
const originalOkrService = okrModule.OkrService;
const originalRetrieveRelevantPlans = memoryModule.retrieveRelevantPlans;
describe('identity-context.buildIdentityContextParts', () => {
let bodyDir;
let buildIdentityContextParts;
let okrCalls;
let hippocampusCalls;
beforeEach(() => {
bodyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'identity-ctx-body-'));
okrCalls = [];
hippocampusCalls = [];
// 原地换成假实现:identity-context.js 的 require('../../services/okr-service')
// 拿到的是同一个 module.exports 对象,属性换了它就用的是假的。
okrModule.OkrService = function FakeOkrService() {
return {
getActiveContextForAgent(agentId) {
okrCalls.push(agentId);
return agentId === 'has-goal' ? '## 🎯 当前参与的目标\n\n模拟目标文本' : '';
},
};
};
memoryModule.retrieveRelevantPlans = (agentId, searchText) => {
hippocampusCalls.push([agentId, searchText]);
return agentId === 'has-goal' ? `模拟检索结果(搜索词:${searchText})` : '';
};
delete require.cache[require.resolve('../../src/core/identity-context.js')];
({ buildIdentityContextParts } = require('../../src/core/identity-context.js'));
});
afterEach(() => {
okrModule.OkrService = originalOkrService;
memoryModule.retrieveRelevantPlans = originalRetrieveRelevantPlans;
delete require.cache[require.resolve('../../src/core/identity-context.js')];
fs.rmSync(bodyDir, { recursive: true, force: true });
});
function write(rel, content) {
const fp = path.join(bodyDir, rel);
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, content);
}
it('bodyDir 为空时返回空数组,不抛错', () => {
expect(buildIdentityContextParts({ bodyDir: null })).toEqual([]);
});
it('soul.md/identity.md/tools.md 都写了时按顺序读出,缺的那个跳过', () => {
write('soul.md', 'MY SOUL');
write('tools.md', 'MY TOOLS'); // identity.md 故意不写
const parts = buildIdentityContextParts({ bodyDir });
expect(parts).toContain('MY SOUL');
expect(parts).toContain('MY TOOLS');
expect(parts.join('\n')).not.toContain('identity.md');
});
it('heartbeat.md 存在时读入,不存在时跳过(不报错)', () => {
expect(buildIdentityContextParts({ bodyDir })).not.toContain(expect.stringContaining('HEARTBEAT'));
write('heartbeat.md', 'HEARTBEAT CONTENT');
expect(buildIdentityContextParts({ bodyDir })).toContain('HEARTBEAT CONTENT');
});
it('没有 agentId 时不查 OKR / hippocampus(不是「查了但是空」,是根本不查)', () => {
buildIdentityContextParts({ bodyDir });
expect(okrCalls).toEqual([]);
expect(hippocampusCalls).toEqual([]);
});
it('有 agentId 时查 OKR 与 hippocampus,结果非空才拼进去', () => {
const parts = buildIdentityContextParts({ bodyDir, agentId: 'has-goal', searchText: '写一份 PRD' });
expect(parts.some((p) => p.includes('模拟目标文本'))).toBe(true);
expect(parts.some((p) => p.includes('模拟检索结果'))).toBe(true);
expect(hippocampusCalls).toContainEqual(['has-goal', '写一份 PRD']);
// agentId 存在但两个依赖都返回空字符串——不拼空段落
const empty = buildIdentityContextParts({ bodyDir, agentId: 'no-goal', searchText: 'x' });
expect(empty.some((p) => p.includes('目标') || p.includes('检索'))).toBe(false);
});
it('OKR 查询抛错时不影响其它内容,只是那一段缺失(不整体失败)', () => {
okrModule.OkrService = function ThrowingOkrService() {
return { getActiveContextForAgent: () => { throw new Error('boom'); } };
};
delete require.cache[require.resolve('../../src/core/identity-context.js')];
({ buildIdentityContextParts } = require('../../src/core/identity-context.js'));
write('soul.md', 'STILL HERE');
const parts = buildIdentityContextParts({ bodyDir, agentId: 'whoever' });
expect(parts).toContain('STILL HERE');
});
it('main_skill/SKILL.md 存在时带专属标题拼入', () => {
write('.claude/skills/main_skill/SKILL.md', 'MAIN SKILL BODY');
const parts = buildIdentityContextParts({ bodyDir });
expect(parts.some((p) => p.includes('我的核心主技能') && p.includes('MAIN SKILL BODY'))).toBe(true);
});
it('workspace/_lessons.md 存在时带警示标题拼入', () => {
write('workspace/_lessons.md', '别再犯这个错');
const parts = buildIdentityContextParts({ bodyDir });
expect(parts.some((p) => p.includes('本地试错防坑经验') && p.includes('别再犯这个错'))).toBe(true);
});
it('skills.md 与其它 skill 子目录(排除 main_skill)都被扫到', () => {
write('.claude/skills/skills.md', 'SKILLS INDEX');
write('.claude/skills/main_skill/SKILL.md', 'MAIN'); // 不应该被当成「依赖技能」再扫一次
write('.claude/skills/foo-skill/SKILL.md', 'FOO SKILL BODY');
write('.claude/skills/bar-skill/SKILL.md', 'BAR SKILL BODY');
const parts = buildIdentityContextParts({ bodyDir });
expect(parts).toContain('SKILLS INDEX');
expect(parts.some((p) => p.includes('依赖技能: foo-skill') && p.includes('FOO SKILL BODY'))).toBe(true);
expect(parts.some((p) => p.includes('依赖技能: bar-skill') && p.includes('BAR SKILL BODY'))).toBe(true);
// main_skill 不能又被当成一个「依赖技能」重复拼一次
expect(parts.filter((p) => p.includes('MAIN')).length).toBe(1);
});
it('.claude/skills 目录本身不存在时不报错,其它内容照常返回', () => {
write('soul.md', 'OK');
expect(() => buildIdentityContextParts({ bodyDir })).not.toThrow();
expect(buildIdentityContextParts({ bodyDir })).toContain('OK');
});
});