@mclawnet/mcp-server 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -4
- package/src/__tests__/e2e-memory-pipeline.test.ts +0 -627
- package/src/__tests__/evolution-tools.test.ts +0 -94
- package/src/__tests__/memory-tools.test.ts +0 -259
- package/src/__tests__/skill-tools.test.ts +0 -78
- package/src/index.ts +0 -3
- package/src/server.ts +0 -77
- package/src/tools/evolution.ts +0 -157
- package/src/tools/index.ts +0 -46
- package/src/tools/memory.ts +0 -280
- package/src/tools/skill.ts +0 -79
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mclawnet/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
"bin": {
|
|
13
13
|
"clawnet-mcp": "dist/server.js"
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
15
21
|
"dependencies": {
|
|
16
22
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
17
|
-
"@mclawnet/logger": "0.1.
|
|
18
|
-
"@mclawnet/
|
|
19
|
-
"@mclawnet/
|
|
23
|
+
"@mclawnet/logger": "0.1.4",
|
|
24
|
+
"@mclawnet/memory": "0.1.3",
|
|
25
|
+
"@mclawnet/skill-manager": "0.1.2"
|
|
20
26
|
},
|
|
21
27
|
"devDependencies": {
|
|
22
28
|
"@types/better-sqlite3": "^7",
|
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2E Memory Pipeline Test
|
|
3
|
-
*
|
|
4
|
-
* Tests the full memory lifecycle as it flows through a swarm session:
|
|
5
|
-
* 1. Role stores memories via MCP tools (Pipeline B)
|
|
6
|
-
* 2. Swarm retrospective writes collaboration memories + role_stats
|
|
7
|
-
* 3. AutoMemorySync exports to auto memory files
|
|
8
|
-
* 4. buildMemorySection reads working memories for prompt injection (Pipeline A)
|
|
9
|
-
* 5. Distillation extracts experience from conversations
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
12
|
-
import { mkdtempSync, rmSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import type { Database as DatabaseType } from "better-sqlite3";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
buildMemorySection,
|
|
19
|
-
distillConversation,
|
|
20
|
-
EmbeddingService,
|
|
21
|
-
initDatabase,
|
|
22
|
-
MemoryStore,
|
|
23
|
-
syncToAutoMemory,
|
|
24
|
-
} from "@mclawnet/memory";
|
|
25
|
-
import {
|
|
26
|
-
handleMemoryToolCall,
|
|
27
|
-
type MemoryToolContext,
|
|
28
|
-
} from "../tools/memory.js";
|
|
29
|
-
|
|
30
|
-
// In the original memory package, ToolContext carried roleId. The mcp-server
|
|
31
|
-
// API accepts roleId per call. Adapt by wrapping the call site so existing
|
|
32
|
-
// test logic stays unchanged.
|
|
33
|
-
type ToolContext = MemoryToolContext & { roleId: string };
|
|
34
|
-
|
|
35
|
-
function handleToolCall(
|
|
36
|
-
name: string,
|
|
37
|
-
args: Record<string, unknown>,
|
|
38
|
-
ctx: ToolContext,
|
|
39
|
-
) {
|
|
40
|
-
const merged = { roleId: ctx.roleId, ...args };
|
|
41
|
-
return handleMemoryToolCall(name, merged, { store: ctx.store, embeddingService: ctx.embeddingService });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Shared state ──────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
let db: DatabaseType;
|
|
47
|
-
let store: MemoryStore;
|
|
48
|
-
let embeddingService: EmbeddingService;
|
|
49
|
-
let tmpDir: string;
|
|
50
|
-
let dbPath: string;
|
|
51
|
-
let memoryDir: string;
|
|
52
|
-
|
|
53
|
-
// Contexts for different roles
|
|
54
|
-
let devCtx: ToolContext;
|
|
55
|
-
let queenCtx: ToolContext;
|
|
56
|
-
|
|
57
|
-
// Track stored memory IDs for later assertions
|
|
58
|
-
const storedIds: string[] = [];
|
|
59
|
-
|
|
60
|
-
beforeAll(() => {
|
|
61
|
-
tmpDir = mkdtempSync(join(tmpdir(), "e2e-memory-pipeline-"));
|
|
62
|
-
dbPath = join(tmpDir, "test.db");
|
|
63
|
-
memoryDir = join(tmpDir, "auto-memory");
|
|
64
|
-
|
|
65
|
-
db = initDatabase(dbPath);
|
|
66
|
-
store = new MemoryStore(db);
|
|
67
|
-
embeddingService = new EmbeddingService(db);
|
|
68
|
-
|
|
69
|
-
devCtx = { store, embeddingService, roleId: "role-developer" };
|
|
70
|
-
queenCtx = { store, embeddingService, roleId: "role-queen" };
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
afterAll(() => {
|
|
74
|
-
db.close();
|
|
75
|
-
if (existsSync(tmpDir)) {
|
|
76
|
-
rmSync(tmpDir, { recursive: true });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ── Full lifecycle ────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
describe("Memory Pipeline E2E", () => {
|
|
83
|
-
it("full lifecycle: store -> search -> retrospective -> sync -> prompt inject", async () => {
|
|
84
|
-
// ── Step 1: Store 3 memories via MCP handleToolCall ──────────────
|
|
85
|
-
|
|
86
|
-
const mem1 = await handleToolCall(
|
|
87
|
-
"memory_store",
|
|
88
|
-
{
|
|
89
|
-
content: "使用连接池提升性能3倍",
|
|
90
|
-
type: "experience",
|
|
91
|
-
domain: "backend",
|
|
92
|
-
importance: 0.8,
|
|
93
|
-
},
|
|
94
|
-
devCtx,
|
|
95
|
-
);
|
|
96
|
-
const parsed1 = JSON.parse(mem1.content[0].text);
|
|
97
|
-
expect(parsed1.success).toBe(true);
|
|
98
|
-
storedIds.push(parsed1.id);
|
|
99
|
-
|
|
100
|
-
const mem2 = await handleToolCall(
|
|
101
|
-
"memory_store",
|
|
102
|
-
{
|
|
103
|
-
content: "不要在WAL模式下vacuum",
|
|
104
|
-
type: "error",
|
|
105
|
-
domain: "database",
|
|
106
|
-
importance: 0.9,
|
|
107
|
-
},
|
|
108
|
-
devCtx,
|
|
109
|
-
);
|
|
110
|
-
const parsed2 = JSON.parse(mem2.content[0].text);
|
|
111
|
-
expect(parsed2.success).toBe(true);
|
|
112
|
-
storedIds.push(parsed2.id);
|
|
113
|
-
|
|
114
|
-
const mem3 = await handleToolCall(
|
|
115
|
-
"memory_store",
|
|
116
|
-
{
|
|
117
|
-
content: "ESM优先于CJS",
|
|
118
|
-
type: "preference",
|
|
119
|
-
domain: "tooling",
|
|
120
|
-
importance: 0.6,
|
|
121
|
-
},
|
|
122
|
-
devCtx,
|
|
123
|
-
);
|
|
124
|
-
const parsed3 = JSON.parse(mem3.content[0].text);
|
|
125
|
-
expect(parsed3.success).toBe(true);
|
|
126
|
-
storedIds.push(parsed3.id);
|
|
127
|
-
|
|
128
|
-
// ── Step 2: Search "数据库" finds related memory ─────────────────
|
|
129
|
-
|
|
130
|
-
const searchResult = await handleToolCall(
|
|
131
|
-
"memory_search",
|
|
132
|
-
{ query: "数据库性能" },
|
|
133
|
-
devCtx,
|
|
134
|
-
);
|
|
135
|
-
expect(searchResult.isError).toBeUndefined();
|
|
136
|
-
const searchParsed = JSON.parse(searchResult.content[0].text);
|
|
137
|
-
expect(searchParsed).toBeInstanceOf(Array);
|
|
138
|
-
expect(searchParsed.length).toBeGreaterThan(0);
|
|
139
|
-
|
|
140
|
-
// ── Step 3: memory_stats returns correct statistics ──────────────
|
|
141
|
-
|
|
142
|
-
const statsResult = await handleToolCall("memory_stats", {}, devCtx);
|
|
143
|
-
const stats = JSON.parse(statsResult.content[0].text);
|
|
144
|
-
expect(stats.totalMemories).toBe(3);
|
|
145
|
-
expect(stats.byType.experience).toBe(1);
|
|
146
|
-
expect(stats.byType.error).toBe(1);
|
|
147
|
-
expect(stats.byType.preference).toBe(1);
|
|
148
|
-
expect(stats.topDomains.length).toBeGreaterThanOrEqual(3);
|
|
149
|
-
|
|
150
|
-
// ── Step 4: Simulate swarm retrospective (manual persistToDb) ───
|
|
151
|
-
// Write collaboration memories as the queen role + populate role_stats
|
|
152
|
-
|
|
153
|
-
await store.addMemoryWithEmbedding(
|
|
154
|
-
{
|
|
155
|
-
roleId: "role-queen",
|
|
156
|
-
content: "Developer与Reviewer并行工作效率最高",
|
|
157
|
-
type: "pattern",
|
|
158
|
-
level: "long-term",
|
|
159
|
-
domain: "collaboration",
|
|
160
|
-
importance: 0.85,
|
|
161
|
-
sourceSwarmIds: ["swarm-test-001"],
|
|
162
|
-
},
|
|
163
|
-
embeddingService,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
await store.addMemoryWithEmbedding(
|
|
167
|
-
{
|
|
168
|
-
roleId: "role-queen",
|
|
169
|
-
content: "分阶段交付比一次性交付风险更低",
|
|
170
|
-
type: "experience",
|
|
171
|
-
level: "long-term",
|
|
172
|
-
domain: "collaboration",
|
|
173
|
-
importance: 0.75,
|
|
174
|
-
sourceSwarmIds: ["swarm-test-001"],
|
|
175
|
-
},
|
|
176
|
-
embeddingService,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// Write role_stats directly
|
|
180
|
-
db.prepare(
|
|
181
|
-
`INSERT OR REPLACE INTO role_stats
|
|
182
|
-
(role_id, tasks_completed, tasks_failed, avg_quality_score, avg_rework_count, total_memories, domains, updated_at)
|
|
183
|
-
VALUES (@roleId, @completed, @failed, @quality, @rework, @total, @domains, @now)`,
|
|
184
|
-
).run({
|
|
185
|
-
roleId: "role-developer",
|
|
186
|
-
completed: 5,
|
|
187
|
-
failed: 1,
|
|
188
|
-
quality: 0.85,
|
|
189
|
-
rework: 0.2,
|
|
190
|
-
total: 3,
|
|
191
|
-
domains: JSON.stringify(["backend", "database", "tooling"]),
|
|
192
|
-
now: Date.now(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Verify collaboration memories are in DB
|
|
196
|
-
const collabMemories = store.getMemoriesByRole("role-queen");
|
|
197
|
-
const collabDomainMemories = collabMemories.filter((m) => m.domain === "collaboration");
|
|
198
|
-
expect(collabDomainMemories.length).toBeGreaterThanOrEqual(2);
|
|
199
|
-
|
|
200
|
-
// Verify role_stats has data
|
|
201
|
-
const roleStatsRow = db
|
|
202
|
-
.prepare("SELECT * FROM role_stats WHERE role_id = ?")
|
|
203
|
-
.get("role-developer") as Record<string, unknown> | undefined;
|
|
204
|
-
expect(roleStatsRow).toBeDefined();
|
|
205
|
-
expect(roleStatsRow!.tasks_completed).toBe(5);
|
|
206
|
-
|
|
207
|
-
// ── Step 5: syncToAutoMemory -> verify output files ──────────────
|
|
208
|
-
|
|
209
|
-
const syncResult = syncToAutoMemory(db, {
|
|
210
|
-
projectPath: "/test-project",
|
|
211
|
-
memoryDir,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(syncResult.filesWritten).toContain("MEMORY.md");
|
|
215
|
-
expect(syncResult.totalEntries).toBeGreaterThanOrEqual(4);
|
|
216
|
-
|
|
217
|
-
// Verify errors.md contains our WAL vacuum memory
|
|
218
|
-
expect(syncResult.filesWritten).toContain("errors.md");
|
|
219
|
-
const errorsContent = readFileSync(join(memoryDir, "errors.md"), "utf-8");
|
|
220
|
-
expect(errorsContent).toContain("不要在WAL模式下vacuum");
|
|
221
|
-
|
|
222
|
-
// Verify patterns.md contains our connection pool experience
|
|
223
|
-
expect(syncResult.filesWritten).toContain("patterns.md");
|
|
224
|
-
const patternsContent = readFileSync(join(memoryDir, "patterns.md"), "utf-8");
|
|
225
|
-
expect(patternsContent).toContain("使用连接池提升性能3倍");
|
|
226
|
-
|
|
227
|
-
// Verify collaboration.md contains retrospective memories
|
|
228
|
-
expect(syncResult.filesWritten).toContain("collaboration.md");
|
|
229
|
-
const collabContent = readFileSync(join(memoryDir, "collaboration.md"), "utf-8");
|
|
230
|
-
expect(collabContent).toContain("Developer与Reviewer并行工作效率最高");
|
|
231
|
-
|
|
232
|
-
// Verify MEMORY.md index has "Top Memories" and topic file links
|
|
233
|
-
const indexContent = readFileSync(join(memoryDir, "MEMORY.md"), "utf-8");
|
|
234
|
-
expect(indexContent).toContain("Top Memories");
|
|
235
|
-
expect(indexContent).toContain("[errors.md](./errors.md)");
|
|
236
|
-
|
|
237
|
-
// ── Step 6: Set memory to working level -> buildMemorySection ────
|
|
238
|
-
|
|
239
|
-
store.updateMemory(storedIds[0], { level: "working" }); // 连接池 experience
|
|
240
|
-
store.updateMemory(storedIds[1], { level: "working" }); // WAL vacuum error
|
|
241
|
-
|
|
242
|
-
// Close db before buildMemorySection (it opens its own connection)
|
|
243
|
-
db.close();
|
|
244
|
-
|
|
245
|
-
const section = buildMemorySection("role-developer", dbPath);
|
|
246
|
-
expect(section).toContain("使用连接池提升性能3倍");
|
|
247
|
-
expect(section).toContain("不要在WAL模式下vacuum");
|
|
248
|
-
// Should contain role-specific instructions
|
|
249
|
-
expect(section).toContain("## 开发者专属记忆指南");
|
|
250
|
-
// Should NOT have the raw placeholder
|
|
251
|
-
expect(section).not.toContain("{workingMemories}");
|
|
252
|
-
|
|
253
|
-
// Re-open db for subsequent tests
|
|
254
|
-
db = initDatabase(dbPath);
|
|
255
|
-
store = new MemoryStore(db);
|
|
256
|
-
embeddingService = new EmbeddingService(db);
|
|
257
|
-
devCtx = { store, embeddingService, roleId: "role-developer" };
|
|
258
|
-
queenCtx = { store, embeddingService, roleId: "role-queen" };
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// ── Cross-role search ──────────────────────────────────────────────
|
|
262
|
-
|
|
263
|
-
it("cross-role search: developer finds queen collaboration memories", async () => {
|
|
264
|
-
// Search as developer with crossRole: true should find queen's collaboration memories
|
|
265
|
-
const result = await handleToolCall(
|
|
266
|
-
"memory_search",
|
|
267
|
-
{ query: "协作模式并行工作", crossRole: true },
|
|
268
|
-
devCtx,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
272
|
-
expect(parsed.length).toBeGreaterThan(0);
|
|
273
|
-
|
|
274
|
-
// At least one result should be from queen and about collaboration
|
|
275
|
-
const queenResults = parsed.filter(
|
|
276
|
-
(m: { roleId: string }) => m.roleId === "role-queen",
|
|
277
|
-
);
|
|
278
|
-
expect(queenResults.length).toBeGreaterThan(0);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// ── Importance filter ──────────────────────────────────────────────
|
|
282
|
-
|
|
283
|
-
it("importance filter: low-importance excluded from sync", async () => {
|
|
284
|
-
// Store a low-importance memory
|
|
285
|
-
await store.addMemoryWithEmbedding(
|
|
286
|
-
{
|
|
287
|
-
roleId: "role-developer",
|
|
288
|
-
content: "低优先级记忆:临时调试信息",
|
|
289
|
-
type: "experience",
|
|
290
|
-
domain: "debug",
|
|
291
|
-
importance: 0.3,
|
|
292
|
-
},
|
|
293
|
-
embeddingService,
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
// Re-sync with default minImportance (0.5)
|
|
297
|
-
const secondMemoryDir = join(tmpDir, "auto-memory-filter");
|
|
298
|
-
const syncResult = syncToAutoMemory(db, {
|
|
299
|
-
projectPath: "/test-project",
|
|
300
|
-
memoryDir: secondMemoryDir,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Low-importance memory should not appear in any output file
|
|
304
|
-
for (const filename of syncResult.filesWritten) {
|
|
305
|
-
const content = readFileSync(join(secondMemoryDir, filename), "utf-8");
|
|
306
|
-
expect(content).not.toContain("低优先级记忆:临时调试信息");
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// ── Distillation roundtrip ─────────────────────────────────────────
|
|
311
|
-
|
|
312
|
-
it("distillation -> search roundtrip", async () => {
|
|
313
|
-
const messages = [
|
|
314
|
-
{ role: "user", content: "这个部署脚本怎么写?" },
|
|
315
|
-
{
|
|
316
|
-
role: "assistant",
|
|
317
|
-
content:
|
|
318
|
-
"best practice: 部署脚本应该是幂等的,使用健康检查确认服务正常启动后再切流量",
|
|
319
|
-
},
|
|
320
|
-
{ role: "user", content: "还有什么注意事项?" },
|
|
321
|
-
{
|
|
322
|
-
role: "assistant",
|
|
323
|
-
content: "注意:回滚策略必须提前准备好,不要等出问题才想",
|
|
324
|
-
},
|
|
325
|
-
];
|
|
326
|
-
|
|
327
|
-
const distillResult = await distillConversation(
|
|
328
|
-
messages,
|
|
329
|
-
"role-developer",
|
|
330
|
-
store,
|
|
331
|
-
embeddingService,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
expect(distillResult.extracted).toBeGreaterThanOrEqual(1);
|
|
335
|
-
|
|
336
|
-
// Now search for the distilled memory via MCP tool
|
|
337
|
-
const searchResult = await handleToolCall(
|
|
338
|
-
"memory_search",
|
|
339
|
-
{ query: "部署脚本幂等健康检查" },
|
|
340
|
-
devCtx,
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
const parsed = JSON.parse(searchResult.content[0].text);
|
|
344
|
-
expect(parsed.length).toBeGreaterThan(0);
|
|
345
|
-
// At least one result should contain content from distillation
|
|
346
|
-
const hasDistilled = parsed.some(
|
|
347
|
-
(m: { content: string }) =>
|
|
348
|
-
m.content.includes("幂等") || m.content.includes("回滚"),
|
|
349
|
-
);
|
|
350
|
-
expect(hasDistilled).toBe(true);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// ── Idempotent sync ────────────────────────────────────────────────
|
|
354
|
-
|
|
355
|
-
it("idempotent sync: repeated sync produces same files", () => {
|
|
356
|
-
const idempotentDir = join(tmpDir, "auto-memory-idempotent");
|
|
357
|
-
|
|
358
|
-
const result1 = syncToAutoMemory(db, {
|
|
359
|
-
projectPath: "/test-project",
|
|
360
|
-
memoryDir: idempotentDir,
|
|
361
|
-
});
|
|
362
|
-
const files1 = result1.filesWritten.map((f) =>
|
|
363
|
-
readFileSync(join(idempotentDir, f), "utf-8"),
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const result2 = syncToAutoMemory(db, {
|
|
367
|
-
projectPath: "/test-project",
|
|
368
|
-
memoryDir: idempotentDir,
|
|
369
|
-
});
|
|
370
|
-
const files2 = result2.filesWritten.map((f) =>
|
|
371
|
-
readFileSync(join(idempotentDir, f), "utf-8"),
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
expect(result1.filesWritten).toEqual(result2.filesWritten);
|
|
375
|
-
expect(result1.totalEntries).toEqual(result2.totalEntries);
|
|
376
|
-
expect(files1).toEqual(files2);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// ── Sprint 3+4 E2E Tests ─────────────────────────────────────────────
|
|
381
|
-
|
|
382
|
-
describe("E2E: Fisher Fusion conflict detection", () => {
|
|
383
|
-
it("should detect and resolve duplicate content via MCP memory_store", async () => {
|
|
384
|
-
// Store a memory first
|
|
385
|
-
const result1 = await handleToolCall(
|
|
386
|
-
"memory_store",
|
|
387
|
-
{ content: "Always use TypeScript strict mode", type: "pattern", importance: 0.6 },
|
|
388
|
-
devCtx,
|
|
389
|
-
);
|
|
390
|
-
const parsed1 = JSON.parse(result1.content[0].text);
|
|
391
|
-
expect(parsed1.success).toBe(true);
|
|
392
|
-
expect(parsed1.fusionAction).toBe("keep_both"); // first store, no conflict
|
|
393
|
-
|
|
394
|
-
// Store identical content — should trigger fusion
|
|
395
|
-
const result2 = await handleToolCall(
|
|
396
|
-
"memory_store",
|
|
397
|
-
{ content: "Always use TypeScript strict mode", type: "pattern", importance: 0.7 },
|
|
398
|
-
devCtx,
|
|
399
|
-
);
|
|
400
|
-
const parsed2 = JSON.parse(result2.content[0].text);
|
|
401
|
-
expect(parsed2.success).toBe(true);
|
|
402
|
-
// With hash embeddings, identical text will have cosine=1.0, triggering conflict
|
|
403
|
-
// The resolution depends on fisherWeight comparison
|
|
404
|
-
expect(["keep_old", "keep_new", "merge"]).toContain(parsed2.fusionAction);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("should keep_both for different content", async () => {
|
|
408
|
-
await handleToolCall(
|
|
409
|
-
"memory_store",
|
|
410
|
-
{ content: "React hooks need dependency arrays", type: "pattern" },
|
|
411
|
-
devCtx,
|
|
412
|
-
);
|
|
413
|
-
const result = await handleToolCall(
|
|
414
|
-
"memory_store",
|
|
415
|
-
{ content: "Database indexes improve query speed", type: "experience" },
|
|
416
|
-
devCtx,
|
|
417
|
-
);
|
|
418
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
419
|
-
expect(parsed.fusionAction).toBe("keep_both");
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
describe("E2E: Importance pruning via memory_reflect", () => {
|
|
424
|
-
it("should prune working memories exceeding cap via light reflect", async () => {
|
|
425
|
-
// Add 55 working-level memories (WORKING_CAP = 50)
|
|
426
|
-
for (let i = 0; i < 55; i++) {
|
|
427
|
-
store.addMemory({
|
|
428
|
-
roleId: "role-developer",
|
|
429
|
-
content: `working memory item ${i}`,
|
|
430
|
-
type: "experience",
|
|
431
|
-
level: "working",
|
|
432
|
-
importance: 0.3 + (i % 10) * 0.05,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const result = await handleToolCall(
|
|
437
|
-
"memory_reflect",
|
|
438
|
-
{ scope: "light" },
|
|
439
|
-
devCtx,
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
443
|
-
expect(parsed.scope).toBe("light");
|
|
444
|
-
expect(parsed.demotedToLongTerm).toBeGreaterThan(0);
|
|
445
|
-
// After pruning, working count should be <= 50
|
|
446
|
-
const stats = store.getStats("role-developer");
|
|
447
|
-
expect(stats.byLevel.working).toBeLessThanOrEqual(50);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("should recalculate importance and prune all roles via full reflect", async () => {
|
|
451
|
-
// Add memories for two different roles
|
|
452
|
-
for (let i = 0; i < 10; i++) {
|
|
453
|
-
store.addMemory({
|
|
454
|
-
roleId: "role-developer",
|
|
455
|
-
content: `dev memory ${i}`,
|
|
456
|
-
type: "experience",
|
|
457
|
-
});
|
|
458
|
-
store.addMemory({
|
|
459
|
-
roleId: "role-reviewer",
|
|
460
|
-
content: `reviewer memory ${i}`,
|
|
461
|
-
type: "pattern",
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const result = await handleToolCall(
|
|
466
|
-
"memory_reflect",
|
|
467
|
-
{ scope: "full" },
|
|
468
|
-
devCtx,
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
472
|
-
expect(parsed.scope).toBe("full");
|
|
473
|
-
expect(parsed.recalculated).toBeGreaterThanOrEqual(20);
|
|
474
|
-
expect(parsed.rolesProcessed).toBeGreaterThanOrEqual(2);
|
|
475
|
-
expect(typeof parsed.profilesRefreshed).toBe("number");
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
describe("E2E: VectorIndex accelerated search", () => {
|
|
480
|
-
it("should return correct results using vector index fast path", async () => {
|
|
481
|
-
// Store several memories with embeddings via MCP
|
|
482
|
-
const topics = [
|
|
483
|
-
"React component lifecycle methods",
|
|
484
|
-
"Database connection pooling strategies",
|
|
485
|
-
"CSS grid layout techniques",
|
|
486
|
-
"Node.js stream processing patterns",
|
|
487
|
-
"Git branching workflow best practices",
|
|
488
|
-
];
|
|
489
|
-
|
|
490
|
-
for (const topic of topics) {
|
|
491
|
-
await handleToolCall(
|
|
492
|
-
"memory_store",
|
|
493
|
-
{ content: topic, type: "experience" },
|
|
494
|
-
devCtx,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Search should use vectorIndex fast path (index has entries)
|
|
499
|
-
const result = await handleToolCall(
|
|
500
|
-
"memory_search",
|
|
501
|
-
{ query: "React component lifecycle methods", limit: 3 },
|
|
502
|
-
devCtx,
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
506
|
-
expect(parsed.length).toBeGreaterThan(0);
|
|
507
|
-
// The exact match should be first or near the top
|
|
508
|
-
expect(parsed[0].content).toBe("React component lifecycle methods");
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe("E2E: Consolidation full pipeline", () => {
|
|
513
|
-
it("should run full consolidation with drift check and domain balance", async () => {
|
|
514
|
-
// Use a dedicated role so domain percentages are not diluted by prior tests
|
|
515
|
-
const consolidationRole = "role-consolidation-test";
|
|
516
|
-
|
|
517
|
-
// Store memories with embeddings across domains
|
|
518
|
-
for (let i = 0; i < 12; i++) {
|
|
519
|
-
await store.addMemoryWithEmbedding(
|
|
520
|
-
{
|
|
521
|
-
roleId: consolidationRole,
|
|
522
|
-
content: `frontend pattern ${i}`,
|
|
523
|
-
type: "pattern",
|
|
524
|
-
domain: "frontend",
|
|
525
|
-
},
|
|
526
|
-
embeddingService,
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
// Add a few in a different domain
|
|
530
|
-
for (let i = 0; i < 3; i++) {
|
|
531
|
-
await store.addMemoryWithEmbedding(
|
|
532
|
-
{
|
|
533
|
-
roleId: consolidationRole,
|
|
534
|
-
content: `backend experience ${i}`,
|
|
535
|
-
type: "experience",
|
|
536
|
-
domain: "backend",
|
|
537
|
-
},
|
|
538
|
-
embeddingService,
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Run consolidation
|
|
543
|
-
const { runConsolidation } = await import("@mclawnet/memory");
|
|
544
|
-
const result = await runConsolidation(db, {
|
|
545
|
-
vectorIndex: store.getVectorIndex(),
|
|
546
|
-
domainBalanceWarningThreshold: 0.7,
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
expect(result.rolesProcessed).toBeGreaterThan(0);
|
|
550
|
-
expect(result.recalculated).toBeGreaterThanOrEqual(15);
|
|
551
|
-
// 12/15 = 80% frontend on consolidationRole, should trigger domain warning
|
|
552
|
-
const frontendWarning = result.domainWarnings.find(
|
|
553
|
-
(w) => w.roleId === consolidationRole && w.domain === "frontend",
|
|
554
|
-
);
|
|
555
|
-
expect(frontendWarning).toBeDefined();
|
|
556
|
-
expect(frontendWarning!.percentage).toBeGreaterThan(0.7);
|
|
557
|
-
expect(typeof result.profilesRefreshed).toBe("number");
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
describe("E2E: EWC++ drift protection", () => {
|
|
562
|
-
it("should protect high-drift memories from pruning during consolidation", async () => {
|
|
563
|
-
const { EWCGuard } = await import("@mclawnet/memory");
|
|
564
|
-
const { runConsolidation } = await import("@mclawnet/memory");
|
|
565
|
-
const { WORKING_CAP } = await import("@mclawnet/memory");
|
|
566
|
-
|
|
567
|
-
const roleId = "role-ewc-drift-test";
|
|
568
|
-
|
|
569
|
-
// Step 1: Store initial memories with a consistent "direction" and compute profile
|
|
570
|
-
for (let i = 0; i < 5; i++) {
|
|
571
|
-
await store.addMemoryWithEmbedding(
|
|
572
|
-
{
|
|
573
|
-
roleId,
|
|
574
|
-
content: `core principle ${i} for role stability`,
|
|
575
|
-
type: "principle",
|
|
576
|
-
importance: 0.9,
|
|
577
|
-
},
|
|
578
|
-
embeddingService,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Manually set high fisher_weight on these memories so they anchor the profile
|
|
583
|
-
const initialMemories = store.getMemoriesByRole(roleId);
|
|
584
|
-
for (const mem of initialMemories) {
|
|
585
|
-
db.prepare("UPDATE memories SET fisher_weight = 1.0 WHERE id = ?").run(mem.id);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Compute and save the initial cognitive profile
|
|
589
|
-
const ewcGuard = new EWCGuard(db);
|
|
590
|
-
const profile = ewcGuard.computeProfile(roleId);
|
|
591
|
-
expect(profile).not.toBeNull();
|
|
592
|
-
expect(profile!.memoryCount).toBe(5);
|
|
593
|
-
|
|
594
|
-
// Step 2: Add many working-level memories to exceed WORKING_CAP and trigger pruning
|
|
595
|
-
// These have low importance and different content direction
|
|
596
|
-
for (let i = 0; i < WORKING_CAP + 10; i++) {
|
|
597
|
-
store.addMemory({
|
|
598
|
-
roleId,
|
|
599
|
-
content: `disposable working item ${i}`,
|
|
600
|
-
type: "experience",
|
|
601
|
-
level: "working",
|
|
602
|
-
importance: 0.2,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// Step 3: Run consolidation — it should check drift and protect core memories
|
|
607
|
-
const result = await runConsolidation(db, {
|
|
608
|
-
vectorIndex: store.getVectorIndex(),
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Pruning should have demoted some working memories
|
|
612
|
-
expect(result.totalDemotedToLongTerm).toBeGreaterThan(0);
|
|
613
|
-
|
|
614
|
-
// Step 4: Verify that the original high-fisher-weight principle memories survived
|
|
615
|
-
const survivingMemories = store.getMemoriesByRole(roleId);
|
|
616
|
-
const principleMemories = survivingMemories.filter(
|
|
617
|
-
(m) => m.type === "principle" && m.content.includes("core principle"),
|
|
618
|
-
);
|
|
619
|
-
// All 5 core principles should still exist (protected by EWC or high importance)
|
|
620
|
-
expect(principleMemories.length).toBe(5);
|
|
621
|
-
|
|
622
|
-
// Step 5: Verify drift was checked for this role
|
|
623
|
-
expect(result.driftResults[roleId]).toBeDefined();
|
|
624
|
-
expect(typeof result.driftResults[roleId].driftLoss).toBe("number");
|
|
625
|
-
expect(typeof result.driftResults[roleId].isAcceptable).toBe("boolean");
|
|
626
|
-
});
|
|
627
|
-
});
|