@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.
- package/README.md +36 -20
- package/dist/cli.js +1857 -30209
- package/dist/index.js +1323 -26686
- package/dist/lib/api-client.js +122 -925
- package/package.json +4 -3
- package/src/api-client.ts +129 -96
- package/src/memory-floor.ts +264 -0
- package/src/memory-park.ts +252 -0
- package/src/memory-session.ts +61 -0
- package/src/prompt-builder.ts +93 -0
- package/src/server.ts +351 -1467
- package/dist/http.js +0 -1959
- package/dist/remote.js +0 -32328
- package/dist/server.js +0 -31967
- package/src/__tests__/active-learning.test.ts +0 -483
- package/src/__tests__/agent-performance-profiles.test.ts +0 -468
- package/src/__tests__/auto-session.test.ts +0 -912
- package/src/__tests__/context-assembly.test.ts +0 -506
- package/src/__tests__/graph-expansion.test.ts +0 -285
- package/src/__tests__/integration-memory-crud.test.ts +0 -948
- package/src/__tests__/integration-memory-system.test.ts +0 -321
- package/src/__tests__/lifecycle-maintenance.test.ts +0 -238
- package/src/__tests__/memory-audit.test.ts +0 -528
- package/src/__tests__/pattern-detection.test.ts +0 -438
- package/src/__tests__/prompt-builder.test.ts +0 -505
- package/src/__tests__/remote-routing.test.ts +0 -285
- package/src/active-learning.ts +0 -1165
- package/src/consolidation.ts +0 -383
- package/src/context-assembly.ts +0 -1175
- package/src/lifecycle-maintenance.ts +0 -120
- package/src/memory-audit.ts +0 -578
- package/src/memory-cleanup.ts +0 -902
|
@@ -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
|
-
});
|