experience-lookup.test.js
6.39 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
/**
* 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('长任务描述 + 通用工程词汇的经验只是偶然重合 1-2 个 bigram 时不算命中'
+ '(真实回归事故复现:46 号文档 Phase 2 真实端到端验证时,一个贪吃蛇任务把'
+ '"验证修复要重新触发流程"这类完全不相关的工程经验当成了"经验库有答案")', () => {
hire(COMPANY, AGENT);
writeExperience(AGENT, [
{ title: '验证修复要重新触发流程看真实产出', body: '不能只信别人汇报,要亲自跑一遍确认修复真的生效' },
]);
const longTask = {
title: '实现命令行贪吃蛇游戏',
description: '用 Node.js 实现一个可以在终端里运行和游玩的贪吃蛇游戏。要求:使用方向键控制蛇的移动'
+ '方向;蛇吃到食物后身体变长,并在场景中重新生成新的食物;蛇撞到边界或撞到自己身体时游戏立即'
+ '结束,并在结束时显示本局得分。达成判据:在终端执行启动命令后能看到游戏画面并用方向键操作蛇'
+ '移动;吃到食物后蛇身变长且得分增加;撞墙或撞到自己身体后游戏停止并打印出最终分数。',
};
const { matches } = queryExperience({ companyId: COMPANY, task: longTask });
expect(matches).toEqual([]);
});
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('的');
});
});