@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,797 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for Memory CRUD, Relations, Search, and Wiki-Links.
|
|
3
|
-
*
|
|
4
|
-
* Covers the full lifecycle of all memory MCP tools:
|
|
5
|
-
* harmony_remember → harmony_recall → harmony_update_memory → harmony_forget
|
|
6
|
-
* harmony_relate → harmony_memory_search → harmony_vault_index → harmony_resolve_links
|
|
7
|
-
*
|
|
8
|
-
* Prerequisites:
|
|
9
|
-
* 1. `supabase db push` (migrations applied)
|
|
10
|
-
* 2. MCP server configured with valid API key
|
|
11
|
-
* 3. Active workspace and project set
|
|
12
|
-
*
|
|
13
|
-
* Run with: bun test packages/mcp-server/src/__tests__/integration-memory-crud.test.ts
|
|
14
|
-
*/
|
|
15
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
16
|
-
import { getClient } from "../api-client.js";
|
|
17
|
-
import { getActiveProjectId, getActiveWorkspaceId, isConfigured, loadConfig, } from "../config.js";
|
|
18
|
-
// Track entities for cleanup
|
|
19
|
-
const createdEntityIds = [];
|
|
20
|
-
const createdRelationIds = [];
|
|
21
|
-
// Skip all tests if MCP server isn't configured
|
|
22
|
-
const configured = (() => {
|
|
23
|
-
try {
|
|
24
|
-
loadConfig();
|
|
25
|
-
return isConfigured() && !!getActiveWorkspaceId();
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
})();
|
|
31
|
-
const describeIf = configured ? describe : describe.skip;
|
|
32
|
-
function getWorkspaceId() {
|
|
33
|
-
return getActiveWorkspaceId();
|
|
34
|
-
}
|
|
35
|
-
function getProjectId() {
|
|
36
|
-
return getActiveProjectId() || undefined;
|
|
37
|
-
}
|
|
38
|
-
function getScope() {
|
|
39
|
-
return getProjectId() ? "project" : "workspace";
|
|
40
|
-
}
|
|
41
|
-
// Cleanup (generous timeout for many API calls)
|
|
42
|
-
afterAll(async () => {
|
|
43
|
-
if (!configured)
|
|
44
|
-
return;
|
|
45
|
-
const client = getClient();
|
|
46
|
-
for (const id of createdRelationIds) {
|
|
47
|
-
try {
|
|
48
|
-
await client.deleteMemoryRelation(id);
|
|
49
|
-
}
|
|
50
|
-
catch { }
|
|
51
|
-
}
|
|
52
|
-
for (const id of createdEntityIds) {
|
|
53
|
-
try {
|
|
54
|
-
await client.deleteMemoryEntity(id);
|
|
55
|
-
}
|
|
56
|
-
catch { }
|
|
57
|
-
}
|
|
58
|
-
}, 30000);
|
|
59
|
-
// ============================================================
|
|
60
|
-
// 1. Entity CRUD Round-Trip
|
|
61
|
-
// ============================================================
|
|
62
|
-
describeIf("Entity CRUD Round-Trip", () => {
|
|
63
|
-
let entityId;
|
|
64
|
-
test("CREATE: harmony_remember creates entity with all fields", async () => {
|
|
65
|
-
const client = getClient();
|
|
66
|
-
const result = await client.createMemoryEntity({
|
|
67
|
-
workspace_id: getWorkspaceId(),
|
|
68
|
-
project_id: getProjectId(),
|
|
69
|
-
type: "pattern",
|
|
70
|
-
scope: getScope(),
|
|
71
|
-
memory_tier: "reference",
|
|
72
|
-
title: "[TEST-CRUD] Unique pattern alpha-bravo-42",
|
|
73
|
-
content: "This is a test pattern for CRUD round-trip testing.",
|
|
74
|
-
confidence: 0.85,
|
|
75
|
-
tags: ["test", "crud", "integration"],
|
|
76
|
-
metadata: { source: "test-suite", version: 1 },
|
|
77
|
-
agent_identifier: "test-runner",
|
|
78
|
-
});
|
|
79
|
-
const entity = result.entity;
|
|
80
|
-
expect(entity.id).toBeTruthy();
|
|
81
|
-
expect(entity.title).toBe("[TEST-CRUD] Unique pattern alpha-bravo-42");
|
|
82
|
-
expect(entity.type).toBe("pattern");
|
|
83
|
-
expect(entity.scope).toBe(getScope());
|
|
84
|
-
expect(entity.memory_tier).toBe("reference");
|
|
85
|
-
expect(entity.confidence).toBe(0.85);
|
|
86
|
-
expect(entity.tags).toEqual(["test", "crud", "integration"]);
|
|
87
|
-
expect(entity.metadata.source).toBe("test-suite");
|
|
88
|
-
expect(entity.agent_identifier).toBe("test-runner");
|
|
89
|
-
entityId = entity.id;
|
|
90
|
-
createdEntityIds.push(entityId);
|
|
91
|
-
});
|
|
92
|
-
test("READ: getMemoryEntity returns the created entity", async () => {
|
|
93
|
-
const client = getClient();
|
|
94
|
-
const result = await client.getMemoryEntity(entityId);
|
|
95
|
-
const entity = result.entity;
|
|
96
|
-
expect(entity.id).toBe(entityId);
|
|
97
|
-
expect(entity.title).toBe("[TEST-CRUD] Unique pattern alpha-bravo-42");
|
|
98
|
-
expect(entity.content).toContain("CRUD round-trip");
|
|
99
|
-
});
|
|
100
|
-
test("READ: listMemoryEntities with type filter finds entity", async () => {
|
|
101
|
-
const client = getClient();
|
|
102
|
-
const result = await client.listMemoryEntities({
|
|
103
|
-
workspace_id: getWorkspaceId(),
|
|
104
|
-
project_id: getProjectId(),
|
|
105
|
-
type: "pattern",
|
|
106
|
-
agent_identifier: "test-runner",
|
|
107
|
-
limit: 50,
|
|
108
|
-
});
|
|
109
|
-
const found = result.entities.find((e) => e.id === entityId);
|
|
110
|
-
expect(found).toBeTruthy();
|
|
111
|
-
});
|
|
112
|
-
test("READ: listMemoryEntities with tag filter finds entity", async () => {
|
|
113
|
-
const client = getClient();
|
|
114
|
-
const result = await client.listMemoryEntities({
|
|
115
|
-
workspace_id: getWorkspaceId(),
|
|
116
|
-
project_id: getProjectId(),
|
|
117
|
-
tags: ["crud"],
|
|
118
|
-
agent_identifier: "test-runner",
|
|
119
|
-
limit: 50,
|
|
120
|
-
});
|
|
121
|
-
const found = result.entities.find((e) => e.id === entityId);
|
|
122
|
-
expect(found).toBeTruthy();
|
|
123
|
-
});
|
|
124
|
-
test("READ: listMemoryEntities with min_confidence filters correctly", async () => {
|
|
125
|
-
const client = getClient();
|
|
126
|
-
// Should find it (0.85 >= 0.8)
|
|
127
|
-
const resultHigh = await client.listMemoryEntities({
|
|
128
|
-
workspace_id: getWorkspaceId(),
|
|
129
|
-
project_id: getProjectId(),
|
|
130
|
-
min_confidence: 0.8,
|
|
131
|
-
agent_identifier: "test-runner",
|
|
132
|
-
limit: 50,
|
|
133
|
-
});
|
|
134
|
-
const foundHigh = resultHigh.entities.find((e) => e.id === entityId);
|
|
135
|
-
expect(foundHigh).toBeTruthy();
|
|
136
|
-
// Should NOT find it (0.85 < 0.9)
|
|
137
|
-
const resultVeryHigh = await client.listMemoryEntities({
|
|
138
|
-
workspace_id: getWorkspaceId(),
|
|
139
|
-
project_id: getProjectId(),
|
|
140
|
-
min_confidence: 0.9,
|
|
141
|
-
agent_identifier: "test-runner",
|
|
142
|
-
limit: 50,
|
|
143
|
-
});
|
|
144
|
-
const foundVeryHigh = resultVeryHigh.entities.find((e) => e.id === entityId);
|
|
145
|
-
expect(foundVeryHigh).toBeFalsy();
|
|
146
|
-
});
|
|
147
|
-
test("UPDATE: harmony_update_memory modifies entity", async () => {
|
|
148
|
-
const client = getClient();
|
|
149
|
-
const result = await client.updateMemoryEntity(entityId, {
|
|
150
|
-
title: "[TEST-CRUD] Updated pattern alpha-bravo-42",
|
|
151
|
-
confidence: 0.95,
|
|
152
|
-
tags: ["test", "crud", "updated"],
|
|
153
|
-
metadata: { source: "test-suite", version: 2, updated: true },
|
|
154
|
-
});
|
|
155
|
-
const entity = result.entity;
|
|
156
|
-
expect(entity.title).toBe("[TEST-CRUD] Updated pattern alpha-bravo-42");
|
|
157
|
-
expect(entity.confidence).toBe(0.95);
|
|
158
|
-
expect(entity.tags).toEqual(["test", "crud", "updated"]);
|
|
159
|
-
expect(entity.metadata.version).toBe(2);
|
|
160
|
-
expect(entity.metadata.updated).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
test("UPDATE: updated_at is newer after update", async () => {
|
|
163
|
-
const client = getClient();
|
|
164
|
-
const result = await client.getMemoryEntity(entityId);
|
|
165
|
-
const entity = result.entity;
|
|
166
|
-
const updatedAt = new Date(entity.updated_at).getTime();
|
|
167
|
-
const createdAt = new Date(entity.created_at).getTime();
|
|
168
|
-
expect(updatedAt).toBeGreaterThanOrEqual(createdAt);
|
|
169
|
-
});
|
|
170
|
-
test("DELETE: harmony_forget removes entity", async () => {
|
|
171
|
-
const client = getClient();
|
|
172
|
-
const result = await client.deleteMemoryEntity(entityId);
|
|
173
|
-
expect(result.success).toBe(true);
|
|
174
|
-
// Remove from cleanup list since it's already deleted
|
|
175
|
-
const idx = createdEntityIds.indexOf(entityId);
|
|
176
|
-
if (idx >= 0)
|
|
177
|
-
createdEntityIds.splice(idx, 1);
|
|
178
|
-
// Verify it's gone
|
|
179
|
-
try {
|
|
180
|
-
await client.getMemoryEntity(entityId);
|
|
181
|
-
// If we get here, entity still exists - fail
|
|
182
|
-
expect(true).toBe(false);
|
|
183
|
-
}
|
|
184
|
-
catch (e) {
|
|
185
|
-
// Expected: 404 or similar
|
|
186
|
-
expect(e).toBeTruthy();
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
// ============================================================
|
|
191
|
-
// 2. All Entity Types
|
|
192
|
-
// ============================================================
|
|
193
|
-
describeIf("All 13 Entity Types", () => {
|
|
194
|
-
const allTypes = [
|
|
195
|
-
"agent",
|
|
196
|
-
"task",
|
|
197
|
-
"decision",
|
|
198
|
-
"context",
|
|
199
|
-
"pattern",
|
|
200
|
-
"error",
|
|
201
|
-
"solution",
|
|
202
|
-
"preference",
|
|
203
|
-
"relationship",
|
|
204
|
-
"commitment",
|
|
205
|
-
"lesson",
|
|
206
|
-
"project",
|
|
207
|
-
"handoff",
|
|
208
|
-
];
|
|
209
|
-
for (const type of allTypes) {
|
|
210
|
-
test(`create entity with type="${type}"`, async () => {
|
|
211
|
-
const client = getClient();
|
|
212
|
-
const result = await client.createMemoryEntity({
|
|
213
|
-
workspace_id: getWorkspaceId(),
|
|
214
|
-
project_id: getProjectId(),
|
|
215
|
-
type,
|
|
216
|
-
scope: getScope(),
|
|
217
|
-
title: `[TEST-TYPE] ${type} entity`,
|
|
218
|
-
content: `Testing entity type: ${type}`,
|
|
219
|
-
agent_identifier: "test-runner",
|
|
220
|
-
});
|
|
221
|
-
const entity = result.entity;
|
|
222
|
-
expect(entity.id).toBeTruthy();
|
|
223
|
-
expect(entity.type).toBe(type);
|
|
224
|
-
createdEntityIds.push(entity.id);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
// ============================================================
|
|
229
|
-
// 3. Scope Validation
|
|
230
|
-
// ============================================================
|
|
231
|
-
describeIf("Scope Validation", () => {
|
|
232
|
-
test("project scope requires project_id", async () => {
|
|
233
|
-
const client = getClient();
|
|
234
|
-
const projectId = getProjectId();
|
|
235
|
-
if (projectId) {
|
|
236
|
-
// With project_id, project scope works
|
|
237
|
-
const result = await client.createMemoryEntity({
|
|
238
|
-
workspace_id: getWorkspaceId(),
|
|
239
|
-
project_id: projectId,
|
|
240
|
-
type: "context",
|
|
241
|
-
scope: "project",
|
|
242
|
-
title: "[TEST-SCOPE] Project scoped entity",
|
|
243
|
-
content: "Testing project scope.",
|
|
244
|
-
agent_identifier: "test-runner",
|
|
245
|
-
});
|
|
246
|
-
const entity = result.entity;
|
|
247
|
-
expect(entity.scope).toBe("project");
|
|
248
|
-
createdEntityIds.push(entity.id);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
test("workspace scope works without project_id", async () => {
|
|
252
|
-
const client = getClient();
|
|
253
|
-
const result = await client.createMemoryEntity({
|
|
254
|
-
workspace_id: getWorkspaceId(),
|
|
255
|
-
type: "context",
|
|
256
|
-
scope: "workspace",
|
|
257
|
-
title: "[TEST-SCOPE] Workspace scoped entity",
|
|
258
|
-
content: "Testing workspace scope.",
|
|
259
|
-
agent_identifier: "test-runner",
|
|
260
|
-
});
|
|
261
|
-
const entity = result.entity;
|
|
262
|
-
expect(entity.scope).toBe("workspace");
|
|
263
|
-
createdEntityIds.push(entity.id);
|
|
264
|
-
});
|
|
265
|
-
for (const scope of ["private", "global"]) {
|
|
266
|
-
test(`${scope} scope creates successfully`, async () => {
|
|
267
|
-
const client = getClient();
|
|
268
|
-
const result = await client.createMemoryEntity({
|
|
269
|
-
workspace_id: getWorkspaceId(),
|
|
270
|
-
project_id: getProjectId(),
|
|
271
|
-
type: "context",
|
|
272
|
-
scope,
|
|
273
|
-
title: `[TEST-SCOPE] ${scope} scoped entity`,
|
|
274
|
-
content: `Testing ${scope} scope.`,
|
|
275
|
-
agent_identifier: "test-runner",
|
|
276
|
-
});
|
|
277
|
-
const entity = result.entity;
|
|
278
|
-
expect(entity.scope).toBe(scope);
|
|
279
|
-
createdEntityIds.push(entity.id);
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
// ============================================================
|
|
284
|
-
// 4. Relations CRUD
|
|
285
|
-
// ============================================================
|
|
286
|
-
describeIf("Relation CRUD", () => {
|
|
287
|
-
let sourceId;
|
|
288
|
-
let targetId;
|
|
289
|
-
let relationId;
|
|
290
|
-
test("setup: create two entities", async () => {
|
|
291
|
-
const client = getClient();
|
|
292
|
-
const source = await client.createMemoryEntity({
|
|
293
|
-
workspace_id: getWorkspaceId(),
|
|
294
|
-
project_id: getProjectId(),
|
|
295
|
-
type: "error",
|
|
296
|
-
scope: getScope(),
|
|
297
|
-
title: "[TEST-REL] Source error entity",
|
|
298
|
-
content: "An error for relation testing.",
|
|
299
|
-
agent_identifier: "test-runner",
|
|
300
|
-
});
|
|
301
|
-
sourceId = source.entity.id;
|
|
302
|
-
createdEntityIds.push(sourceId);
|
|
303
|
-
const target = await client.createMemoryEntity({
|
|
304
|
-
workspace_id: getWorkspaceId(),
|
|
305
|
-
project_id: getProjectId(),
|
|
306
|
-
type: "solution",
|
|
307
|
-
scope: getScope(),
|
|
308
|
-
title: "[TEST-REL] Target solution entity",
|
|
309
|
-
content: "A solution for relation testing.",
|
|
310
|
-
agent_identifier: "test-runner",
|
|
311
|
-
});
|
|
312
|
-
targetId = target.entity.id;
|
|
313
|
-
createdEntityIds.push(targetId);
|
|
314
|
-
});
|
|
315
|
-
test("CREATE: harmony_relate creates relation", async () => {
|
|
316
|
-
const client = getClient();
|
|
317
|
-
const result = await client.createMemoryRelation({
|
|
318
|
-
source_id: sourceId,
|
|
319
|
-
target_id: targetId,
|
|
320
|
-
relation_type: "resolved_by",
|
|
321
|
-
confidence: 0.9,
|
|
322
|
-
});
|
|
323
|
-
const rel = result.relation;
|
|
324
|
-
expect(rel.id).toBeTruthy();
|
|
325
|
-
expect(rel.relation_type).toBe("resolved_by");
|
|
326
|
-
relationId = rel.id;
|
|
327
|
-
createdRelationIds.push(relationId);
|
|
328
|
-
});
|
|
329
|
-
test("READ: getRelatedEntities shows the relation", async () => {
|
|
330
|
-
const client = getClient();
|
|
331
|
-
const result = await client.getRelatedEntities(sourceId);
|
|
332
|
-
expect(result.outgoing.length).toBeGreaterThan(0);
|
|
333
|
-
const found = result.outgoing.find((r) => {
|
|
334
|
-
const rel = r;
|
|
335
|
-
const target = rel.target;
|
|
336
|
-
return target?.id === targetId || rel.target_id === targetId;
|
|
337
|
-
});
|
|
338
|
-
expect(found).toBeTruthy();
|
|
339
|
-
});
|
|
340
|
-
test("READ: target entity shows incoming relation", async () => {
|
|
341
|
-
const client = getClient();
|
|
342
|
-
const result = await client.getRelatedEntities(targetId);
|
|
343
|
-
expect(result.incoming.length).toBeGreaterThan(0);
|
|
344
|
-
const found = result.incoming.find((r) => {
|
|
345
|
-
const rel = r;
|
|
346
|
-
const source = rel.source;
|
|
347
|
-
return source?.id === sourceId || rel.source_id === sourceId;
|
|
348
|
-
});
|
|
349
|
-
expect(found).toBeTruthy();
|
|
350
|
-
});
|
|
351
|
-
test("all 8 relation types are valid", async () => {
|
|
352
|
-
const client = getClient();
|
|
353
|
-
// Create a second target for the other relation types
|
|
354
|
-
const target2 = await client.createMemoryEntity({
|
|
355
|
-
workspace_id: getWorkspaceId(),
|
|
356
|
-
project_id: getProjectId(),
|
|
357
|
-
type: "pattern",
|
|
358
|
-
scope: getScope(),
|
|
359
|
-
title: "[TEST-REL] Extra target for relation types",
|
|
360
|
-
content: "Testing all relation types.",
|
|
361
|
-
agent_identifier: "test-runner",
|
|
362
|
-
});
|
|
363
|
-
const target2Id = target2.entity.id;
|
|
364
|
-
createdEntityIds.push(target2Id);
|
|
365
|
-
const types = [
|
|
366
|
-
"learned_from",
|
|
367
|
-
"contradicts",
|
|
368
|
-
"supports",
|
|
369
|
-
"depends_on",
|
|
370
|
-
"part_of",
|
|
371
|
-
"caused_by",
|
|
372
|
-
"relates_to",
|
|
373
|
-
];
|
|
374
|
-
// Run in parallel (semaphore limits to 3 concurrent)
|
|
375
|
-
const _results = await Promise.all(types.map((relType) => client
|
|
376
|
-
.createMemoryRelation({
|
|
377
|
-
source_id: sourceId,
|
|
378
|
-
target_id: target2Id,
|
|
379
|
-
relation_type: relType,
|
|
380
|
-
})
|
|
381
|
-
.then((result) => {
|
|
382
|
-
const rel = result.relation;
|
|
383
|
-
expect(rel.id).toBeTruthy();
|
|
384
|
-
expect(rel.relation_type).toBe(relType);
|
|
385
|
-
createdRelationIds.push(rel.id);
|
|
386
|
-
})));
|
|
387
|
-
}, 15000);
|
|
388
|
-
test("duplicate relation type between same entities is rejected", async () => {
|
|
389
|
-
const client = getClient();
|
|
390
|
-
let threw = false;
|
|
391
|
-
try {
|
|
392
|
-
// Already created resolved_by between sourceId → targetId
|
|
393
|
-
// Use a short timeout since retries would be wasteful here
|
|
394
|
-
await Promise.race([
|
|
395
|
-
client.createMemoryRelation({
|
|
396
|
-
source_id: sourceId,
|
|
397
|
-
target_id: targetId,
|
|
398
|
-
relation_type: "resolved_by",
|
|
399
|
-
}),
|
|
400
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000)),
|
|
401
|
-
]);
|
|
402
|
-
}
|
|
403
|
-
catch (_e) {
|
|
404
|
-
threw = true;
|
|
405
|
-
}
|
|
406
|
-
// Either the API rejected it or we timed out (retry on 409) - both acceptable
|
|
407
|
-
expect(threw).toBe(true);
|
|
408
|
-
});
|
|
409
|
-
test("DELETE: deleteMemoryRelation removes relation", async () => {
|
|
410
|
-
const client = getClient();
|
|
411
|
-
const result = await client.deleteMemoryRelation(relationId);
|
|
412
|
-
expect(result.success).toBe(true);
|
|
413
|
-
// Remove from cleanup
|
|
414
|
-
const idx = createdRelationIds.indexOf(relationId);
|
|
415
|
-
if (idx >= 0)
|
|
416
|
-
createdRelationIds.splice(idx, 1);
|
|
417
|
-
// Verify it's gone from outgoing
|
|
418
|
-
const related = await client.getRelatedEntities(sourceId);
|
|
419
|
-
const found = related.outgoing.find((r) => {
|
|
420
|
-
const rel = r;
|
|
421
|
-
return rel.id === relationId;
|
|
422
|
-
});
|
|
423
|
-
expect(found).toBeFalsy();
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
// ============================================================
|
|
427
|
-
// 5. Search (Full-Text)
|
|
428
|
-
// ============================================================
|
|
429
|
-
describeIf("Full-Text Search", () => {
|
|
430
|
-
let searchEntityId;
|
|
431
|
-
test("setup: create searchable entity with unique term", async () => {
|
|
432
|
-
const client = getClient();
|
|
433
|
-
const result = await client.createMemoryEntity({
|
|
434
|
-
workspace_id: getWorkspaceId(),
|
|
435
|
-
project_id: getProjectId(),
|
|
436
|
-
type: "solution",
|
|
437
|
-
scope: getScope(),
|
|
438
|
-
memory_tier: "reference",
|
|
439
|
-
title: "[TEST-SEARCH] Zebra quuxplonk algorithm",
|
|
440
|
-
content: "A unique solution describing the zebra quuxplonk algorithm for distributed systems.",
|
|
441
|
-
confidence: 0.9,
|
|
442
|
-
tags: ["test", "search", "algorithm"],
|
|
443
|
-
agent_identifier: "test-runner",
|
|
444
|
-
});
|
|
445
|
-
searchEntityId = result.entity.id;
|
|
446
|
-
createdEntityIds.push(searchEntityId);
|
|
447
|
-
});
|
|
448
|
-
test("harmony_memory_search finds entity by unique term", async () => {
|
|
449
|
-
const client = getClient();
|
|
450
|
-
const result = await client.searchMemoryEntities(getWorkspaceId(), "quuxplonk", { project_id: getProjectId(), limit: 10 });
|
|
451
|
-
expect(result.entities.length).toBeGreaterThan(0);
|
|
452
|
-
const found = result.entities.find((e) => e.id === searchEntityId);
|
|
453
|
-
expect(found).toBeTruthy();
|
|
454
|
-
});
|
|
455
|
-
test("search with type filter narrows results", async () => {
|
|
456
|
-
const client = getClient();
|
|
457
|
-
// Should find it (type=solution)
|
|
458
|
-
const resultSolution = await client.searchMemoryEntities(getWorkspaceId(), "quuxplonk", { project_id: getProjectId(), type: "solution", limit: 10 });
|
|
459
|
-
const found = resultSolution.entities.find((e) => e.id === searchEntityId);
|
|
460
|
-
expect(found).toBeTruthy();
|
|
461
|
-
// Should NOT find it (type=error)
|
|
462
|
-
const resultError = await client.searchMemoryEntities(getWorkspaceId(), "quuxplonk", { project_id: getProjectId(), type: "error", limit: 10 });
|
|
463
|
-
const notFound = resultError.entities.find((e) => e.id === searchEntityId);
|
|
464
|
-
expect(notFound).toBeFalsy();
|
|
465
|
-
});
|
|
466
|
-
test("search with no results returns empty array", async () => {
|
|
467
|
-
const client = getClient();
|
|
468
|
-
const result = await client.searchMemoryEntities(getWorkspaceId(), "xyznonexistenttermthatnobodywouldevercreate999", { project_id: getProjectId(), limit: 10 });
|
|
469
|
-
expect(result.entities).toHaveLength(0);
|
|
470
|
-
});
|
|
471
|
-
test("markdown search returns text content", async () => {
|
|
472
|
-
const client = getClient();
|
|
473
|
-
const markdown = await client.searchMemoryEntitiesMarkdown(getWorkspaceId(), "quuxplonk", { project_id: getProjectId(), limit: 10 });
|
|
474
|
-
expect(typeof markdown).toBe("string");
|
|
475
|
-
expect(markdown.length).toBeGreaterThan(0);
|
|
476
|
-
expect(markdown).toContain("quuxplonk");
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
// ============================================================
|
|
480
|
-
// 6. Vault Index
|
|
481
|
-
// ============================================================
|
|
482
|
-
describeIf("Vault Index", () => {
|
|
483
|
-
test("vault index returns entities", async () => {
|
|
484
|
-
const client = getClient();
|
|
485
|
-
let result;
|
|
486
|
-
try {
|
|
487
|
-
result = await client.getVaultIndex({
|
|
488
|
-
workspace_id: getWorkspaceId(),
|
|
489
|
-
project_id: getProjectId(),
|
|
490
|
-
limit: 10,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
catch (e) {
|
|
494
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
495
|
-
if (msg.includes("Unknown endpoint")) {
|
|
496
|
-
console.log("Skipping: vault index endpoint not deployed");
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
throw e;
|
|
500
|
-
}
|
|
501
|
-
expect(result.entities).toBeDefined();
|
|
502
|
-
expect(Array.isArray(result.entities)).toBe(true);
|
|
503
|
-
});
|
|
504
|
-
test("vault index with type filter returns only matching type", async () => {
|
|
505
|
-
const client = getClient();
|
|
506
|
-
// First create a known entity
|
|
507
|
-
const createResult = await client.createMemoryEntity({
|
|
508
|
-
workspace_id: getWorkspaceId(),
|
|
509
|
-
project_id: getProjectId(),
|
|
510
|
-
type: "preference",
|
|
511
|
-
scope: getScope(),
|
|
512
|
-
title: "[TEST-INDEX] Preference for vault index test",
|
|
513
|
-
content: "Testing vault index filtering.",
|
|
514
|
-
agent_identifier: "test-runner",
|
|
515
|
-
});
|
|
516
|
-
createdEntityIds.push(createResult.entity.id);
|
|
517
|
-
let result;
|
|
518
|
-
try {
|
|
519
|
-
result = await client.getVaultIndex({
|
|
520
|
-
workspace_id: getWorkspaceId(),
|
|
521
|
-
project_id: getProjectId(),
|
|
522
|
-
type: "preference",
|
|
523
|
-
limit: 50,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
catch (e) {
|
|
527
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
528
|
-
if (msg.includes("Unknown endpoint"))
|
|
529
|
-
return;
|
|
530
|
-
throw e;
|
|
531
|
-
}
|
|
532
|
-
// All returned entities should be of type 'preference'
|
|
533
|
-
for (const entity of result.entities) {
|
|
534
|
-
expect(entity.type).toBe("preference");
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
test("vault index markdown returns table format", async () => {
|
|
538
|
-
const client = getClient();
|
|
539
|
-
try {
|
|
540
|
-
const markdown = await client.getVaultIndexMarkdown({
|
|
541
|
-
workspace_id: getWorkspaceId(),
|
|
542
|
-
project_id: getProjectId(),
|
|
543
|
-
limit: 10,
|
|
544
|
-
});
|
|
545
|
-
expect(typeof markdown).toBe("string");
|
|
546
|
-
// Markdown table should contain pipe separators
|
|
547
|
-
if (markdown.length > 0) {
|
|
548
|
-
expect(markdown).toContain("|");
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
catch (e) {
|
|
552
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
553
|
-
if (msg.includes("Unknown endpoint"))
|
|
554
|
-
return;
|
|
555
|
-
throw e;
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
// ============================================================
|
|
560
|
-
// 7. Wiki-Link Resolution
|
|
561
|
-
// ============================================================
|
|
562
|
-
describeIf("Wiki-Link Resolution", () => {
|
|
563
|
-
test("resolveLinks processes entities with [[wiki-links]]", async () => {
|
|
564
|
-
const client = getClient();
|
|
565
|
-
// Create an entity with a wiki-link in its content
|
|
566
|
-
const targetResult = await client.createMemoryEntity({
|
|
567
|
-
workspace_id: getWorkspaceId(),
|
|
568
|
-
project_id: getProjectId(),
|
|
569
|
-
type: "pattern",
|
|
570
|
-
scope: getScope(),
|
|
571
|
-
title: "[TEST-WIKI] Target pattern for linking",
|
|
572
|
-
content: "This is the target entity.",
|
|
573
|
-
agent_identifier: "test-runner",
|
|
574
|
-
});
|
|
575
|
-
const targetId = targetResult.entity
|
|
576
|
-
.id;
|
|
577
|
-
createdEntityIds.push(targetId);
|
|
578
|
-
const sourceResult = await client.createMemoryEntity({
|
|
579
|
-
workspace_id: getWorkspaceId(),
|
|
580
|
-
project_id: getProjectId(),
|
|
581
|
-
type: "lesson",
|
|
582
|
-
scope: getScope(),
|
|
583
|
-
title: "[TEST-WIKI] Source with wiki-link",
|
|
584
|
-
content: "Learned from [[[TEST-WIKI] Target pattern for linking]] that patterns are useful.",
|
|
585
|
-
agent_identifier: "test-runner",
|
|
586
|
-
});
|
|
587
|
-
const sourceId = sourceResult.entity
|
|
588
|
-
.id;
|
|
589
|
-
createdEntityIds.push(sourceId);
|
|
590
|
-
// Resolve links
|
|
591
|
-
try {
|
|
592
|
-
const result = await client.resolveLinks({
|
|
593
|
-
workspace_id: getWorkspaceId(),
|
|
594
|
-
project_id: getProjectId(),
|
|
595
|
-
});
|
|
596
|
-
expect(result).toBeTruthy();
|
|
597
|
-
// The result should report resolved/unresolved counts
|
|
598
|
-
expect(typeof result.resolved).toBe("number");
|
|
599
|
-
expect(typeof result.unresolved).toBe("number");
|
|
600
|
-
}
|
|
601
|
-
catch (e) {
|
|
602
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
603
|
-
if (msg.includes("Unknown endpoint")) {
|
|
604
|
-
console.log("Skipping: resolve-links endpoint not deployed");
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
throw e;
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
// ============================================================
|
|
612
|
-
// 8. Touch (Access Tracking)
|
|
613
|
-
// ============================================================
|
|
614
|
-
describeIf("Access Tracking", () => {
|
|
615
|
-
test("touchMemoryEntity increments access count", async () => {
|
|
616
|
-
const client = getClient();
|
|
617
|
-
// Create entity
|
|
618
|
-
const result = await client.createMemoryEntity({
|
|
619
|
-
workspace_id: getWorkspaceId(),
|
|
620
|
-
project_id: getProjectId(),
|
|
621
|
-
type: "context",
|
|
622
|
-
scope: getScope(),
|
|
623
|
-
title: "[TEST-TOUCH] Access tracking test",
|
|
624
|
-
content: "Testing access count increment.",
|
|
625
|
-
agent_identifier: "test-runner",
|
|
626
|
-
});
|
|
627
|
-
const entityId = result.entity.id;
|
|
628
|
-
createdEntityIds.push(entityId);
|
|
629
|
-
// Touch it (endpoint may not be deployed)
|
|
630
|
-
try {
|
|
631
|
-
const touchResult = await client.touchMemoryEntity(entityId);
|
|
632
|
-
expect(touchResult.success).toBe(true);
|
|
633
|
-
// Verify access_count increased
|
|
634
|
-
const readResult = await client.getMemoryEntity(entityId);
|
|
635
|
-
const entity = readResult.entity;
|
|
636
|
-
expect(entity.access_count).toBeGreaterThanOrEqual(1);
|
|
637
|
-
}
|
|
638
|
-
catch (e) {
|
|
639
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
640
|
-
if (msg.includes("Unknown endpoint")) {
|
|
641
|
-
console.log("Skipping: touch endpoint not deployed");
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
throw e;
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
// ============================================================
|
|
649
|
-
// 9. Edge Cases
|
|
650
|
-
// ============================================================
|
|
651
|
-
describeIf("Edge Cases", () => {
|
|
652
|
-
test("entity with minimal fields (just required)", async () => {
|
|
653
|
-
const client = getClient();
|
|
654
|
-
const result = await client.createMemoryEntity({
|
|
655
|
-
workspace_id: getWorkspaceId(),
|
|
656
|
-
type: "context",
|
|
657
|
-
scope: "workspace",
|
|
658
|
-
title: "[TEST-EDGE] Minimal entity",
|
|
659
|
-
content: "Minimal.",
|
|
660
|
-
agent_identifier: "test-runner",
|
|
661
|
-
});
|
|
662
|
-
const entity = result.entity;
|
|
663
|
-
expect(entity.id).toBeTruthy();
|
|
664
|
-
expect(entity.confidence).toBe(1.0); // Default
|
|
665
|
-
expect(entity.tags).toEqual([]); // Default
|
|
666
|
-
createdEntityIds.push(entity.id);
|
|
667
|
-
});
|
|
668
|
-
test("entity with empty tags array", async () => {
|
|
669
|
-
const client = getClient();
|
|
670
|
-
const result = await client.createMemoryEntity({
|
|
671
|
-
workspace_id: getWorkspaceId(),
|
|
672
|
-
type: "context",
|
|
673
|
-
scope: "workspace",
|
|
674
|
-
title: "[TEST-EDGE] Empty tags",
|
|
675
|
-
content: "No tags.",
|
|
676
|
-
tags: [],
|
|
677
|
-
agent_identifier: "test-runner",
|
|
678
|
-
});
|
|
679
|
-
const entity = result.entity;
|
|
680
|
-
expect(entity.tags).toEqual([]);
|
|
681
|
-
createdEntityIds.push(entity.id);
|
|
682
|
-
});
|
|
683
|
-
test("entity with confidence at boundaries (0.0 and 1.0)", async () => {
|
|
684
|
-
const client = getClient();
|
|
685
|
-
const low = await client.createMemoryEntity({
|
|
686
|
-
workspace_id: getWorkspaceId(),
|
|
687
|
-
type: "context",
|
|
688
|
-
scope: "workspace",
|
|
689
|
-
title: "[TEST-EDGE] Zero confidence",
|
|
690
|
-
content: "Low confidence.",
|
|
691
|
-
confidence: 0.0,
|
|
692
|
-
agent_identifier: "test-runner",
|
|
693
|
-
});
|
|
694
|
-
expect(low.entity.confidence).toBe(0);
|
|
695
|
-
createdEntityIds.push(low.entity.id);
|
|
696
|
-
const high = await client.createMemoryEntity({
|
|
697
|
-
workspace_id: getWorkspaceId(),
|
|
698
|
-
type: "context",
|
|
699
|
-
scope: "workspace",
|
|
700
|
-
title: "[TEST-EDGE] Max confidence",
|
|
701
|
-
content: "High confidence.",
|
|
702
|
-
confidence: 1.0,
|
|
703
|
-
agent_identifier: "test-runner",
|
|
704
|
-
});
|
|
705
|
-
expect(high.entity.confidence).toBe(1.0);
|
|
706
|
-
createdEntityIds.push(high.entity.id);
|
|
707
|
-
});
|
|
708
|
-
test("deleting entity cascades relations", async () => {
|
|
709
|
-
const client = getClient();
|
|
710
|
-
// Create two entities with a relation
|
|
711
|
-
const a = await client.createMemoryEntity({
|
|
712
|
-
workspace_id: getWorkspaceId(),
|
|
713
|
-
project_id: getProjectId(),
|
|
714
|
-
type: "error",
|
|
715
|
-
scope: getScope(),
|
|
716
|
-
title: "[TEST-CASCADE] Entity A",
|
|
717
|
-
content: "Will be deleted.",
|
|
718
|
-
agent_identifier: "test-runner",
|
|
719
|
-
});
|
|
720
|
-
const aId = a.entity.id;
|
|
721
|
-
const b = await client.createMemoryEntity({
|
|
722
|
-
workspace_id: getWorkspaceId(),
|
|
723
|
-
project_id: getProjectId(),
|
|
724
|
-
type: "solution",
|
|
725
|
-
scope: getScope(),
|
|
726
|
-
title: "[TEST-CASCADE] Entity B",
|
|
727
|
-
content: "Target of relation.",
|
|
728
|
-
agent_identifier: "test-runner",
|
|
729
|
-
});
|
|
730
|
-
const bId = b.entity.id;
|
|
731
|
-
createdEntityIds.push(bId);
|
|
732
|
-
const _rel = await client.createMemoryRelation({
|
|
733
|
-
source_id: aId,
|
|
734
|
-
target_id: bId,
|
|
735
|
-
relation_type: "resolved_by",
|
|
736
|
-
});
|
|
737
|
-
// Delete entity A - relation should cascade
|
|
738
|
-
await client.deleteMemoryEntity(aId);
|
|
739
|
-
// Verify entity B has no incoming relations from A
|
|
740
|
-
const related = await client.getRelatedEntities(bId);
|
|
741
|
-
const found = related.incoming.find((r) => {
|
|
742
|
-
const item = r;
|
|
743
|
-
const source = item.source;
|
|
744
|
-
return source?.id === aId || item.source_id === aId;
|
|
745
|
-
});
|
|
746
|
-
expect(found).toBeFalsy();
|
|
747
|
-
});
|
|
748
|
-
test("update non-existent entity returns error", async () => {
|
|
749
|
-
const client = getClient();
|
|
750
|
-
const fakeId = "00000000-0000-0000-0000-000000000000";
|
|
751
|
-
try {
|
|
752
|
-
await client.updateMemoryEntity(fakeId, { title: "Should fail" });
|
|
753
|
-
expect(true).toBe(false);
|
|
754
|
-
}
|
|
755
|
-
catch (e) {
|
|
756
|
-
expect(e).toBeTruthy();
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
test("delete non-existent entity returns error", async () => {
|
|
760
|
-
const client = getClient();
|
|
761
|
-
const fakeId = "00000000-0000-0000-0000-000000000000";
|
|
762
|
-
try {
|
|
763
|
-
await client.deleteMemoryEntity(fakeId);
|
|
764
|
-
expect(true).toBe(false);
|
|
765
|
-
}
|
|
766
|
-
catch (e) {
|
|
767
|
-
expect(e).toBeTruthy();
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
test("relation with non-existent source entity fails", async () => {
|
|
771
|
-
const client = getClient();
|
|
772
|
-
const fakeId = "00000000-0000-0000-0000-000000000000";
|
|
773
|
-
// Create a valid target
|
|
774
|
-
const target = await client.createMemoryEntity({
|
|
775
|
-
workspace_id: getWorkspaceId(),
|
|
776
|
-
project_id: getProjectId(),
|
|
777
|
-
type: "context",
|
|
778
|
-
scope: getScope(),
|
|
779
|
-
title: "[TEST-EDGE] Valid target for bad relation",
|
|
780
|
-
content: "Target.",
|
|
781
|
-
agent_identifier: "test-runner",
|
|
782
|
-
});
|
|
783
|
-
const targetId = target.entity.id;
|
|
784
|
-
createdEntityIds.push(targetId);
|
|
785
|
-
try {
|
|
786
|
-
await client.createMemoryRelation({
|
|
787
|
-
source_id: fakeId,
|
|
788
|
-
target_id: targetId,
|
|
789
|
-
relation_type: "relates_to",
|
|
790
|
-
});
|
|
791
|
-
expect(true).toBe(false);
|
|
792
|
-
}
|
|
793
|
-
catch (e) {
|
|
794
|
-
expect(e).toBeTruthy();
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
});
|