experience-lookup.test.js
5.1 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
/**
* 46 号文档 Phase 2:experience-lookup 查经验库。
*
* 夹具模式照抄 `dream-service.test.js`:真实磁盘下建一个 `zz-explookup-*` 测试 agentId,
* 写 `memory/experience.md`(dream `writeDistilled` 落的同一种格式),`company_agents`
* 表插入雇佣关系——测的是"读得到 dream 已经写好的经验"这一半,不是 dream 本身。
*/
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'lemonboss-explookup-'));
process.env.GRAPH_DB_PATH = path.join(TMP, 'graph.sqlite');
const ROOT = path.resolve(__dirname, '../../..');
const AGENT = 'zz-explookup-body';
const OTHER_AGENT = 'zz-explookup-other';
const COMPANY = 'zz-explookup-co';
const BODY = path.join(ROOT, 'system', 'agents', AGENT);
const OTHER_BODY = path.join(ROOT, 'system', 'agents', OTHER_AGENT);
const { queryExperience, tokenize, readExperienceBlocks } = require('../../services/experience-lookup');
const wdb = require('../../services/work-graph-db');
function writeExperience(agentId, blocks) {
const dir = path.join(ROOT, 'system', 'agents', agentId, 'memory');
fs.mkdirSync(dir, { recursive: true });
const body = blocks.map((b) => `### ${b.title}\n\n${b.body}\n\n> 提炼于 2026-09-01;原料:x:memory.md#1\n`).join('\n');
fs.writeFileSync(path.join(dir, 'experience.md'), `# 跨公司经验(dream 提炼)\n\n${body}`, 'utf-8');
}
function hire(companyId, agentId) {
const db = wdb.getDb();
const now = new Date().toISOString();
db.prepare(`INSERT OR IGNORE INTO system_agents
(id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
.run(agentId, agentId, `system/agents/${agentId}`, now, now);
db.prepare(`INSERT OR IGNORE INTO company_agents (company_id, system_agent_id, role_name, status, hired_at)
VALUES (?, ?, ?, 'active', ?)`).run(companyId, agentId, agentId, now);
}
describe('experience-lookup · queryExperience', () => {
beforeEach(() => {
fs.rmSync(BODY, { recursive: true, force: true });
fs.rmSync(OTHER_BODY, { recursive: true, force: true });
});
afterAll(() => {
fs.rmSync(BODY, { recursive: true, force: true });
fs.rmSync(OTHER_BODY, { recursive: true, force: true });
try { fs.rmSync(TMP, { recursive: true, force: true }); } catch {}
});
it('命中相关经验,按关键词重叠打分排序', () => {
hire(COMPANY, AGENT);
writeExperience(AGENT, [
{ title: '贪吃蛇碰撞检测踩过的坑', body: '棋盘边界判断要在移动前做,不然会穿墙' },
{ title: '完全不相关的经验', body: '发工资流程走财务系统,不归这里管' },
]);
const { matches, queriedAgents } = queryExperience({
companyId: COMPANY,
task: { title: '实现贪吃蛇', description: '贪吃蛇游戏的碰撞检测和棋盘边界' },
});
expect(queriedAgents).toContain(AGENT);
expect(matches.length).toBeGreaterThan(0);
expect(matches[0].title).toBe('贪吃蛇碰撞检测踩过的坑');
expect(matches[0].agentId).toBe(AGENT);
expect(matches[0].score).toBeGreaterThan(0);
// 不相关的那条不该排在命中结果里靠前的位置(分数应明显更低或不命中)
const irrelevant = matches.find((m) => m.title === '完全不相关的经验');
if (irrelevant) expect(irrelevant.score).toBeLessThan(matches[0].score);
});
it('没有命中时返回空数组,不是异常', () => {
hire(COMPANY, AGENT);
writeExperience(AGENT, [{ title: '完全不相关的经验', body: '发工资流程走财务系统' }]);
const { matches } = queryExperience({
companyId: COMPANY,
task: { title: '实现贪吃蛇', description: '碰撞检测' },
});
expect(matches).toEqual([]);
});
it('agent 没有 experience.md 文件时不报错,视为该 agent 无命中', () => {
hire(COMPANY, AGENT); // 雇了但从没被 dream 写过经验
expect(() => queryExperience({
companyId: COMPANY,
task: { title: '实现贪吃蛇', description: '碰撞检测' },
})).not.toThrow();
expect(readExperienceBlocks(AGENT)).toEqual([]);
});
it('公司没有雇任何 agent 时返回空数组', () => {
const { matches, queriedAgents } = queryExperience({
companyId: 'zz-explookup-empty-co',
task: { title: '实现贪吃蛇', description: '碰撞检测' },
});
expect(matches).toEqual([]);
expect(queriedAgents).toEqual([]);
});
it('task 标题/描述都为空时返回空数组,不做无意义的全量扫描', () => {
hire(COMPANY, AGENT);
const { matches } = queryExperience({ companyId: COMPANY, task: {} });
expect(matches).toEqual([]);
});
it('tokenize:拉丁词整段切,中文用双字滑窗(无分词依赖),单字符丢弃', () => {
const tokens = tokenize('贪吃蛇 game 的 A 实现');
expect(tokens).toEqual(expect.arrayContaining(['贪吃', '吃蛇', 'game', '实现']));
expect(tokens).not.toContain('a');
expect(tokens).not.toContain('的');
});
});