@gethmy/mcp 2.3.1 → 2.3.3
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/dist/lib/api-client.js +2099 -648
- package/dist/lib/config.js +217 -201
- package/package.json +9 -5
- package/src/memory-cleanup.ts +2 -4
- package/dist/lib/__tests__/active-learning.test.js +0 -386
- package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
- package/dist/lib/__tests__/auto-session.test.js +0 -661
- package/dist/lib/__tests__/context-assembly.test.js +0 -362
- package/dist/lib/__tests__/graph-expansion.test.js +0 -150
- package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
- package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
- package/dist/lib/__tests__/pattern-detection.test.js +0 -295
- package/dist/lib/__tests__/prompt-builder.test.js +0 -418
- package/dist/lib/active-learning.js +0 -822
- package/dist/lib/auto-session.js +0 -214
- package/dist/lib/cli.js +0 -138
- package/dist/lib/consolidation.js +0 -303
- package/dist/lib/context-assembly.js +0 -884
- package/dist/lib/graph-expansion.js +0 -163
- package/dist/lib/http.js +0 -175
- package/dist/lib/index.js +0 -7
- package/dist/lib/lifecycle-maintenance.js +0 -88
- package/dist/lib/memory-cleanup.js +0 -455
- package/dist/lib/onboard.js +0 -36
- package/dist/lib/prompt-builder.js +0 -488
- package/dist/lib/remote.js +0 -166
- package/dist/lib/server.js +0 -3365
- package/dist/lib/skills.js +0 -593
- package/dist/lib/tui/agents.js +0 -116
- package/dist/lib/tui/docs.js +0 -744
- package/dist/lib/tui/setup.js +0 -934
- package/dist/lib/tui/theme.js +0 -95
- package/dist/lib/tui/writer.js +0 -200
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the Active Learning system.
|
|
3
|
-
*
|
|
4
|
-
* These tests use a mock API client to verify extraction rules
|
|
5
|
-
* and confidence-based routing - no real API calls.
|
|
6
|
-
*
|
|
7
|
-
* Run with: bun test packages/mcp-server/src/__tests__/active-learning.test.ts
|
|
8
|
-
*/
|
|
9
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
10
|
-
// Mock the config module before importing active-learning
|
|
11
|
-
let mockWorkspaceId = "ws-test-123";
|
|
12
|
-
const mockProjectId = "proj-test-456";
|
|
13
|
-
mock.module("../config.js", () => ({
|
|
14
|
-
getActiveWorkspaceId: () => mockWorkspaceId,
|
|
15
|
-
getActiveProjectId: () => mockProjectId,
|
|
16
|
-
isConfigured: () => true,
|
|
17
|
-
loadConfig: () => { },
|
|
18
|
-
}));
|
|
19
|
-
// Import after mocking
|
|
20
|
-
const { extractLearnings } = await import("../active-learning.js");
|
|
21
|
-
// Extended mock client that satisfies all methods called by fire-and-forget paths
|
|
22
|
-
// (autoExpandGraph, detectAndCreatePatterns). Unused by most tests but prevents crashes.
|
|
23
|
-
function makeFullMockClient() {
|
|
24
|
-
const createdEntities = [];
|
|
25
|
-
let nextId = 1;
|
|
26
|
-
return {
|
|
27
|
-
createdEntities,
|
|
28
|
-
createMemoryEntity: mock(async (data) => {
|
|
29
|
-
const entity = { id: `entity-${nextId++}`, ...data };
|
|
30
|
-
createdEntities.push(entity);
|
|
31
|
-
return { entity };
|
|
32
|
-
}),
|
|
33
|
-
updateMemoryEntity: mock(async (entityId, updates) => ({
|
|
34
|
-
entity: { id: entityId, ...updates },
|
|
35
|
-
})),
|
|
36
|
-
deleteMemoryEntity: mock(async () => ({ success: true })),
|
|
37
|
-
listMemoryEntities: mock(async () => ({
|
|
38
|
-
entities: [],
|
|
39
|
-
count: 0,
|
|
40
|
-
})),
|
|
41
|
-
getMemoryEntity: mock(async (id) => ({
|
|
42
|
-
entity: { id, type: "lesson" },
|
|
43
|
-
})),
|
|
44
|
-
searchMemoryEntities: mock(async () => ({
|
|
45
|
-
entities: [],
|
|
46
|
-
count: 0,
|
|
47
|
-
})),
|
|
48
|
-
createMemoryRelation: mock(async () => ({ relation: {} })),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
// ---- Mock helpers ----
|
|
52
|
-
function makeMockClient() {
|
|
53
|
-
const createdEntities = [];
|
|
54
|
-
let nextId = 1;
|
|
55
|
-
return {
|
|
56
|
-
createdEntities,
|
|
57
|
-
createMemoryEntity: mock(async (data) => {
|
|
58
|
-
const entity = { id: `entity-${nextId++}`, ...data };
|
|
59
|
-
createdEntities.push(entity);
|
|
60
|
-
return { entity };
|
|
61
|
-
}),
|
|
62
|
-
updateMemoryEntity: mock(async (entityId, updates) => {
|
|
63
|
-
return { entity: { id: entityId, ...updates } };
|
|
64
|
-
}),
|
|
65
|
-
deleteMemoryEntity: mock(async () => ({ success: true })),
|
|
66
|
-
listMemoryEntities: mock(async () => ({
|
|
67
|
-
entities: [],
|
|
68
|
-
count: 0,
|
|
69
|
-
})),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function makeSession(overrides = {}) {
|
|
73
|
-
return {
|
|
74
|
-
cardId: "card-1",
|
|
75
|
-
cardTitle: "Fix login bug",
|
|
76
|
-
cardLabels: ["feature"],
|
|
77
|
-
agentIdentifier: "claude-code",
|
|
78
|
-
agentName: "Claude Code",
|
|
79
|
-
status: "completed",
|
|
80
|
-
progressPercent: 100,
|
|
81
|
-
...overrides,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// ---- Tests ----
|
|
85
|
-
describe("extractLearnings", () => {
|
|
86
|
-
describe("Rule 1: Blockers create error entities", () => {
|
|
87
|
-
test("session with blockers creates error entities", async () => {
|
|
88
|
-
const client = makeMockClient();
|
|
89
|
-
const session = makeSession({
|
|
90
|
-
blockers: ["API rate limit exceeded", "Database timeout"],
|
|
91
|
-
status: "completed",
|
|
92
|
-
});
|
|
93
|
-
const result = await extractLearnings(client, session);
|
|
94
|
-
// 2 blockers + 1 session lesson = 3 entities
|
|
95
|
-
expect(result.count).toBe(3);
|
|
96
|
-
expect(result.entityIds).toHaveLength(3);
|
|
97
|
-
// Check error entities were created for each blocker
|
|
98
|
-
const errorEntities = client.createdEntities.filter((e) => e.type === "error");
|
|
99
|
-
expect(errorEntities).toHaveLength(2);
|
|
100
|
-
expect(errorEntities[0].title).toContain("Blocker:");
|
|
101
|
-
expect(errorEntities[0].title).toContain("API rate limit");
|
|
102
|
-
expect(errorEntities[0].memory_tier).toBe("reference");
|
|
103
|
-
expect(errorEntities[0].confidence).toBe(0.7);
|
|
104
|
-
expect(errorEntities[0].tags ?? []).toContain("auto-extracted");
|
|
105
|
-
expect(errorEntities[0].tags ?? []).toContain("blocker");
|
|
106
|
-
});
|
|
107
|
-
test("blocker title is truncated to 100 chars", async () => {
|
|
108
|
-
const client = makeMockClient();
|
|
109
|
-
const longBlocker = "A".repeat(200);
|
|
110
|
-
const session = makeSession({
|
|
111
|
-
blockers: [longBlocker],
|
|
112
|
-
status: "paused",
|
|
113
|
-
});
|
|
114
|
-
await extractLearnings(client, session);
|
|
115
|
-
const errorEntities = client.createdEntities.filter((e) => e.type === "error");
|
|
116
|
-
expect(errorEntities).toHaveLength(1);
|
|
117
|
-
// Title: "Blocker: " + 100 chars
|
|
118
|
-
expect(errorEntities[0].title.length).toBeLessThanOrEqual(110);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe("Rule 2: Completed sessions create lesson entities", () => {
|
|
122
|
-
test("clean completed session does NOT create lesson entity", async () => {
|
|
123
|
-
const client = makeMockClient();
|
|
124
|
-
const session = makeSession({
|
|
125
|
-
status: "completed",
|
|
126
|
-
currentTask: "Deployed to production",
|
|
127
|
-
progressPercent: 100,
|
|
128
|
-
sessionDurationMs: 300000, // 5 minutes
|
|
129
|
-
});
|
|
130
|
-
const result = await extractLearnings(client, session);
|
|
131
|
-
expect(result.count).toBe(0);
|
|
132
|
-
const lessons = client.createdEntities.filter((e) => e.type === "lesson");
|
|
133
|
-
expect(lessons).toHaveLength(0);
|
|
134
|
-
});
|
|
135
|
-
test("completed session with blockers creates lesson entity", async () => {
|
|
136
|
-
const client = makeMockClient();
|
|
137
|
-
const session = makeSession({
|
|
138
|
-
status: "completed",
|
|
139
|
-
currentTask: "Deployed to production",
|
|
140
|
-
progressPercent: 100,
|
|
141
|
-
sessionDurationMs: 300000, // 5 minutes
|
|
142
|
-
blockers: ["API rate limit hit"],
|
|
143
|
-
});
|
|
144
|
-
const _result = await extractLearnings(client, session);
|
|
145
|
-
// 1 error entity (Rule 1) + 1 lesson entity (Rule 2)
|
|
146
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
147
|
-
expect(lesson).toBeTruthy();
|
|
148
|
-
expect(lesson.title).toContain("Session:");
|
|
149
|
-
expect(lesson.memory_tier).toBe("episode");
|
|
150
|
-
expect(lesson.confidence).toBe(0.7);
|
|
151
|
-
expect(lesson.content).toContain("Deployed to production");
|
|
152
|
-
expect(lesson.content).toContain("5 minutes");
|
|
153
|
-
});
|
|
154
|
-
test("completed session with incomplete subtasks creates lesson entity", async () => {
|
|
155
|
-
const client = makeMockClient();
|
|
156
|
-
const session = makeSession({
|
|
157
|
-
status: "completed",
|
|
158
|
-
currentTask: "Finished main work",
|
|
159
|
-
progressPercent: 100,
|
|
160
|
-
cardSubtasks: [
|
|
161
|
-
{ title: "Step 1", done: true },
|
|
162
|
-
{ title: "Step 2", done: false },
|
|
163
|
-
],
|
|
164
|
-
});
|
|
165
|
-
const _result = await extractLearnings(client, session);
|
|
166
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
167
|
-
expect(lesson).toBeTruthy();
|
|
168
|
-
expect(lesson.title).toContain("Session:");
|
|
169
|
-
});
|
|
170
|
-
test("paused session does NOT create lesson entity", async () => {
|
|
171
|
-
const client = makeMockClient();
|
|
172
|
-
const session = makeSession({ status: "paused", blockers: undefined });
|
|
173
|
-
const result = await extractLearnings(client, session);
|
|
174
|
-
expect(result.count).toBe(0);
|
|
175
|
-
const lessons = client.createdEntities.filter((e) => e.type === "lesson");
|
|
176
|
-
expect(lessons).toHaveLength(0);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
describe("Rule 3: Bug label + completed creates solution entity", () => {
|
|
180
|
-
test('card with "bug" label creates solution entity', async () => {
|
|
181
|
-
const client = makeMockClient();
|
|
182
|
-
const session = makeSession({
|
|
183
|
-
cardLabels: ["bug", "priority-high"],
|
|
184
|
-
status: "completed",
|
|
185
|
-
});
|
|
186
|
-
const _result = await extractLearnings(client, session);
|
|
187
|
-
const solutions = client.createdEntities.filter((e) => e.type === "solution");
|
|
188
|
-
expect(solutions).toHaveLength(1);
|
|
189
|
-
expect(solutions[0].title).toContain("Solution:");
|
|
190
|
-
expect(solutions[0].memory_tier).toBe("reference");
|
|
191
|
-
expect(solutions[0].confidence).toBe(0.8);
|
|
192
|
-
expect(solutions[0].tags ?? []).toContain("bug-fix");
|
|
193
|
-
});
|
|
194
|
-
test('case-insensitive "Bug" label works', async () => {
|
|
195
|
-
const client = makeMockClient();
|
|
196
|
-
const session = makeSession({
|
|
197
|
-
cardLabels: ["Bug"],
|
|
198
|
-
status: "completed",
|
|
199
|
-
});
|
|
200
|
-
await extractLearnings(client, session);
|
|
201
|
-
const solutions = client.createdEntities.filter((e) => e.type === "solution");
|
|
202
|
-
// The check is .toLowerCase(), so "Bug" matches
|
|
203
|
-
expect(solutions).toHaveLength(1);
|
|
204
|
-
});
|
|
205
|
-
test("bug label + paused does NOT create solution", async () => {
|
|
206
|
-
const client = makeMockClient();
|
|
207
|
-
const session = makeSession({
|
|
208
|
-
cardLabels: ["bug"],
|
|
209
|
-
status: "paused",
|
|
210
|
-
});
|
|
211
|
-
await extractLearnings(client, session);
|
|
212
|
-
const solutions = client.createdEntities.filter((e) => e.type === "solution");
|
|
213
|
-
expect(solutions).toHaveLength(0);
|
|
214
|
-
});
|
|
215
|
-
for (const label of ["fix", "hotfix", "defect", "error"]) {
|
|
216
|
-
test(`"${label}" label also triggers solution creation`, async () => {
|
|
217
|
-
const client = makeMockClient();
|
|
218
|
-
const session = makeSession({
|
|
219
|
-
cardLabels: [label],
|
|
220
|
-
status: "completed",
|
|
221
|
-
});
|
|
222
|
-
await extractLearnings(client, session);
|
|
223
|
-
const solutions = client.createdEntities.filter((e) => e.type === "solution");
|
|
224
|
-
expect(solutions).toHaveLength(1);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
describe("Card labels are included in tags", () => {
|
|
229
|
-
test("first 3 card labels are appended to tags", async () => {
|
|
230
|
-
const client = makeMockClient();
|
|
231
|
-
const session = makeSession({
|
|
232
|
-
cardLabels: ["feature", "frontend", "urgent", "extra"],
|
|
233
|
-
status: "completed",
|
|
234
|
-
blockers: ["test blocker"],
|
|
235
|
-
});
|
|
236
|
-
await extractLearnings(client, session);
|
|
237
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
238
|
-
const tags = lesson.tags;
|
|
239
|
-
expect(tags).toContain("feature");
|
|
240
|
-
expect(tags).toContain("frontend");
|
|
241
|
-
expect(tags).toContain("urgent");
|
|
242
|
-
expect(tags).not.toContain("extra"); // Only first 3
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
describe("Error handling", () => {
|
|
246
|
-
test("individual entity creation failure does not break others", async () => {
|
|
247
|
-
let callCount = 0;
|
|
248
|
-
const client = {
|
|
249
|
-
createMemoryEntity: mock(async (_data) => {
|
|
250
|
-
callCount++;
|
|
251
|
-
if (callCount === 1)
|
|
252
|
-
throw new Error("Network error");
|
|
253
|
-
return { entity: { id: `entity-${callCount}` } };
|
|
254
|
-
}),
|
|
255
|
-
};
|
|
256
|
-
const session = makeSession({
|
|
257
|
-
blockers: ["blocker-1"],
|
|
258
|
-
status: "completed",
|
|
259
|
-
});
|
|
260
|
-
// blocker creates error (fails), completed creates lesson (succeeds)
|
|
261
|
-
const result = await extractLearnings(client, session);
|
|
262
|
-
// Only 1 succeeded out of 2
|
|
263
|
-
expect(result.entityIds).toHaveLength(1);
|
|
264
|
-
expect(result.count).toBe(1);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
describe("No workspace returns empty", () => {
|
|
268
|
-
test("returns empty when no workspace configured", async () => {
|
|
269
|
-
// Temporarily override
|
|
270
|
-
const origWs = mockWorkspaceId;
|
|
271
|
-
mockWorkspaceId = null;
|
|
272
|
-
const client = makeMockClient();
|
|
273
|
-
const session = makeSession();
|
|
274
|
-
const result = await extractLearnings(client, session);
|
|
275
|
-
expect(result.count).toBe(0);
|
|
276
|
-
expect(result.entityIds).toHaveLength(0);
|
|
277
|
-
// Restore
|
|
278
|
-
mockWorkspaceId = origWs;
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
describe("extractLearnings - entity metadata", () => {
|
|
283
|
-
test("stores card_id and source in metadata", async () => {
|
|
284
|
-
const client = makeMockClient();
|
|
285
|
-
const session = makeSession({
|
|
286
|
-
cardId: "card-abc",
|
|
287
|
-
status: "completed",
|
|
288
|
-
blockers: ["test blocker"],
|
|
289
|
-
});
|
|
290
|
-
await extractLearnings(client, session);
|
|
291
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
292
|
-
const meta = lesson.metadata;
|
|
293
|
-
expect(meta.source).toBe("active_learning");
|
|
294
|
-
expect(meta.card_id).toBe("card-abc");
|
|
295
|
-
});
|
|
296
|
-
test("lesson entity carries agent_identifier", async () => {
|
|
297
|
-
const client = makeMockClient();
|
|
298
|
-
const session = makeSession({
|
|
299
|
-
agentIdentifier: "my-agent-v2",
|
|
300
|
-
status: "completed",
|
|
301
|
-
blockers: ["test blocker"],
|
|
302
|
-
});
|
|
303
|
-
await extractLearnings(client, session);
|
|
304
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
305
|
-
expect(lesson.agent_identifier).toBe("my-agent-v2");
|
|
306
|
-
});
|
|
307
|
-
test("session lesson content includes agent name", async () => {
|
|
308
|
-
const client = makeMockClient();
|
|
309
|
-
const session = makeSession({
|
|
310
|
-
agentName: "TestBot",
|
|
311
|
-
status: "completed",
|
|
312
|
-
blockers: ["test blocker"],
|
|
313
|
-
});
|
|
314
|
-
await extractLearnings(client, session);
|
|
315
|
-
const lesson = client.createdEntities.find((e) => e.type === "lesson");
|
|
316
|
-
expect(lesson.content).toContain("TestBot");
|
|
317
|
-
});
|
|
318
|
-
test("clean completed session without meaningful content skips lesson", async () => {
|
|
319
|
-
const client = makeMockClient();
|
|
320
|
-
const session = makeSession({
|
|
321
|
-
status: "completed",
|
|
322
|
-
currentTask: undefined,
|
|
323
|
-
progressPercent: undefined,
|
|
324
|
-
sessionDurationMs: undefined,
|
|
325
|
-
blockers: undefined,
|
|
326
|
-
});
|
|
327
|
-
const result = await extractLearnings(client, session);
|
|
328
|
-
expect(result.count).toBe(0);
|
|
329
|
-
const lessons = client.createdEntities.filter((e) => e.type === "lesson");
|
|
330
|
-
expect(lessons).toHaveLength(0);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
describe("extractLearnings - returns correct entityIds", () => {
|
|
334
|
-
test("entityIds array matches returned count", async () => {
|
|
335
|
-
const client = makeMockClient();
|
|
336
|
-
const session = makeSession({
|
|
337
|
-
cardLabels: ["bug"],
|
|
338
|
-
blockers: ["error occurred"],
|
|
339
|
-
status: "completed",
|
|
340
|
-
});
|
|
341
|
-
const result = await extractLearnings(client, session);
|
|
342
|
-
// error + lesson + solution = 3
|
|
343
|
-
expect(result.count).toBe(result.entityIds.length);
|
|
344
|
-
expect(result.entityIds).toHaveLength(3);
|
|
345
|
-
// All IDs should be truthy strings
|
|
346
|
-
for (const id of result.entityIds) {
|
|
347
|
-
expect(typeof id).toBe("string");
|
|
348
|
-
expect(id.length).toBeGreaterThan(0);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
test("entityIds are unique", async () => {
|
|
352
|
-
const client = makeMockClient();
|
|
353
|
-
const session = makeSession({
|
|
354
|
-
cardLabels: ["bug"],
|
|
355
|
-
blockers: ["err-1", "err-2"],
|
|
356
|
-
status: "completed",
|
|
357
|
-
});
|
|
358
|
-
const result = await extractLearnings(client, session);
|
|
359
|
-
const unique = new Set(result.entityIds);
|
|
360
|
-
expect(unique.size).toBe(result.entityIds.length);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
describe("extractLearnings - fire-and-forget calls don't crash", () => {
|
|
364
|
-
test("works with full client (autoExpandGraph and detectAndCreatePatterns paths)", async () => {
|
|
365
|
-
const client = makeFullMockClient();
|
|
366
|
-
const session = makeSession({ status: "completed", blockers: ["test"] });
|
|
367
|
-
// Should not throw even when graph expansion / pattern detection run
|
|
368
|
-
const result = await extractLearnings(client, session);
|
|
369
|
-
expect(result.count).toBeGreaterThan(0);
|
|
370
|
-
// Flush microtasks so fire-and-forget paths complete
|
|
371
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
372
|
-
// searchMemoryEntities was called by autoExpandGraph
|
|
373
|
-
expect(client.searchMemoryEntities).toHaveBeenCalled();
|
|
374
|
-
});
|
|
375
|
-
test("client errors in fire-and-forget paths do not surface to caller", async () => {
|
|
376
|
-
const client = makeFullMockClient();
|
|
377
|
-
// Make the search throw - should be swallowed
|
|
378
|
-
client.searchMemoryEntities = mock(async () => {
|
|
379
|
-
throw new Error("Search service unavailable");
|
|
380
|
-
});
|
|
381
|
-
const session = makeSession({ status: "completed", blockers: ["test"] });
|
|
382
|
-
const result = await extractLearnings(client, session);
|
|
383
|
-
// Main extraction still succeeded (error + lesson)
|
|
384
|
-
expect(result.count).toBe(2);
|
|
385
|
-
});
|
|
386
|
-
});
|