@gethmy/mcp 2.0.0 → 2.1.1
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/README.md +6 -1
- package/dist/cli.js +711 -59
- package/dist/index.js +5 -3
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +550 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +744 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/package.json +15 -6
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +969 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +863 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pattern detection logic (detectAndCreatePatterns).
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test packages/mcp-server/src/__tests__/pattern-detection.test.ts
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
7
|
+
// Mock config before importing
|
|
8
|
+
mock.module("../config.js", () => ({
|
|
9
|
+
getActiveWorkspaceId: () => "ws-test",
|
|
10
|
+
getActiveProjectId: () => "proj-test",
|
|
11
|
+
isConfigured: () => true,
|
|
12
|
+
loadConfig: () => { },
|
|
13
|
+
}));
|
|
14
|
+
// Mock graph-expansion so autoExpandGraph calls don't interfere with pattern tests
|
|
15
|
+
mock.module("../graph-expansion.js", () => ({
|
|
16
|
+
autoExpandGraph: mock(async () => ({ relationsCreated: 0 })),
|
|
17
|
+
}));
|
|
18
|
+
const { detectAndCreatePatterns } = await import("../active-learning.js");
|
|
19
|
+
function makeFullMockClient(opts) {
|
|
20
|
+
const createdEntities = [];
|
|
21
|
+
const createdRelations = [];
|
|
22
|
+
let nextId = 100;
|
|
23
|
+
return {
|
|
24
|
+
createdEntities,
|
|
25
|
+
createdRelations,
|
|
26
|
+
getMemoryEntity: mock(async (_id) => ({
|
|
27
|
+
entity: opts.entity ?? { id: _id, type: "lesson" },
|
|
28
|
+
})),
|
|
29
|
+
searchMemoryEntities: mock(async () => ({
|
|
30
|
+
entities: opts.searchResults ?? [],
|
|
31
|
+
count: opts.searchResults?.length ?? 0,
|
|
32
|
+
})),
|
|
33
|
+
listMemoryEntities: mock(async () => ({
|
|
34
|
+
entities: opts.existingPatterns ?? [],
|
|
35
|
+
count: opts.existingPatterns?.length ?? 0,
|
|
36
|
+
})),
|
|
37
|
+
createMemoryEntity: mock(async (data) => {
|
|
38
|
+
const entity = { id: `pattern-${nextId++}`, ...data };
|
|
39
|
+
createdEntities.push(entity);
|
|
40
|
+
return { entity };
|
|
41
|
+
}),
|
|
42
|
+
updateMemoryEntity: mock(async (id, updates) => ({
|
|
43
|
+
entity: { id, ...updates },
|
|
44
|
+
})),
|
|
45
|
+
createMemoryRelation: mock(async (data) => {
|
|
46
|
+
createdRelations.push(data);
|
|
47
|
+
return { relation: { id: `rel-${nextId++}`, ...data } };
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function makeSession(overrides = {}) {
|
|
52
|
+
return {
|
|
53
|
+
cardId: "card-1",
|
|
54
|
+
cardTitle: "Implement feature X",
|
|
55
|
+
cardLabels: ["backend", "api"],
|
|
56
|
+
agentIdentifier: "claude-code",
|
|
57
|
+
agentName: "Claude Code",
|
|
58
|
+
status: "completed",
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ---- Tests ----
|
|
63
|
+
describe("detectAndCreatePatterns", () => {
|
|
64
|
+
describe("threshold gating", () => {
|
|
65
|
+
test("does nothing when fewer than 3 similar entities exist", async () => {
|
|
66
|
+
const client = makeFullMockClient({
|
|
67
|
+
entity: { id: "new-1", type: "lesson" },
|
|
68
|
+
searchResults: [
|
|
69
|
+
{ id: "old-1", type: "lesson" },
|
|
70
|
+
{ id: "old-2", type: "lesson" },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
74
|
+
expect(client.createMemoryEntity).not.toHaveBeenCalled();
|
|
75
|
+
expect(client.createMemoryRelation).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
test("creates pattern when exactly 3 similar entities exist", async () => {
|
|
78
|
+
const client = makeFullMockClient({
|
|
79
|
+
entity: { id: "new-1", type: "lesson" },
|
|
80
|
+
searchResults: [
|
|
81
|
+
{ id: "old-1", type: "lesson" },
|
|
82
|
+
{ id: "old-2", type: "lesson" },
|
|
83
|
+
{ id: "old-3", type: "lesson" },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
87
|
+
expect(client.createMemoryEntity).toHaveBeenCalledTimes(1);
|
|
88
|
+
const created = client.createdEntities[0];
|
|
89
|
+
expect(created.type).toBe("pattern");
|
|
90
|
+
expect(created.memory_tier).toBe("reference");
|
|
91
|
+
expect(created.confidence).toBe(0.75);
|
|
92
|
+
});
|
|
93
|
+
test("creates pattern when more than 3 similar entities exist", async () => {
|
|
94
|
+
const client = makeFullMockClient({
|
|
95
|
+
entity: { id: "new-1", type: "error" },
|
|
96
|
+
searchResults: Array.from({ length: 10 }, (_, i) => ({
|
|
97
|
+
id: `old-${i}`,
|
|
98
|
+
type: "error",
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
102
|
+
expect(client.createMemoryEntity).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("newly-created entities are excluded from match count", () => {
|
|
106
|
+
test("newEntityIds are excluded from the existing count", async () => {
|
|
107
|
+
// 3 search results but 2 of them are in newEntityIds → only 1 truly existing
|
|
108
|
+
const client = makeFullMockClient({
|
|
109
|
+
entity: { id: "new-1", type: "lesson" },
|
|
110
|
+
searchResults: [
|
|
111
|
+
{ id: "new-2", type: "lesson" }, // also new - should be excluded
|
|
112
|
+
{ id: "new-3", type: "lesson" }, // also new - should be excluded
|
|
113
|
+
{ id: "old-1", type: "lesson" }, // truly existing
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
await detectAndCreatePatterns(client, ["new-1", "new-2", "new-3"], makeSession(), "ws-test");
|
|
117
|
+
// Only 1 truly existing → below threshold → no pattern
|
|
118
|
+
expect(client.createMemoryEntity).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("existing pattern update", () => {
|
|
122
|
+
test("updates existing pattern entity instead of creating a new one", async () => {
|
|
123
|
+
const client = makeFullMockClient({
|
|
124
|
+
entity: { id: "new-1", type: "lesson" },
|
|
125
|
+
searchResults: Array.from({ length: 5 }, (_, i) => ({
|
|
126
|
+
id: `old-${i}`,
|
|
127
|
+
type: "lesson",
|
|
128
|
+
})),
|
|
129
|
+
existingPatterns: [{ id: "existing-pattern", type: "pattern" }],
|
|
130
|
+
});
|
|
131
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
132
|
+
// No new pattern entity created
|
|
133
|
+
expect(client.createMemoryEntity).not.toHaveBeenCalled();
|
|
134
|
+
// Existing one was updated
|
|
135
|
+
expect(client.updateMemoryEntity).toHaveBeenCalledWith("existing-pattern", expect.objectContaining({
|
|
136
|
+
content: expect.stringContaining("Recurring pattern"),
|
|
137
|
+
metadata: expect.objectContaining({
|
|
138
|
+
pattern_count: expect.any(Number),
|
|
139
|
+
}),
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
test("creates relation from existing pattern to new entity", async () => {
|
|
143
|
+
const client = makeFullMockClient({
|
|
144
|
+
entity: { id: "new-1", type: "lesson" },
|
|
145
|
+
searchResults: Array.from({ length: 4 }, (_, i) => ({
|
|
146
|
+
id: `old-${i}`,
|
|
147
|
+
type: "lesson",
|
|
148
|
+
})),
|
|
149
|
+
existingPatterns: [{ id: "existing-pattern", type: "pattern" }],
|
|
150
|
+
});
|
|
151
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
152
|
+
const relationsFromPattern = client.createdRelations.filter((r) => r.source_id === "existing-pattern");
|
|
153
|
+
expect(relationsFromPattern.length).toBeGreaterThan(0);
|
|
154
|
+
const targetIds = relationsFromPattern.map((r) => r.target_id);
|
|
155
|
+
expect(targetIds).toContain("new-1");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("new pattern creation", () => {
|
|
159
|
+
test("pattern entity has correct metadata", async () => {
|
|
160
|
+
const session = makeSession({ cardLabels: ["backend", "api"] });
|
|
161
|
+
const client = makeFullMockClient({
|
|
162
|
+
entity: { id: "new-1", type: "error" },
|
|
163
|
+
searchResults: Array.from({ length: 4 }, (_, i) => ({
|
|
164
|
+
id: `old-${i}`,
|
|
165
|
+
type: "error",
|
|
166
|
+
})),
|
|
167
|
+
});
|
|
168
|
+
await detectAndCreatePatterns(client, ["new-1"], session, "ws-test", "proj-test");
|
|
169
|
+
const pattern = client.createdEntities[0];
|
|
170
|
+
expect(pattern.type).toBe("pattern");
|
|
171
|
+
expect(pattern.memory_tier).toBe("reference");
|
|
172
|
+
expect(pattern.confidence).toBe(0.75);
|
|
173
|
+
expect(pattern.workspace_id).toBe("ws-test");
|
|
174
|
+
expect(pattern.project_id).toBe("proj-test");
|
|
175
|
+
const meta = pattern.metadata;
|
|
176
|
+
expect(meta.source).toBe("pattern_detection");
|
|
177
|
+
expect(meta.pattern_type).toBe("error");
|
|
178
|
+
});
|
|
179
|
+
test("pattern title references the entity type", async () => {
|
|
180
|
+
const client = makeFullMockClient({
|
|
181
|
+
entity: { id: "new-1", type: "solution" },
|
|
182
|
+
searchResults: Array.from({ length: 3 }, (_, i) => ({
|
|
183
|
+
id: `old-${i}`,
|
|
184
|
+
type: "solution",
|
|
185
|
+
})),
|
|
186
|
+
});
|
|
187
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession({ cardLabels: ["backend", "api"] }), "ws-test");
|
|
188
|
+
const pattern = client.createdEntities[0];
|
|
189
|
+
expect(pattern.title).toContain("Pattern:");
|
|
190
|
+
expect(pattern.title).toContain("solution");
|
|
191
|
+
});
|
|
192
|
+
test("creates relates_to relations from pattern to source entities", async () => {
|
|
193
|
+
const client = makeFullMockClient({
|
|
194
|
+
entity: { id: "new-1", type: "lesson" },
|
|
195
|
+
searchResults: [
|
|
196
|
+
{ id: "old-1", type: "lesson" },
|
|
197
|
+
{ id: "old-2", type: "lesson" },
|
|
198
|
+
{ id: "old-3", type: "lesson" },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
202
|
+
// Relations should link pattern → new entity + up to 4 existing
|
|
203
|
+
const relations = client.createdRelations;
|
|
204
|
+
expect(relations.length).toBeGreaterThan(0);
|
|
205
|
+
for (const rel of relations) {
|
|
206
|
+
expect(rel.relation_type).toBe("relates_to");
|
|
207
|
+
expect(rel.confidence).toBe(0.75);
|
|
208
|
+
}
|
|
209
|
+
const targetIds = relations.map((r) => r.target_id);
|
|
210
|
+
expect(targetIds).toContain("new-1");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe("error handling", () => {
|
|
214
|
+
test("is non-fatal when getMemoryEntity throws", async () => {
|
|
215
|
+
const client = {
|
|
216
|
+
getMemoryEntity: mock(async () => {
|
|
217
|
+
throw new Error("Not found");
|
|
218
|
+
}),
|
|
219
|
+
searchMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
|
|
220
|
+
listMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
|
|
221
|
+
createMemoryEntity: mock(async () => ({ entity: { id: "x" } })),
|
|
222
|
+
updateMemoryEntity: mock(async () => ({ entity: {} })),
|
|
223
|
+
createMemoryRelation: mock(async () => ({ relation: {} })),
|
|
224
|
+
};
|
|
225
|
+
// Should not throw
|
|
226
|
+
const result = await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
227
|
+
expect(result).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
test("skips entity if type is missing", async () => {
|
|
230
|
+
const client = makeFullMockClient({
|
|
231
|
+
// entity with no type
|
|
232
|
+
entity: undefined,
|
|
233
|
+
searchResults: [],
|
|
234
|
+
});
|
|
235
|
+
// Override getMemoryEntity to return entity without type
|
|
236
|
+
client.getMemoryEntity = mock(async () => ({
|
|
237
|
+
entity: { id: "new-1" },
|
|
238
|
+
}));
|
|
239
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
240
|
+
expect(client.createMemoryEntity).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
test("duplicate relation failures do not abort remaining relations", async () => {
|
|
243
|
+
let relCount = 0;
|
|
244
|
+
const client = makeFullMockClient({
|
|
245
|
+
entity: { id: "new-1", type: "lesson" },
|
|
246
|
+
searchResults: [
|
|
247
|
+
{ id: "old-1", type: "lesson" },
|
|
248
|
+
{ id: "old-2", type: "lesson" },
|
|
249
|
+
{ id: "old-3", type: "lesson" },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
// First relation call throws 409, rest succeed
|
|
253
|
+
client.createMemoryRelation = mock(async (data) => {
|
|
254
|
+
relCount++;
|
|
255
|
+
if (relCount === 1)
|
|
256
|
+
throw Object.assign(new Error("Conflict"), { status: 409 });
|
|
257
|
+
client.createdRelations.push(data);
|
|
258
|
+
return { relation: {} };
|
|
259
|
+
});
|
|
260
|
+
// Should not throw
|
|
261
|
+
await detectAndCreatePatterns(client, ["new-1"], makeSession(), "ws-test");
|
|
262
|
+
// Pattern was still created despite relation error
|
|
263
|
+
expect(client.createdEntities).toHaveLength(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe("multiple new entities in one session", () => {
|
|
267
|
+
test("processes each new entity independently", async () => {
|
|
268
|
+
let callCount = 0;
|
|
269
|
+
const entityTypes = ["lesson", "error"];
|
|
270
|
+
const client = {
|
|
271
|
+
getMemoryEntity: mock(async (id) => ({
|
|
272
|
+
entity: { id, type: entityTypes[callCount++ % 2] ?? "lesson" },
|
|
273
|
+
})),
|
|
274
|
+
searchMemoryEntities: mock(async () => ({
|
|
275
|
+
entities: Array.from({ length: 4 }, (_, i) => ({
|
|
276
|
+
id: `old-${i}`,
|
|
277
|
+
type: "lesson",
|
|
278
|
+
})),
|
|
279
|
+
count: 4,
|
|
280
|
+
})),
|
|
281
|
+
listMemoryEntities: mock(async () => ({ entities: [], count: 0 })),
|
|
282
|
+
createMemoryEntity: mock(async (data) => ({
|
|
283
|
+
entity: { id: `pat-${Math.random()}`, ...data },
|
|
284
|
+
})),
|
|
285
|
+
updateMemoryEntity: mock(async () => ({ entity: {} })),
|
|
286
|
+
createMemoryRelation: mock(async () => ({ relation: {} })),
|
|
287
|
+
createdEntities: [],
|
|
288
|
+
createdRelations: [],
|
|
289
|
+
};
|
|
290
|
+
await detectAndCreatePatterns(client, ["new-1", "new-2"], makeSession(), "ws-test");
|
|
291
|
+
// Both new entities processed → 2 pattern entities potentially created
|
|
292
|
+
expect(client.getMemoryEntity).toHaveBeenCalledTimes(2);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|