@gethmy/mcp 2.4.7 → 2.5.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.
@@ -1,321 +0,0 @@
1
- /**
2
- * Integration tests for the Intelligent Agent Memory System.
3
- *
4
- * These tests exercise the MCP tools end-to-end via the Harmony API.
5
- * Prerequisites:
6
- * 1. `supabase db push` (migration applied)
7
- * 2. MCP server configured with valid API key
8
- * 3. Active workspace and project set
9
- *
10
- * Run with: bun test packages/mcp-server/src/__tests__/integration-memory-system.test.ts
11
- *
12
- * These tests create real entities and clean them up afterwards.
13
- * If a test fails mid-way, you may need to manually delete leftover entities.
14
- */
15
-
16
- import { afterAll, describe, expect, test } from "bun:test";
17
- import { getClient } from "../api-client.js";
18
- import {
19
- getActiveProjectId,
20
- getActiveWorkspaceId,
21
- isConfigured,
22
- loadConfig,
23
- } from "../config.js";
24
-
25
- // Track entities created during tests for cleanup
26
- const createdEntityIds: string[] = [];
27
-
28
- // Skip all tests if MCP server isn't configured
29
- const configured = (() => {
30
- try {
31
- loadConfig();
32
- return isConfigured() && !!getActiveWorkspaceId();
33
- } catch {
34
- return false;
35
- }
36
- })();
37
-
38
- // Check if the tier migration has been applied by testing a create
39
- const migrationApplied = await (async () => {
40
- if (!configured) return false;
41
- try {
42
- const client = getClient();
43
- const result = await client.createMemoryEntity({
44
- workspace_id: getActiveWorkspaceId()!,
45
- project_id: getActiveProjectId() || undefined,
46
- type: "context",
47
- scope: getActiveProjectId() ? "project" : "workspace",
48
- memory_tier: "draft",
49
- title: "[TEST] Migration check - delete me",
50
- content: "Checking if memory_tier column exists.",
51
- agent_identifier: "test-runner",
52
- });
53
- const entity = result.entity as Record<string, unknown>;
54
- const hasTier = entity.memory_tier === "draft";
55
- // Clean up probe entity
56
- if (entity.id) {
57
- await client.deleteMemoryEntity(entity.id as string).catch(() => {});
58
- }
59
- return hasTier;
60
- } catch {
61
- return false;
62
- }
63
- })();
64
-
65
- const describeIf = configured ? describe : describe.skip;
66
- const describeIfMigrated =
67
- configured && migrationApplied ? describe : describe.skip;
68
-
69
- // Cleanup: delete all test entities
70
- afterAll(async () => {
71
- if (!configured || createdEntityIds.length === 0) return;
72
- const client = getClient();
73
- for (const id of createdEntityIds) {
74
- try {
75
- await client.deleteMemoryEntity(id);
76
- } catch {
77
- // Entity may already be deleted
78
- }
79
- }
80
- });
81
-
82
- describeIfMigrated("Memory Tier System (integration)", () => {
83
- test("create entity with explicit tier", async () => {
84
- const client = getClient();
85
- const workspaceId = getActiveWorkspaceId()!;
86
- const projectId = getActiveProjectId() || undefined;
87
-
88
- const result = await client.createMemoryEntity({
89
- workspace_id: workspaceId,
90
- project_id: projectId,
91
- type: "context",
92
- scope: projectId ? "project" : "workspace",
93
- memory_tier: "draft",
94
- title: "[TEST] Draft memory tier test",
95
- content: "This is a test draft memory.",
96
- tags: ["test", "integration"],
97
- agent_identifier: "test-runner",
98
- });
99
-
100
- const entity = result.entity as Record<string, unknown>;
101
- expect(entity.id).toBeTruthy();
102
- expect(entity.memory_tier).toBe("draft");
103
- createdEntityIds.push(entity.id as string);
104
- });
105
-
106
- test("create entity with default tier based on type", async () => {
107
- const client = getClient();
108
- const workspaceId = getActiveWorkspaceId()!;
109
- const projectId = getActiveProjectId() || undefined;
110
-
111
- // 'lesson' type should default to 'episode' tier
112
- const result = await client.createMemoryEntity({
113
- workspace_id: workspaceId,
114
- project_id: projectId,
115
- type: "lesson",
116
- scope: projectId ? "project" : "workspace",
117
- title: "[TEST] Lesson tier default",
118
- content: "Testing that lessons default to episode tier.",
119
- tags: ["test", "integration"],
120
- agent_identifier: "test-runner",
121
- });
122
-
123
- const entity = result.entity as Record<string, unknown>;
124
- expect(entity.id).toBeTruthy();
125
- expect(entity.memory_tier).toBe("episode");
126
- createdEntityIds.push(entity.id as string);
127
- });
128
-
129
- test("create pattern defaults to reference tier", async () => {
130
- const client = getClient();
131
- const workspaceId = getActiveWorkspaceId()!;
132
- const projectId = getActiveProjectId() || undefined;
133
-
134
- const result = await client.createMemoryEntity({
135
- workspace_id: workspaceId,
136
- project_id: projectId,
137
- type: "pattern",
138
- scope: projectId ? "project" : "workspace",
139
- title: "[TEST] Pattern tier default",
140
- content: "Testing that patterns default to reference tier.",
141
- tags: ["test", "integration"],
142
- agent_identifier: "test-runner",
143
- });
144
-
145
- const entity = result.entity as Record<string, unknown>;
146
- expect(entity.memory_tier).toBe("reference");
147
- createdEntityIds.push(entity.id as string);
148
- });
149
-
150
- test("update entity tier via promote", async () => {
151
- const client = getClient();
152
- const workspaceId = getActiveWorkspaceId()!;
153
- const projectId = getActiveProjectId() || undefined;
154
-
155
- // Create a draft
156
- const createResult = await client.createMemoryEntity({
157
- workspace_id: workspaceId,
158
- project_id: projectId,
159
- type: "context",
160
- scope: projectId ? "project" : "workspace",
161
- memory_tier: "draft",
162
- title: "[TEST] Promotion test draft",
163
- content: "Draft that will be promoted.",
164
- tags: ["test", "integration"],
165
- agent_identifier: "test-runner",
166
- });
167
-
168
- const entityId = (createResult.entity as Record<string, unknown>)
169
- .id as string;
170
- createdEntityIds.push(entityId);
171
-
172
- // Promote to episode
173
- const updateResult = await client.updateMemoryEntity(entityId, {
174
- memory_tier: "episode",
175
- metadata: {
176
- promoted_from_tier: "draft",
177
- promotion_reason: "test promotion",
178
- promoted_at: new Date().toISOString(),
179
- },
180
- });
181
-
182
- const updated = updateResult.entity as Record<string, unknown>;
183
- expect(updated.memory_tier).toBe("episode");
184
- });
185
- });
186
-
187
- describeIf("Context Assembly (integration)", () => {
188
- test("search returns entities with tier info", async () => {
189
- const client = getClient();
190
- const workspaceId = getActiveWorkspaceId()!;
191
- const projectId = getActiveProjectId() || undefined;
192
-
193
- // Create a searchable entity
194
- const createResult = await client.createMemoryEntity({
195
- workspace_id: workspaceId,
196
- project_id: projectId,
197
- type: "solution",
198
- scope: projectId ? "project" : "workspace",
199
- memory_tier: "reference",
200
- title: "[TEST] Unique search term xyzzy42",
201
- content: "A solution for testing context assembly search.",
202
- confidence: 0.95,
203
- tags: ["test", "context-assembly"],
204
- agent_identifier: "test-runner",
205
- });
206
-
207
- const entityId = (createResult.entity as Record<string, unknown>)
208
- .id as string;
209
- createdEntityIds.push(entityId);
210
-
211
- // Search for it
212
- const searchResult = await client.searchMemoryEntities(
213
- workspaceId,
214
- "xyzzy42",
215
- { project_id: projectId, limit: 5 },
216
- );
217
-
218
- expect(searchResult.entities.length).toBeGreaterThan(0);
219
- const found = searchResult.entities.find(
220
- (e: unknown) => (e as Record<string, unknown>).id === entityId,
221
- );
222
- expect(found).toBeTruthy();
223
- expect((found as Record<string, unknown>).memory_tier).toBe("reference");
224
- });
225
- });
226
-
227
- describeIf("Graph Walk (integration)", () => {
228
- test("create entities with relations and traverse", async () => {
229
- const client = getClient();
230
- const workspaceId = getActiveWorkspaceId()!;
231
- const projectId = getActiveProjectId() || undefined;
232
- const scope = projectId ? "project" : "workspace";
233
-
234
- // Create two related entities
235
- const errorResult = await client.createMemoryEntity({
236
- workspace_id: workspaceId,
237
- project_id: projectId,
238
- type: "error",
239
- scope,
240
- memory_tier: "reference",
241
- title: "[TEST] Graph walk error",
242
- content: "An error for graph walk testing.",
243
- tags: ["test", "graph-walk"],
244
- agent_identifier: "test-runner",
245
- });
246
- const errorId = (errorResult.entity as Record<string, unknown>)
247
- .id as string;
248
- createdEntityIds.push(errorId);
249
-
250
- const solutionResult = await client.createMemoryEntity({
251
- workspace_id: workspaceId,
252
- project_id: projectId,
253
- type: "solution",
254
- scope,
255
- memory_tier: "reference",
256
- title: "[TEST] Graph walk solution",
257
- content: "A solution for graph walk testing.",
258
- tags: ["test", "graph-walk"],
259
- agent_identifier: "test-runner",
260
- });
261
- const solutionId = (solutionResult.entity as Record<string, unknown>)
262
- .id as string;
263
- createdEntityIds.push(solutionId);
264
-
265
- // Create relation
266
- const relResult = await client.createMemoryRelation({
267
- source_id: errorId,
268
- target_id: solutionId,
269
- relation_type: "resolved_by",
270
- confidence: 1.0,
271
- });
272
- expect((relResult.relation as Record<string, unknown>).id).toBeTruthy();
273
-
274
- // Verify relations via getRelatedEntities
275
- const related = await client.getRelatedEntities(errorId);
276
- expect(related.outgoing.length).toBeGreaterThan(0);
277
-
278
- // API returns relations with embedded target entity, not flat target_id
279
- const outRel = related.outgoing.find((r: unknown) => {
280
- const rel = r as Record<string, unknown>;
281
- const target = rel.target as Record<string, unknown> | undefined;
282
- return target?.id === solutionId || rel.target_id === solutionId;
283
- });
284
- expect(outRel).toBeTruthy();
285
- });
286
- });
287
-
288
- describeIf("Vault Index with Tiers (integration)", () => {
289
- test("vault index includes tier information", async () => {
290
- const client = getClient();
291
- const workspaceId = getActiveWorkspaceId()!;
292
- const projectId = getActiveProjectId() || undefined;
293
-
294
- let result: { entities: unknown[]; count: number };
295
- try {
296
- result = await client.getVaultIndex({
297
- workspace_id: workspaceId,
298
- project_id: projectId,
299
- limit: 10,
300
- });
301
- } catch (e) {
302
- // Endpoint may not be deployed yet - skip gracefully
303
- const msg = e instanceof Error ? e.message : String(e);
304
- if (msg.includes("Unknown endpoint")) {
305
- console.log("Skipping: vault index endpoint not deployed yet");
306
- return;
307
- }
308
- throw e;
309
- }
310
-
311
- expect(result.entities).toBeDefined();
312
- // If there are entities, check they have tier info
313
- if (result.entities.length > 0) {
314
- const first = result.entities[0] as Record<string, unknown>;
315
- // memory_tier might be present if migration is applied
316
- if (first.memory_tier !== undefined) {
317
- expect(["draft", "episode", "reference"]).toContain(first.memory_tier);
318
- }
319
- }
320
- });
321
- });
@@ -1,238 +0,0 @@
1
- /**
2
- * Unit tests for automatic lifecycle maintenance.
3
- *
4
- * Run with: bun test packages/mcp-server/src/__tests__/lifecycle-maintenance.test.ts
5
- */
6
-
7
- import { describe, expect, mock, test } from "bun:test";
8
- import { runLifecycleMaintenance } from "../lifecycle-maintenance.js";
9
-
10
- function daysAgo(days: number): string {
11
- return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
12
- }
13
-
14
- function makeMockClient(entities: unknown[]) {
15
- const deletedIds: string[] = [];
16
- const updatedEntities: Array<{
17
- id: string;
18
- updates: Record<string, unknown>;
19
- }> = [];
20
-
21
- return {
22
- client: {
23
- listMemoryEntities: mock(async () => ({
24
- entities,
25
- count: entities.length,
26
- })),
27
- deleteMemoryEntity: mock(async (id: string) => {
28
- deletedIds.push(id);
29
- return { success: true };
30
- }),
31
- updateMemoryEntity: mock(
32
- async (id: string, updates: Record<string, unknown>) => {
33
- updatedEntities.push({ id, updates });
34
- return { entity: { id, ...updates } };
35
- },
36
- ),
37
- } as any,
38
- deletedIds,
39
- updatedEntities,
40
- };
41
- }
42
-
43
- describe("runLifecycleMaintenance", () => {
44
- test("archives low-confidence entities", async () => {
45
- const { client, deletedIds } = makeMockClient([
46
- {
47
- id: "low-conf-1",
48
- title: "Bad memory",
49
- memory_tier: "draft",
50
- confidence: 0.1,
51
- access_count: 0,
52
- last_accessed_at: daysAgo(5),
53
- created_at: daysAgo(10),
54
- },
55
- ]);
56
-
57
- const result = await runLifecycleMaintenance(client, "ws-1");
58
- expect(result.archived).toBe(1);
59
- expect(deletedIds).toContain("low-conf-1");
60
- });
61
-
62
- test("prunes stale drafts older than 30 days with low decay", async () => {
63
- const { client, deletedIds } = makeMockClient([
64
- {
65
- id: "stale-draft-1",
66
- title: "Old draft",
67
- memory_tier: "draft",
68
- confidence: 0.5,
69
- access_count: 0,
70
- last_accessed_at: daysAgo(35),
71
- created_at: daysAgo(35),
72
- },
73
- ]);
74
-
75
- const result = await runLifecycleMaintenance(client, "ws-1");
76
- expect(result.pruned).toBe(1);
77
- expect(deletedIds).toContain("stale-draft-1");
78
- });
79
-
80
- test("does NOT prune recent drafts", async () => {
81
- const { client, deletedIds } = makeMockClient([
82
- {
83
- id: "fresh-draft",
84
- title: "New draft",
85
- memory_tier: "draft",
86
- confidence: 0.5,
87
- access_count: 1,
88
- last_accessed_at: daysAgo(2),
89
- created_at: daysAgo(5),
90
- },
91
- ]);
92
-
93
- const result = await runLifecycleMaintenance(client, "ws-1");
94
- expect(result.pruned).toBe(0);
95
- expect(result.archived).toBe(0);
96
- expect(deletedIds).toHaveLength(0);
97
- });
98
-
99
- test("auto-promotes eligible draft to episode", async () => {
100
- const { client, updatedEntities } = makeMockClient([
101
- {
102
- id: "promote-me",
103
- title: "Well-used draft",
104
- memory_tier: "draft",
105
- confidence: 0.85,
106
- access_count: 6,
107
- last_accessed_at: daysAgo(0),
108
- created_at: daysAgo(3),
109
- },
110
- ]);
111
-
112
- const result = await runLifecycleMaintenance(client, "ws-1");
113
- expect(result.promoted).toBe(1);
114
- expect(updatedEntities[0].id).toBe("promote-me");
115
- expect(updatedEntities[0].updates.memory_tier).toBe("episode");
116
- });
117
-
118
- test("auto-promotes eligible episode to reference", async () => {
119
- const { client, updatedEntities } = makeMockClient([
120
- {
121
- id: "promote-ep",
122
- title: "Proven episode",
123
- memory_tier: "episode",
124
- confidence: 0.95,
125
- access_count: 12,
126
- last_accessed_at: daysAgo(1),
127
- created_at: daysAgo(10),
128
- },
129
- ]);
130
-
131
- const result = await runLifecycleMaintenance(client, "ws-1");
132
- expect(result.promoted).toBe(1);
133
- expect(updatedEntities[0].updates.memory_tier).toBe("reference");
134
- });
135
-
136
- test("flags stale entities for review", async () => {
137
- const { client, updatedEntities } = makeMockClient([
138
- {
139
- id: "stale-ep",
140
- title: "Forgotten episode",
141
- memory_tier: "episode",
142
- confidence: 0.6,
143
- access_count: 2,
144
- last_accessed_at: daysAgo(100),
145
- created_at: daysAgo(120),
146
- },
147
- ]);
148
-
149
- const result = await runLifecycleMaintenance(client, "ws-1");
150
- expect(result.reviewed).toBe(1);
151
- expect(updatedEntities[0].id).toBe("stale-ep");
152
- expect(updatedEntities[0].updates.metadata).toHaveProperty(
153
- "needs_review",
154
- true,
155
- );
156
- });
157
-
158
- test("does not flag frequently accessed stale entities", async () => {
159
- const { client, updatedEntities } = makeMockClient([
160
- {
161
- id: "old-but-used",
162
- title: "Still relevant",
163
- memory_tier: "episode",
164
- confidence: 0.7,
165
- access_count: 10,
166
- last_accessed_at: daysAgo(100),
167
- created_at: daysAgo(120),
168
- },
169
- ]);
170
-
171
- const result = await runLifecycleMaintenance(client, "ws-1");
172
- expect(result.reviewed).toBe(0);
173
- expect(updatedEntities).toHaveLength(0);
174
- });
175
-
176
- test("handles empty entity list", async () => {
177
- const { client } = makeMockClient([]);
178
- const result = await runLifecycleMaintenance(client, "ws-1");
179
- expect(result.archived).toBe(0);
180
- expect(result.pruned).toBe(0);
181
- expect(result.promoted).toBe(0);
182
- expect(result.reviewed).toBe(0);
183
- });
184
-
185
- test("handles API errors gracefully", async () => {
186
- const client = {
187
- listMemoryEntities: mock(async () => {
188
- throw new Error("API down");
189
- }),
190
- } as any;
191
-
192
- const result = await runLifecycleMaintenance(client, "ws-1");
193
- expect(result.errors).toBe(0); // Early return, no entity-level errors
194
- expect(result.archived).toBe(0);
195
- });
196
-
197
- test("processes mixed entities correctly", async () => {
198
- const { client, deletedIds, updatedEntities } = makeMockClient([
199
- // Should be archived (low confidence)
200
- {
201
- id: "archive-me",
202
- title: "Low conf",
203
- memory_tier: "draft",
204
- confidence: 0.2,
205
- access_count: 1,
206
- last_accessed_at: daysAgo(5),
207
- created_at: daysAgo(10),
208
- },
209
- // Should be promoted (draft→episode)
210
- {
211
- id: "promote-me",
212
- title: "Good draft",
213
- memory_tier: "draft",
214
- confidence: 0.85,
215
- access_count: 7,
216
- last_accessed_at: daysAgo(0),
217
- created_at: daysAgo(5),
218
- },
219
- // Should be left alone (healthy reference)
220
- {
221
- id: "leave-me",
222
- title: "Healthy ref",
223
- memory_tier: "reference",
224
- confidence: 0.95,
225
- access_count: 20,
226
- last_accessed_at: daysAgo(1),
227
- created_at: daysAgo(60),
228
- },
229
- ]);
230
-
231
- const result = await runLifecycleMaintenance(client, "ws-1");
232
- expect(result.archived).toBe(1);
233
- expect(result.promoted).toBe(1);
234
- expect(deletedIds).toContain("archive-me");
235
- expect(updatedEntities[0].id).toBe("promote-me");
236
- expect(updatedEntities[0].updates.memory_tier).toBe("episode");
237
- });
238
- });