@gethmy/mcp 2.4.7 → 2.5.0
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 +34 -1
- package/dist/cli.js +20826 -18366
- package/dist/index.js +20924 -18464
- package/dist/lib/api-client.js +122 -925
- package/package.json +2 -2
- package/src/__tests__/mcp-integration.test.ts +141 -0
- package/src/__tests__/memory-floor.test.ts +126 -0
- package/src/__tests__/memory-park.test.ts +213 -0
- package/src/__tests__/memory-session.test.ts +77 -0
- package/src/__tests__/prompt-builder.test.ts +234 -0
- package/src/__tests__/skills.test.ts +111 -0
- package/src/__tests__/tool-dispatch.test.ts +260 -0
- 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/src/__tests__/active-learning.test.ts +0 -483
- package/src/__tests__/agent-performance-profiles.test.ts +0 -468
- package/src/__tests__/context-assembly.test.ts +0 -506
- 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/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,528 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for memory quality audit.
|
|
3
|
-
*
|
|
4
|
-
* Run with: bun test packages/mcp-server/src/__tests__/memory-audit.test.ts
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
8
|
-
import { runMemoryAudit } from "../memory-audit.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(
|
|
15
|
-
entities: unknown[],
|
|
16
|
-
relations?: Record<string, number>,
|
|
17
|
-
) {
|
|
18
|
-
const deletedIds: string[] = [];
|
|
19
|
-
const updatedEntities: Array<{
|
|
20
|
-
id: string;
|
|
21
|
-
updates: Record<string, unknown>;
|
|
22
|
-
}> = [];
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
client: {
|
|
26
|
-
listMemoryEntities: mock(async (opts: { offset?: number }) => {
|
|
27
|
-
const offset = opts.offset ?? 0;
|
|
28
|
-
if (offset > 0) return { entities: [], count: 0 };
|
|
29
|
-
return { entities, count: entities.length };
|
|
30
|
-
}),
|
|
31
|
-
getRelatedEntities: mock(async (id: string) => {
|
|
32
|
-
const n = relations?.[id] ?? 0;
|
|
33
|
-
return {
|
|
34
|
-
outgoing: Array(n).fill({}),
|
|
35
|
-
incoming: [],
|
|
36
|
-
};
|
|
37
|
-
}),
|
|
38
|
-
deleteMemoryEntity: mock(async (id: string) => {
|
|
39
|
-
deletedIds.push(id);
|
|
40
|
-
return { success: true };
|
|
41
|
-
}),
|
|
42
|
-
updateMemoryEntity: mock(
|
|
43
|
-
async (id: string, updates: Record<string, unknown>) => {
|
|
44
|
-
updatedEntities.push({ id, updates });
|
|
45
|
-
return { entity: { id, ...updates } };
|
|
46
|
-
},
|
|
47
|
-
),
|
|
48
|
-
} as any,
|
|
49
|
-
deletedIds,
|
|
50
|
-
updatedEntities,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("runMemoryAudit", () => {
|
|
55
|
-
test("buckets a healthy modern entity into 'keep'", async () => {
|
|
56
|
-
const { client } = makeMockClient(
|
|
57
|
-
[
|
|
58
|
-
{
|
|
59
|
-
id: "healthy",
|
|
60
|
-
type: "pattern",
|
|
61
|
-
title: "Auth refresh token rotation pattern",
|
|
62
|
-
content:
|
|
63
|
-
"Rotate refresh tokens on every use. Keep a rolling window of two valid tokens to allow retry on network failures.",
|
|
64
|
-
confidence: 0.95,
|
|
65
|
-
memory_tier: "reference",
|
|
66
|
-
access_count: 25,
|
|
67
|
-
last_accessed_at: daysAgo(1),
|
|
68
|
-
created_at: daysAgo(90),
|
|
69
|
-
tags: ["auth", "security"],
|
|
70
|
-
embedding: [0.1, 0.2, 0.3],
|
|
71
|
-
promoted_from_id: "orig-1",
|
|
72
|
-
metadata: {},
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
{ healthy: 3 },
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
79
|
-
expect(report.summary.keep).toBe(1);
|
|
80
|
-
expect(report.summary.delete).toBe(0);
|
|
81
|
-
expect(report.lowest[0].score).toBeGreaterThanOrEqual(70);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("buckets a legacy default-confidence entity into archive/delete", async () => {
|
|
85
|
-
const { client } = makeMockClient([
|
|
86
|
-
{
|
|
87
|
-
id: "legacy",
|
|
88
|
-
type: "context",
|
|
89
|
-
title: "x",
|
|
90
|
-
content: "",
|
|
91
|
-
confidence: 1.0,
|
|
92
|
-
memory_tier: "draft",
|
|
93
|
-
access_count: 0,
|
|
94
|
-
last_accessed_at: null,
|
|
95
|
-
created_at: daysAgo(120),
|
|
96
|
-
tags: [],
|
|
97
|
-
embedding: null,
|
|
98
|
-
promoted_from_id: null,
|
|
99
|
-
metadata: {},
|
|
100
|
-
},
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
104
|
-
expect(report.summary.legacyCount).toBe(1);
|
|
105
|
-
const a = report.lowest[0];
|
|
106
|
-
expect(a.legacy).toBe(true);
|
|
107
|
-
expect(a.bucket === "archive" || a.bucket === "delete").toBe(true);
|
|
108
|
-
expect(a.legacyReasons.length).toBeGreaterThan(1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("dryRun does not delete, archive, or flag", async () => {
|
|
112
|
-
const { client, deletedIds, updatedEntities } = makeMockClient([
|
|
113
|
-
{
|
|
114
|
-
id: "bad",
|
|
115
|
-
type: "context",
|
|
116
|
-
title: "x",
|
|
117
|
-
content: "",
|
|
118
|
-
confidence: 0.1,
|
|
119
|
-
memory_tier: "draft",
|
|
120
|
-
access_count: 0,
|
|
121
|
-
last_accessed_at: null,
|
|
122
|
-
created_at: daysAgo(100),
|
|
123
|
-
tags: [],
|
|
124
|
-
embedding: null,
|
|
125
|
-
},
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
await runMemoryAudit(client, "ws-1", undefined, { dryRun: true });
|
|
129
|
-
expect(deletedIds).toHaveLength(0);
|
|
130
|
-
expect(updatedEntities).toHaveLength(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("executes delete + archive + flag in non-dryRun", async () => {
|
|
134
|
-
const { client, deletedIds, updatedEntities } = makeMockClient([
|
|
135
|
-
// delete (very low)
|
|
136
|
-
{
|
|
137
|
-
id: "trash",
|
|
138
|
-
type: "context",
|
|
139
|
-
title: "x",
|
|
140
|
-
content: "",
|
|
141
|
-
confidence: 0.05,
|
|
142
|
-
memory_tier: "draft",
|
|
143
|
-
access_count: 0,
|
|
144
|
-
last_accessed_at: null,
|
|
145
|
-
created_at: daysAgo(200),
|
|
146
|
-
tags: [],
|
|
147
|
-
},
|
|
148
|
-
// archive (middling-low)
|
|
149
|
-
{
|
|
150
|
-
id: "archive-me",
|
|
151
|
-
type: "pattern",
|
|
152
|
-
title: "Partial pattern that lacks context here",
|
|
153
|
-
content: "Some content that is a bit more substantive than nothing.",
|
|
154
|
-
confidence: 0.3,
|
|
155
|
-
memory_tier: "draft",
|
|
156
|
-
access_count: 0,
|
|
157
|
-
last_accessed_at: daysAgo(40),
|
|
158
|
-
created_at: daysAgo(40),
|
|
159
|
-
tags: [],
|
|
160
|
-
},
|
|
161
|
-
// review (medium) — decent content but no tags, no relations, no embedding
|
|
162
|
-
{
|
|
163
|
-
id: "review-me",
|
|
164
|
-
type: "pattern",
|
|
165
|
-
title: "Reasonable pattern with decent content body here",
|
|
166
|
-
content:
|
|
167
|
-
"This entity has enough content to pass the length check. Confidence is moderate, access is limited.",
|
|
168
|
-
confidence: 0.5,
|
|
169
|
-
memory_tier: "episode",
|
|
170
|
-
access_count: 1,
|
|
171
|
-
last_accessed_at: daysAgo(25),
|
|
172
|
-
created_at: daysAgo(40),
|
|
173
|
-
tags: [],
|
|
174
|
-
embedding: null,
|
|
175
|
-
},
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
const report = await runMemoryAudit(client, "ws-1", undefined, {
|
|
179
|
-
dryRun: false,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(deletedIds).toContain("trash");
|
|
183
|
-
expect(
|
|
184
|
-
updatedEntities.find((u) => u.id === "archive-me")?.updates.confidence,
|
|
185
|
-
).toBe(0.25);
|
|
186
|
-
expect(
|
|
187
|
-
(
|
|
188
|
-
updatedEntities.find((u) => u.id === "review-me")?.updates
|
|
189
|
-
.metadata as Record<string, unknown>
|
|
190
|
-
)?.needs_review,
|
|
191
|
-
).toBe(true);
|
|
192
|
-
|
|
193
|
-
expect(report.actionsTaken.deleted).toBeGreaterThanOrEqual(1);
|
|
194
|
-
expect(report.actionsTaken.archived).toBeGreaterThanOrEqual(1);
|
|
195
|
-
expect(report.actionsTaken.flaggedReview).toBeGreaterThanOrEqual(1);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("detects stuck-draft legacy signal", async () => {
|
|
199
|
-
const { client } = makeMockClient([
|
|
200
|
-
{
|
|
201
|
-
id: "stuck",
|
|
202
|
-
type: "context",
|
|
203
|
-
title: "Old draft that never made it",
|
|
204
|
-
content:
|
|
205
|
-
"Some content that is long enough to not count as thin content right here.",
|
|
206
|
-
confidence: 0.6,
|
|
207
|
-
memory_tier: "draft",
|
|
208
|
-
access_count: 1,
|
|
209
|
-
last_accessed_at: daysAgo(70),
|
|
210
|
-
created_at: daysAgo(75),
|
|
211
|
-
tags: ["x"],
|
|
212
|
-
embedding: [0.1],
|
|
213
|
-
promoted_from_id: null,
|
|
214
|
-
},
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
218
|
-
expect(report.legacyBreakdown.stuckDraft).toBe(1);
|
|
219
|
-
expect(report.lowest[0].reasons).toContain(
|
|
220
|
-
"stuck draft >60d never promoted",
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("distribution buckets sum to scanned count", async () => {
|
|
225
|
-
const entities = Array.from({ length: 10 }, (_, i) => ({
|
|
226
|
-
id: `e${i}`,
|
|
227
|
-
type: "context",
|
|
228
|
-
title: `Entity number ${i} with decent title length`,
|
|
229
|
-
content: "Some content string that is long enough to count properly.",
|
|
230
|
-
confidence: 0.1 * (i + 1),
|
|
231
|
-
memory_tier: i % 3 === 0 ? "reference" : "episode",
|
|
232
|
-
access_count: i,
|
|
233
|
-
last_accessed_at: daysAgo(i * 2),
|
|
234
|
-
created_at: daysAgo(i * 5 + 1),
|
|
235
|
-
tags: i % 2 === 0 ? ["tag"] : [],
|
|
236
|
-
embedding: i % 2 === 0 ? [0.1] : null,
|
|
237
|
-
}));
|
|
238
|
-
const { client } = makeMockClient(entities);
|
|
239
|
-
|
|
240
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
241
|
-
const total =
|
|
242
|
-
report.distribution["0-20"] +
|
|
243
|
-
report.distribution["20-40"] +
|
|
244
|
-
report.distribution["40-70"] +
|
|
245
|
-
report.distribution["70-100"];
|
|
246
|
-
expect(total).toBe(report.summary.scanned);
|
|
247
|
-
expect(report.summary.scanned).toBe(10);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("empty workspace returns success with zeros", async () => {
|
|
251
|
-
const { client } = makeMockClient([]);
|
|
252
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
253
|
-
expect(report.success).toBe(true);
|
|
254
|
-
expect(report.summary.scanned).toBe(0);
|
|
255
|
-
expect(report.lowest).toHaveLength(0);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("deleteBelow=0 disables deletion entirely", async () => {
|
|
259
|
-
const { client, deletedIds, updatedEntities } = makeMockClient([
|
|
260
|
-
{
|
|
261
|
-
id: "trash",
|
|
262
|
-
type: "context",
|
|
263
|
-
title: "x",
|
|
264
|
-
content: "",
|
|
265
|
-
confidence: 0.05,
|
|
266
|
-
memory_tier: "draft",
|
|
267
|
-
access_count: 0,
|
|
268
|
-
last_accessed_at: null,
|
|
269
|
-
created_at: daysAgo(200),
|
|
270
|
-
tags: [],
|
|
271
|
-
},
|
|
272
|
-
]);
|
|
273
|
-
|
|
274
|
-
await runMemoryAudit(client, "ws-1", undefined, {
|
|
275
|
-
dryRun: false,
|
|
276
|
-
deleteBelow: 0,
|
|
277
|
-
});
|
|
278
|
-
expect(deletedIds).toHaveLength(0);
|
|
279
|
-
// Should land in archive bucket (score < 40 but >= 0)
|
|
280
|
-
expect(
|
|
281
|
-
updatedEntities.find((u) => u.id === "trash")?.updates.confidence,
|
|
282
|
-
).toBe(0.25);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("boilerplate override forces delete bucket regardless of confidence/access/tier", async () => {
|
|
286
|
-
// The exact failure mode that motivated this override: legacy task-transition
|
|
287
|
-
// entries promoted to reference tier with confidence=1.0 and high access_count
|
|
288
|
-
// were scoring ~80 and surviving in "keep". Verify the override demotes them.
|
|
289
|
-
const { client, deletedIds } = makeMockClient(
|
|
290
|
-
[
|
|
291
|
-
{
|
|
292
|
-
id: "promoted-junk",
|
|
293
|
-
type: "context",
|
|
294
|
-
title: "Task transition: legacy auto-extracted noise",
|
|
295
|
-
content:
|
|
296
|
-
"Agent transitioned tasks. Previous: doing X. Current: doing Y. Progress: 100%.",
|
|
297
|
-
confidence: 1.0,
|
|
298
|
-
memory_tier: "reference",
|
|
299
|
-
access_count: 91,
|
|
300
|
-
last_accessed_at: daysAgo(0),
|
|
301
|
-
created_at: daysAgo(29),
|
|
302
|
-
tags: ["auto-extracted", "task-transition", "mid-session"],
|
|
303
|
-
embedding: [0.1],
|
|
304
|
-
promoted_from_id: "orig-junk",
|
|
305
|
-
},
|
|
306
|
-
],
|
|
307
|
-
{ "promoted-junk": 2 },
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const report = await runMemoryAudit(client, "ws-1", undefined, {
|
|
311
|
-
dryRun: false,
|
|
312
|
-
});
|
|
313
|
-
expect(report.summary.delete).toBe(1);
|
|
314
|
-
expect(deletedIds).toContain("promoted-junk");
|
|
315
|
-
expect(report.lowest[0].reasons).toContain("boilerplate title override");
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("legitimate titles starting with boilerplate-prefix words are NOT deleted", async () => {
|
|
319
|
-
// Regression test for the over-broad regex bug. Pre-fix patterns matched
|
|
320
|
-
// any title starting with "Placeholder", "Untitled", "Note", etc. After
|
|
321
|
-
// tightening, only exact boilerplate forms (with optional digit suffix
|
|
322
|
-
// or colon) match — real titles survive.
|
|
323
|
-
const { client, deletedIds } = makeMockClient(
|
|
324
|
-
[
|
|
325
|
-
{
|
|
326
|
-
id: "legit-placeholder",
|
|
327
|
-
type: "pattern",
|
|
328
|
-
title: "Placeholder pattern in React Suspense",
|
|
329
|
-
content:
|
|
330
|
-
"Use React.Suspense with a fallback component as the placeholder pattern for streaming SSR.",
|
|
331
|
-
confidence: 0.9,
|
|
332
|
-
memory_tier: "reference",
|
|
333
|
-
access_count: 12,
|
|
334
|
-
last_accessed_at: daysAgo(1),
|
|
335
|
-
created_at: daysAgo(60),
|
|
336
|
-
tags: ["react", "ssr"],
|
|
337
|
-
embedding: [0.1],
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
id: "legit-untitled",
|
|
341
|
-
type: "context",
|
|
342
|
-
title: "UntitledMaster.fig — design source for the homepage",
|
|
343
|
-
content:
|
|
344
|
-
"Reference Figma file containing master components for landing page assets.",
|
|
345
|
-
confidence: 0.85,
|
|
346
|
-
memory_tier: "reference",
|
|
347
|
-
access_count: 8,
|
|
348
|
-
last_accessed_at: daysAgo(2),
|
|
349
|
-
created_at: daysAgo(45),
|
|
350
|
-
tags: ["design"],
|
|
351
|
-
embedding: [0.1],
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
id: "legit-note",
|
|
355
|
-
type: "context",
|
|
356
|
-
title: "Note: schema migration order matters",
|
|
357
|
-
content: "Always run 0042 before 0043 because of FK dependency.",
|
|
358
|
-
confidence: 0.8,
|
|
359
|
-
memory_tier: "reference",
|
|
360
|
-
access_count: 5,
|
|
361
|
-
last_accessed_at: daysAgo(3),
|
|
362
|
-
created_at: daysAgo(30),
|
|
363
|
-
tags: ["db"],
|
|
364
|
-
embedding: [0.1],
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
{ "legit-placeholder": 3, "legit-untitled": 2, "legit-note": 1 },
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
const report = await runMemoryAudit(client, "ws-1", undefined, {
|
|
371
|
-
dryRun: false,
|
|
372
|
-
});
|
|
373
|
-
expect(deletedIds).toHaveLength(0);
|
|
374
|
-
expect(report.summary.delete).toBe(0);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test("empty-content draft with real title is NOT delete-bucketed", async () => {
|
|
378
|
-
// Users sometimes save a draft with title only and fill content later.
|
|
379
|
-
// The override is title-only, so empty content alone must not delete.
|
|
380
|
-
const { client, deletedIds } = makeMockClient([
|
|
381
|
-
{
|
|
382
|
-
id: "draft-empty-body",
|
|
383
|
-
type: "decision",
|
|
384
|
-
title: "Decision: skip Q3 launch",
|
|
385
|
-
content: "",
|
|
386
|
-
confidence: 0.7,
|
|
387
|
-
memory_tier: "draft",
|
|
388
|
-
access_count: 1,
|
|
389
|
-
last_accessed_at: daysAgo(1),
|
|
390
|
-
created_at: daysAgo(2),
|
|
391
|
-
tags: ["q3"],
|
|
392
|
-
embedding: null,
|
|
393
|
-
},
|
|
394
|
-
]);
|
|
395
|
-
|
|
396
|
-
await runMemoryAudit(client, "ws-1", undefined, { dryRun: false });
|
|
397
|
-
expect(deletedIds).not.toContain("draft-empty-body");
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test("boilerplate override respects deleteBelow=0 escape hatch", async () => {
|
|
401
|
-
// deleteBelow=0 is a "no deletions, audit-only" knob. Boilerplate must
|
|
402
|
-
// honor it — operators should be able to inspect findings without losing
|
|
403
|
-
// data on the same call.
|
|
404
|
-
const { client, deletedIds } = makeMockClient([
|
|
405
|
-
{
|
|
406
|
-
id: "boilerplate-protected",
|
|
407
|
-
type: "context",
|
|
408
|
-
title: "Task transition: would normally delete",
|
|
409
|
-
content: "noise content",
|
|
410
|
-
confidence: 1.0,
|
|
411
|
-
memory_tier: "reference",
|
|
412
|
-
access_count: 50,
|
|
413
|
-
last_accessed_at: daysAgo(0),
|
|
414
|
-
created_at: daysAgo(20),
|
|
415
|
-
tags: ["x"],
|
|
416
|
-
embedding: [0.1],
|
|
417
|
-
},
|
|
418
|
-
]);
|
|
419
|
-
|
|
420
|
-
await runMemoryAudit(client, "ws-1", undefined, {
|
|
421
|
-
dryRun: false,
|
|
422
|
-
deleteBelow: 0,
|
|
423
|
-
});
|
|
424
|
-
expect(deletedIds).toHaveLength(0);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test("stale-draft filter flags draft+0access+age>threshold separately from bucket", async () => {
|
|
428
|
-
const { client } = makeMockClient(
|
|
429
|
-
[
|
|
430
|
-
// Stale draft — should be flagged by the filter, but otherwise healthy
|
|
431
|
-
// enough to bucket as "review" (not archive).
|
|
432
|
-
{
|
|
433
|
-
id: "stale-draft",
|
|
434
|
-
type: "context",
|
|
435
|
-
title:
|
|
436
|
-
"Task transition: feature work started but never touched again",
|
|
437
|
-
content:
|
|
438
|
-
"This draft has enough content and tags to score reasonably, " +
|
|
439
|
-
"but nobody ever accessed it after creation — classic promote-or-drop candidate.",
|
|
440
|
-
confidence: 0.4,
|
|
441
|
-
memory_tier: "draft",
|
|
442
|
-
access_count: 0,
|
|
443
|
-
last_accessed_at: null,
|
|
444
|
-
created_at: daysAgo(10),
|
|
445
|
-
tags: ["task"],
|
|
446
|
-
embedding: [0.1],
|
|
447
|
-
},
|
|
448
|
-
// Fresh draft — same shape but under the age threshold, must NOT flag.
|
|
449
|
-
{
|
|
450
|
-
id: "fresh-draft",
|
|
451
|
-
type: "context",
|
|
452
|
-
title: "Task transition: a fresh draft still within the grace window",
|
|
453
|
-
content:
|
|
454
|
-
"Content long enough to not be thin at all, really properly sized.",
|
|
455
|
-
confidence: 0.4,
|
|
456
|
-
memory_tier: "draft",
|
|
457
|
-
access_count: 0,
|
|
458
|
-
last_accessed_at: null,
|
|
459
|
-
created_at: daysAgo(3),
|
|
460
|
-
tags: ["task"],
|
|
461
|
-
embedding: [0.1],
|
|
462
|
-
},
|
|
463
|
-
// Non-draft old zero-access — must NOT flag (filter is draft-only).
|
|
464
|
-
{
|
|
465
|
-
id: "old-episode",
|
|
466
|
-
type: "pattern",
|
|
467
|
-
title: "Episode entity that is old and unaccessed but not a draft",
|
|
468
|
-
content:
|
|
469
|
-
"Sometimes reference/episode tier entities sit unaccessed; " +
|
|
470
|
-
"they're not draft-promotion candidates so the filter should skip them.",
|
|
471
|
-
confidence: 0.8,
|
|
472
|
-
memory_tier: "episode",
|
|
473
|
-
access_count: 0,
|
|
474
|
-
last_accessed_at: null,
|
|
475
|
-
created_at: daysAgo(30),
|
|
476
|
-
tags: ["pat"],
|
|
477
|
-
embedding: [0.1],
|
|
478
|
-
},
|
|
479
|
-
],
|
|
480
|
-
{ "stale-draft": 1, "fresh-draft": 1, "old-episode": 2 },
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
484
|
-
expect(report.summary.staleDraftCount).toBe(1);
|
|
485
|
-
expect(report.staleDrafts).toHaveLength(1);
|
|
486
|
-
expect(report.staleDrafts[0].id).toBe("stale-draft");
|
|
487
|
-
expect(report.healthReport).toContain("Stale Drafts");
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
test("staleDraftAgeDays option tunes the filter threshold", async () => {
|
|
491
|
-
const { client } = makeMockClient([
|
|
492
|
-
{
|
|
493
|
-
id: "draft-5d",
|
|
494
|
-
type: "context",
|
|
495
|
-
title: "Five-day-old untouched draft",
|
|
496
|
-
content:
|
|
497
|
-
"Content long enough to pass the thin-content check, absolutely.",
|
|
498
|
-
confidence: 0.4,
|
|
499
|
-
memory_tier: "draft",
|
|
500
|
-
access_count: 0,
|
|
501
|
-
last_accessed_at: null,
|
|
502
|
-
created_at: daysAgo(5),
|
|
503
|
-
tags: ["x"],
|
|
504
|
-
embedding: [0.1],
|
|
505
|
-
},
|
|
506
|
-
]);
|
|
507
|
-
|
|
508
|
-
const defaultRun = await runMemoryAudit(client, "ws-1");
|
|
509
|
-
expect(defaultRun.summary.staleDraftCount).toBe(0);
|
|
510
|
-
|
|
511
|
-
const tightRun = await runMemoryAudit(client, "ws-1", undefined, {
|
|
512
|
-
staleDraftAgeDays: 3,
|
|
513
|
-
});
|
|
514
|
-
expect(tightRun.summary.staleDraftCount).toBe(1);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
test("fetch error surfaces as report.success=false", async () => {
|
|
518
|
-
const client = {
|
|
519
|
-
listMemoryEntities: mock(async () => {
|
|
520
|
-
throw new Error("API down");
|
|
521
|
-
}),
|
|
522
|
-
} as any;
|
|
523
|
-
|
|
524
|
-
const report = await runMemoryAudit(client, "ws-1");
|
|
525
|
-
expect(report.success).toBe(false);
|
|
526
|
-
expect(report.errors.length).toBeGreaterThan(0);
|
|
527
|
-
});
|
|
528
|
-
});
|