@aeriondyseti/vector-memory-mcp 2.3.0 → 2.4.4
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/package.json +6 -6
- package/server/core/connection.ts +1 -1
- package/server/core/conversation.repository.ts +91 -2
- package/server/core/conversation.service.ts +19 -19
- package/server/core/conversation.ts +2 -5
- package/server/core/embeddings.service.ts +108 -17
- package/server/core/memory.repository.ts +35 -9
- package/server/core/memory.service.ts +37 -36
- package/server/core/migration.service.ts +3 -3
- package/server/core/migrations.ts +60 -20
- package/server/core/parsers/claude-code.parser.ts +3 -3
- package/server/core/parsers/types.ts +1 -1
- package/server/core/sqlite-utils.ts +22 -0
- package/server/index.ts +13 -15
- package/server/transports/http/mcp-transport.ts +5 -5
- package/server/transports/http/server.ts +18 -6
- package/server/transports/mcp/handlers.ts +47 -23
- package/server/transports/mcp/server.ts +5 -5
- package/scripts/lancedb-extract.ts +0 -181
- package/scripts/smoke-test.ts +0 -699
- package/scripts/sync-version.ts +0 -35
- package/scripts/test-runner.ts +0 -76
- package/scripts/warmup.ts +0 -72
package/scripts/smoke-test.ts
DELETED
|
@@ -1,699 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Smoke test script for vector-memory-mcp 2.0
|
|
5
|
-
*
|
|
6
|
-
* Spawns a real server process and exercises the full HTTP API surface:
|
|
7
|
-
* - Server startup & lockfile discovery
|
|
8
|
-
* - Memory lifecycle (store/search/get/delete)
|
|
9
|
-
* - Waypoint lifecycle (set/get via MCP + HTTP)
|
|
10
|
-
* - Conversation history indexing
|
|
11
|
-
* - Memory usefulness voting
|
|
12
|
-
* - Lockfile cleanup on shutdown
|
|
13
|
-
* - Migration detection & subcommand
|
|
14
|
-
*
|
|
15
|
-
* Usage: bun run scripts/smoke-test.ts
|
|
16
|
-
* Exit code: 0 on success, 1 on any failure
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { spawn, type Subprocess } from "bun";
|
|
20
|
-
import {
|
|
21
|
-
mkdtempSync,
|
|
22
|
-
rmSync,
|
|
23
|
-
mkdirSync,
|
|
24
|
-
writeFileSync,
|
|
25
|
-
existsSync,
|
|
26
|
-
readFileSync,
|
|
27
|
-
} from "fs";
|
|
28
|
-
import { join } from "path";
|
|
29
|
-
import { tmpdir } from "os";
|
|
30
|
-
|
|
31
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
const SERVER_PATH = join(import.meta.dir, "../server/index.ts");
|
|
34
|
-
|
|
35
|
-
let passed = 0;
|
|
36
|
-
let failed = 0;
|
|
37
|
-
const failures: string[] = [];
|
|
38
|
-
|
|
39
|
-
function assert(condition: boolean, description: string): void {
|
|
40
|
-
if (condition) {
|
|
41
|
-
passed++;
|
|
42
|
-
console.log(` ✅ ${description}`);
|
|
43
|
-
} else {
|
|
44
|
-
failed++;
|
|
45
|
-
failures.push(description);
|
|
46
|
-
console.log(` ❌ ${description}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function assertContains(text: string, substring: string, description: string): void {
|
|
51
|
-
assert(text.includes(substring), description);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function assertNotContains(text: string, substring: string, description: string): void {
|
|
55
|
-
assert(!text.includes(substring), description);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface JsonRpcResponse {
|
|
59
|
-
jsonrpc: "2.0";
|
|
60
|
-
id: number;
|
|
61
|
-
result?: { content: { type: string; text: string }[] };
|
|
62
|
-
error?: { code: number; message: string };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function httpGet(baseUrl: string, path: string): Promise<Response> {
|
|
66
|
-
return fetch(`${baseUrl}${path}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function httpPost(
|
|
70
|
-
baseUrl: string,
|
|
71
|
-
path: string,
|
|
72
|
-
body: Record<string, unknown>
|
|
73
|
-
): Promise<Response> {
|
|
74
|
-
return fetch(`${baseUrl}${path}`, {
|
|
75
|
-
method: "POST",
|
|
76
|
-
headers: { "Content-Type": "application/json" },
|
|
77
|
-
body: JSON.stringify(body),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function httpDelete(baseUrl: string, path: string): Promise<Response> {
|
|
82
|
-
return fetch(`${baseUrl}${path}`, { method: "DELETE" });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Initialize an MCP session over HTTP and return the session ID.
|
|
87
|
-
*/
|
|
88
|
-
async function initMcpSession(baseUrl: string): Promise<string> {
|
|
89
|
-
const res = await fetch(`${baseUrl}/mcp`, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
headers: { "Content-Type": "application/json" },
|
|
92
|
-
body: JSON.stringify({
|
|
93
|
-
jsonrpc: "2.0",
|
|
94
|
-
id: 0,
|
|
95
|
-
method: "initialize",
|
|
96
|
-
params: {
|
|
97
|
-
protocolVersion: "2024-11-05",
|
|
98
|
-
capabilities: {},
|
|
99
|
-
clientInfo: { name: "smoke-test", version: "1.0" },
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const sessionId = res.headers.get("mcp-session-id");
|
|
105
|
-
if (!sessionId) {
|
|
106
|
-
throw new Error("No mcp-session-id header in initialize response");
|
|
107
|
-
}
|
|
108
|
-
return sessionId;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let mcpRequestId = 100;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Call an MCP tool via HTTP JSON-RPC and return the text result.
|
|
115
|
-
*/
|
|
116
|
-
async function mcpCall(
|
|
117
|
-
baseUrl: string,
|
|
118
|
-
sessionId: string,
|
|
119
|
-
toolName: string,
|
|
120
|
-
args: Record<string, unknown>
|
|
121
|
-
): Promise<string> {
|
|
122
|
-
const id = mcpRequestId++;
|
|
123
|
-
const res = await fetch(`${baseUrl}/mcp`, {
|
|
124
|
-
method: "POST",
|
|
125
|
-
headers: {
|
|
126
|
-
"Content-Type": "application/json",
|
|
127
|
-
"mcp-session-id": sessionId,
|
|
128
|
-
},
|
|
129
|
-
body: JSON.stringify({
|
|
130
|
-
jsonrpc: "2.0",
|
|
131
|
-
id,
|
|
132
|
-
method: "tools/call",
|
|
133
|
-
params: { name: toolName, arguments: args },
|
|
134
|
-
}),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const response = (await res.json()) as JsonRpcResponse;
|
|
138
|
-
|
|
139
|
-
if (response.error) {
|
|
140
|
-
return `ERROR: ${response.error.message}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return response.result?.content[0]?.text ?? "";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function extractMemoryId(text: string): string {
|
|
147
|
-
const match = text.match(/ID: ([a-f0-9-]+)/i);
|
|
148
|
-
if (!match) throw new Error(`Could not extract ID from: ${text}`);
|
|
149
|
-
return match[1];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Wait for a lockfile to appear, returning its parsed contents.
|
|
154
|
-
*/
|
|
155
|
-
async function waitForLockfile(
|
|
156
|
-
lockfilePath: string,
|
|
157
|
-
timeoutMs: number = 30000
|
|
158
|
-
): Promise<{ port: number; pid: number }> {
|
|
159
|
-
const start = Date.now();
|
|
160
|
-
while (Date.now() - start < timeoutMs) {
|
|
161
|
-
if (existsSync(lockfilePath)) {
|
|
162
|
-
try {
|
|
163
|
-
const content = readFileSync(lockfilePath, "utf-8");
|
|
164
|
-
return JSON.parse(content);
|
|
165
|
-
} catch {
|
|
166
|
-
// File may be partially written, retry
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
170
|
-
}
|
|
171
|
-
throw new Error(`Lockfile not found at ${lockfilePath} after ${timeoutMs}ms`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Wait for server health endpoint to respond.
|
|
176
|
-
*/
|
|
177
|
-
async function waitForHealth(baseUrl: string, timeoutMs: number = 30000): Promise<void> {
|
|
178
|
-
const start = Date.now();
|
|
179
|
-
while (Date.now() - start < timeoutMs) {
|
|
180
|
-
try {
|
|
181
|
-
const res = await fetch(`${baseUrl}/health`);
|
|
182
|
-
if (res.ok) return;
|
|
183
|
-
} catch {
|
|
184
|
-
// Server not ready yet
|
|
185
|
-
}
|
|
186
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
187
|
-
}
|
|
188
|
-
throw new Error(`Server health check failed after ${timeoutMs}ms`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Collect stderr output from a subprocess.
|
|
193
|
-
*/
|
|
194
|
-
async function collectStderr(
|
|
195
|
-
proc: Subprocess,
|
|
196
|
-
timeoutMs: number = 10000
|
|
197
|
-
): Promise<string> {
|
|
198
|
-
const reader = (proc.stderr as ReadableStream).getReader();
|
|
199
|
-
const decoder = new TextDecoder();
|
|
200
|
-
let output = "";
|
|
201
|
-
const start = Date.now();
|
|
202
|
-
|
|
203
|
-
while (Date.now() - start < timeoutMs) {
|
|
204
|
-
const result = await Promise.race([
|
|
205
|
-
reader.read(),
|
|
206
|
-
new Promise<{ done: true; value: undefined }>((r) =>
|
|
207
|
-
setTimeout(() => r({ done: true, value: undefined }), timeoutMs - (Date.now() - start))
|
|
208
|
-
),
|
|
209
|
-
]);
|
|
210
|
-
|
|
211
|
-
if (result.done) break;
|
|
212
|
-
if (result.value) output += decoder.decode(result.value);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
reader.releaseLock();
|
|
216
|
-
return output;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ── Main Test Sections ──────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
async function main(): Promise<void> {
|
|
222
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "vector-memory-smoke-"));
|
|
223
|
-
const dbPath = join(tmpDir, "smoke-test.db");
|
|
224
|
-
const sessionsPath = join(tmpDir, "sessions");
|
|
225
|
-
const lockfilePath = join(tmpDir, ".vector-memory", "server.lock");
|
|
226
|
-
|
|
227
|
-
console.log(`\n🔬 vector-memory-mcp 2.0 Smoke Test`);
|
|
228
|
-
console.log(` Temp dir: ${tmpDir}\n`);
|
|
229
|
-
|
|
230
|
-
// Pick a random high port to avoid conflicts with running servers
|
|
231
|
-
const smokePort = 30000 + Math.floor(Math.random() * 30000);
|
|
232
|
-
|
|
233
|
-
let proc: Subprocess | null = null;
|
|
234
|
-
let baseUrl = "";
|
|
235
|
-
let sessionId = "";
|
|
236
|
-
let storedMemoryId = "";
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
// ────────────────────────────────────────────────────────────────
|
|
240
|
-
// Section 1: Server Startup & Health
|
|
241
|
-
// ────────────────────────────────────────────────────────────────
|
|
242
|
-
console.log("§1 Server Startup & Health");
|
|
243
|
-
|
|
244
|
-
proc = spawn(
|
|
245
|
-
[
|
|
246
|
-
"bun", "run", SERVER_PATH,
|
|
247
|
-
"--db-file", dbPath,
|
|
248
|
-
"--port", String(smokePort),
|
|
249
|
-
"--enable-history",
|
|
250
|
-
"--history-path", sessionsPath,
|
|
251
|
-
],
|
|
252
|
-
{
|
|
253
|
-
stdin: "pipe",
|
|
254
|
-
stdout: "pipe",
|
|
255
|
-
stderr: "pipe",
|
|
256
|
-
cwd: tmpDir,
|
|
257
|
-
}
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
// Wait for lockfile
|
|
261
|
-
const lockData = await waitForLockfile(lockfilePath);
|
|
262
|
-
assert(lockData.port > 0, "Lockfile contains valid port");
|
|
263
|
-
assert(lockData.pid === proc.pid, "Lockfile PID matches spawned process");
|
|
264
|
-
|
|
265
|
-
baseUrl = `http://127.0.0.1:${lockData.port}`;
|
|
266
|
-
await waitForHealth(baseUrl);
|
|
267
|
-
|
|
268
|
-
// Health check
|
|
269
|
-
const healthRes = await httpGet(baseUrl, "/health");
|
|
270
|
-
const health = await healthRes.json() as any;
|
|
271
|
-
|
|
272
|
-
assert(health.status === "ok", "GET /health returns status: ok");
|
|
273
|
-
assert(health.config.historyEnabled === true, "Health reports historyEnabled: true");
|
|
274
|
-
assertContains(health.config.dbPath, "smoke-test.db", "Health reports correct dbPath");
|
|
275
|
-
|
|
276
|
-
// ────────────────────────────────────────────────────────────────
|
|
277
|
-
// Section 2: Memory Lifecycle (HTTP)
|
|
278
|
-
// ────────────────────────────────────────────────────────────────
|
|
279
|
-
console.log("\n§2 Memory Lifecycle (HTTP)");
|
|
280
|
-
|
|
281
|
-
// Initialize MCP session for tool calls
|
|
282
|
-
sessionId = await initMcpSession(baseUrl);
|
|
283
|
-
assert(sessionId.length > 0, "MCP session initialized with session ID");
|
|
284
|
-
|
|
285
|
-
// Store via HTTP endpoint
|
|
286
|
-
const storeRes = await httpPost(baseUrl, "/store", {
|
|
287
|
-
content: "The velocity of an unladen swallow is approximately 11 m/s",
|
|
288
|
-
metadata: { category: "ornithology", source: "smoke-test" },
|
|
289
|
-
});
|
|
290
|
-
const storeBody = await storeRes.json() as any;
|
|
291
|
-
assert(storeBody.id !== undefined, "POST /store returns memory ID");
|
|
292
|
-
storedMemoryId = storeBody.id;
|
|
293
|
-
|
|
294
|
-
// Search via HTTP
|
|
295
|
-
const searchRes = await httpPost(baseUrl, "/search", {
|
|
296
|
-
query: "unladen swallow velocity",
|
|
297
|
-
intent: "fact_check",
|
|
298
|
-
});
|
|
299
|
-
const searchBody = await searchRes.json() as any;
|
|
300
|
-
assert(searchBody.count > 0, "POST /search finds stored memory");
|
|
301
|
-
assert(
|
|
302
|
-
searchBody.results.some((r: any) => r.id === storedMemoryId),
|
|
303
|
-
"Search results contain the stored memory ID"
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
// Get via HTTP
|
|
307
|
-
const getRes = await httpGet(baseUrl, `/memories/${storedMemoryId}`);
|
|
308
|
-
const getBody = await getRes.json() as any;
|
|
309
|
-
assert(getBody.id === storedMemoryId, "GET /memories/:id returns correct memory");
|
|
310
|
-
assertContains(
|
|
311
|
-
getBody.content,
|
|
312
|
-
"unladen swallow",
|
|
313
|
-
"GET /memories/:id content matches"
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// Delete via HTTP
|
|
317
|
-
const deleteRes = await httpDelete(baseUrl, `/memories/${storedMemoryId}`);
|
|
318
|
-
const deleteBody = await deleteRes.json() as any;
|
|
319
|
-
assert(deleteBody.deleted === true, "DELETE /memories/:id returns deleted: true");
|
|
320
|
-
|
|
321
|
-
// Search should NOT find deleted memory
|
|
322
|
-
const searchAfterDelete = await httpPost(baseUrl, "/search", {
|
|
323
|
-
query: "unladen swallow velocity",
|
|
324
|
-
intent: "fact_check",
|
|
325
|
-
});
|
|
326
|
-
const afterDeleteBody = await searchAfterDelete.json() as any;
|
|
327
|
-
const foundDeleted = afterDeleteBody.results.some(
|
|
328
|
-
(r: any) => r.id === storedMemoryId
|
|
329
|
-
);
|
|
330
|
-
assert(!foundDeleted, "Deleted memory not found in regular search");
|
|
331
|
-
|
|
332
|
-
// Search with include_deleted via MCP
|
|
333
|
-
const searchWithDeleted = await mcpCall(baseUrl, sessionId, "search_memories", {
|
|
334
|
-
query: "unladen swallow velocity",
|
|
335
|
-
intent: "fact_check",
|
|
336
|
-
reason_for_search: "smoke test: verify soft-delete recovery",
|
|
337
|
-
include_deleted: true,
|
|
338
|
-
});
|
|
339
|
-
assertContains(
|
|
340
|
-
searchWithDeleted,
|
|
341
|
-
storedMemoryId,
|
|
342
|
-
"Deleted memory found with include_deleted via MCP"
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// ────────────────────────────────────────────────────────────────
|
|
346
|
-
// Section 3: Waypoint Lifecycle
|
|
347
|
-
// ────────────────────────────────────────────────────────────────
|
|
348
|
-
console.log("\n§3 Waypoint Lifecycle");
|
|
349
|
-
|
|
350
|
-
// Store a memory to reference from the waypoint
|
|
351
|
-
const wpStoreResult = await mcpCall(baseUrl, sessionId, "store_memories", {
|
|
352
|
-
memories: [
|
|
353
|
-
{
|
|
354
|
-
content: "Waypoint reference: migration to sqlite-vec is complete",
|
|
355
|
-
metadata: { phase: "migration" },
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
});
|
|
359
|
-
const wpMemoryId = extractMemoryId(wpStoreResult);
|
|
360
|
-
assert(wpMemoryId.length > 0, "Stored waypoint reference memory");
|
|
361
|
-
|
|
362
|
-
// Set waypoint via MCP
|
|
363
|
-
const setWpResult = await mcpCall(baseUrl, sessionId, "set_waypoint", {
|
|
364
|
-
project: "smoke-test",
|
|
365
|
-
branch: "main",
|
|
366
|
-
summary: "Completed sqlite-vec migration smoke test",
|
|
367
|
-
next_steps: ["verify retrieval", "run benchmarks"],
|
|
368
|
-
memory_ids: [wpMemoryId],
|
|
369
|
-
});
|
|
370
|
-
assertContains(setWpResult, "Waypoint", "set_waypoint returns confirmation");
|
|
371
|
-
|
|
372
|
-
// Get waypoint via MCP
|
|
373
|
-
const getWpResult = await mcpCall(baseUrl, sessionId, "get_waypoint", {});
|
|
374
|
-
assertContains(
|
|
375
|
-
getWpResult,
|
|
376
|
-
"sqlite-vec migration",
|
|
377
|
-
"get_waypoint returns summary"
|
|
378
|
-
);
|
|
379
|
-
assertContains(
|
|
380
|
-
getWpResult,
|
|
381
|
-
"verify retrieval",
|
|
382
|
-
"get_waypoint returns next_steps"
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
// Get waypoint via HTTP
|
|
386
|
-
const wpHttpRes = await httpGet(baseUrl, "/waypoint");
|
|
387
|
-
const wpHttpBody = await wpHttpRes.json() as any;
|
|
388
|
-
assert(wpHttpRes.status === 200, "GET /waypoint returns 200");
|
|
389
|
-
assert(
|
|
390
|
-
wpHttpBody.referencedMemories?.length > 0,
|
|
391
|
-
"GET /waypoint includes referencedMemories"
|
|
392
|
-
);
|
|
393
|
-
assertContains(
|
|
394
|
-
wpHttpBody.referencedMemories[0].content,
|
|
395
|
-
"sqlite-vec",
|
|
396
|
-
"Referenced memory content matches"
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
// ────────────────────────────────────────────────────────────────
|
|
400
|
-
// Section 4: Conversation History Indexing
|
|
401
|
-
// ────────────────────────────────────────────────────────────────
|
|
402
|
-
console.log("\n§4 Conversation History Indexing");
|
|
403
|
-
|
|
404
|
-
// Create a fake Claude Code session JSONL file
|
|
405
|
-
const fakeSessionId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
406
|
-
const projectDir = join(sessionsPath, "-tmp-smoke-project");
|
|
407
|
-
const sessionDir = join(projectDir, fakeSessionId);
|
|
408
|
-
mkdirSync(sessionDir, { recursive: true });
|
|
409
|
-
|
|
410
|
-
const now = new Date().toISOString();
|
|
411
|
-
const sessionLines = [
|
|
412
|
-
JSON.stringify({
|
|
413
|
-
type: "user",
|
|
414
|
-
sessionId: fakeSessionId,
|
|
415
|
-
timestamp: now,
|
|
416
|
-
uuid: "msg-001",
|
|
417
|
-
message: {
|
|
418
|
-
role: "user",
|
|
419
|
-
content: "How does the sqlite-vec extension handle vector similarity search?",
|
|
420
|
-
},
|
|
421
|
-
}),
|
|
422
|
-
JSON.stringify({
|
|
423
|
-
type: "assistant",
|
|
424
|
-
sessionId: fakeSessionId,
|
|
425
|
-
timestamp: now,
|
|
426
|
-
uuid: "msg-002",
|
|
427
|
-
message: {
|
|
428
|
-
role: "assistant",
|
|
429
|
-
content: [
|
|
430
|
-
{
|
|
431
|
-
type: "text",
|
|
432
|
-
text: "The sqlite-vec extension uses a virtual table to store float32 vectors and supports cosine distance for similarity queries.",
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
},
|
|
436
|
-
}),
|
|
437
|
-
JSON.stringify({
|
|
438
|
-
type: "user",
|
|
439
|
-
sessionId: fakeSessionId,
|
|
440
|
-
timestamp: now,
|
|
441
|
-
uuid: "msg-003",
|
|
442
|
-
message: {
|
|
443
|
-
role: "user",
|
|
444
|
-
content: "What about full-text search integration?",
|
|
445
|
-
},
|
|
446
|
-
}),
|
|
447
|
-
JSON.stringify({
|
|
448
|
-
type: "assistant",
|
|
449
|
-
sessionId: fakeSessionId,
|
|
450
|
-
timestamp: now,
|
|
451
|
-
uuid: "msg-004",
|
|
452
|
-
message: {
|
|
453
|
-
role: "assistant",
|
|
454
|
-
content: [
|
|
455
|
-
{
|
|
456
|
-
type: "text",
|
|
457
|
-
text: "We use FTS5 alongside sqlite-vec for hybrid search, combining vector similarity with keyword matching for better recall.",
|
|
458
|
-
},
|
|
459
|
-
],
|
|
460
|
-
},
|
|
461
|
-
}),
|
|
462
|
-
];
|
|
463
|
-
|
|
464
|
-
writeFileSync(
|
|
465
|
-
join(sessionDir, `${fakeSessionId}.jsonl`),
|
|
466
|
-
sessionLines.join("\n") + "\n"
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
// Index via MCP
|
|
470
|
-
const indexResult = await mcpCall(baseUrl, sessionId, "index_conversations", {
|
|
471
|
-
path: sessionsPath,
|
|
472
|
-
});
|
|
473
|
-
assertContains(indexResult, "Indexed", "index_conversations returns indexed count (MCP)");
|
|
474
|
-
// Check it didn't report 0 indexed
|
|
475
|
-
assert(
|
|
476
|
-
!indexResult.includes("Indexed: 0"),
|
|
477
|
-
"index_conversations indexed at least 1 session (MCP)"
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
// Create a second session for HTTP endpoint test
|
|
481
|
-
const httpSessionId = "b2c3d4e5-f6a7-8901-bcde-f12345678901";
|
|
482
|
-
const httpSessionDir = join(projectDir, httpSessionId);
|
|
483
|
-
mkdirSync(httpSessionDir, { recursive: true });
|
|
484
|
-
writeFileSync(
|
|
485
|
-
join(httpSessionDir, `${httpSessionId}.jsonl`),
|
|
486
|
-
[
|
|
487
|
-
JSON.stringify({
|
|
488
|
-
type: "user", sessionId: httpSessionId, timestamp: now, uuid: "msg-h01",
|
|
489
|
-
message: { role: "user", content: "How does the warmup script download the ONNX model?" },
|
|
490
|
-
}),
|
|
491
|
-
JSON.stringify({
|
|
492
|
-
type: "assistant", sessionId: httpSessionId, timestamp: now, uuid: "msg-h02",
|
|
493
|
-
message: { role: "assistant", content: [{ type: "text", text: "The warmup script uses @huggingface/transformers to download and cache the ONNX model on first run." }] },
|
|
494
|
-
}),
|
|
495
|
-
].join("\n") + "\n"
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
// Index via HTTP endpoint (POST /index-conversations)
|
|
499
|
-
const httpIndexRes = await httpPost(baseUrl, "/index-conversations", {
|
|
500
|
-
path: sessionsPath,
|
|
501
|
-
});
|
|
502
|
-
const httpIndexBody = await httpIndexRes.json() as any;
|
|
503
|
-
assert(httpIndexRes.status === 200, "POST /index-conversations returns 200");
|
|
504
|
-
assert(httpIndexBody.indexed >= 1, "POST /index-conversations indexed at least 1 new session");
|
|
505
|
-
|
|
506
|
-
// Search history
|
|
507
|
-
const historySearch = await mcpCall(baseUrl, sessionId, "search_memories", {
|
|
508
|
-
query: "sqlite-vec vector similarity search",
|
|
509
|
-
intent: "fact_check",
|
|
510
|
-
reason_for_search: "smoke test: verify conversation history search",
|
|
511
|
-
history_only: true,
|
|
512
|
-
});
|
|
513
|
-
assertContains(
|
|
514
|
-
historySearch,
|
|
515
|
-
"sqlite-vec",
|
|
516
|
-
"history_only search returns conversation content"
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
// List indexed sessions
|
|
520
|
-
const listResult = await mcpCall(baseUrl, sessionId, "list_indexed_sessions", {});
|
|
521
|
-
assertContains(
|
|
522
|
-
listResult,
|
|
523
|
-
fakeSessionId,
|
|
524
|
-
"list_indexed_sessions shows our session"
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
// Reindex session
|
|
528
|
-
const reindexResult = await mcpCall(baseUrl, sessionId, "reindex_session", {
|
|
529
|
-
session_id: fakeSessionId,
|
|
530
|
-
});
|
|
531
|
-
assertContains(
|
|
532
|
-
reindexResult,
|
|
533
|
-
"success",
|
|
534
|
-
"reindex_session reports success"
|
|
535
|
-
);
|
|
536
|
-
|
|
537
|
-
// ────────────────────────────────────────────────────────────────
|
|
538
|
-
// Section 5: Memory Usefulness Voting
|
|
539
|
-
// ────────────────────────────────────────────────────────────────
|
|
540
|
-
console.log("\n§5 Memory Usefulness Voting");
|
|
541
|
-
|
|
542
|
-
// Store a fresh memory for voting
|
|
543
|
-
const voteStoreResult = await mcpCall(baseUrl, sessionId, "store_memories", {
|
|
544
|
-
memories: [
|
|
545
|
-
{
|
|
546
|
-
content: "Bun is faster than Node.js for starting processes",
|
|
547
|
-
metadata: { topic: "runtime" },
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
});
|
|
551
|
-
const voteMemoryId = extractMemoryId(voteStoreResult);
|
|
552
|
-
|
|
553
|
-
// Vote useful
|
|
554
|
-
const voteUpResult = await mcpCall(
|
|
555
|
-
baseUrl,
|
|
556
|
-
sessionId,
|
|
557
|
-
"report_memory_usefulness",
|
|
558
|
-
{ memory_id: voteMemoryId, useful: true }
|
|
559
|
-
);
|
|
560
|
-
assert(
|
|
561
|
-
!voteUpResult.startsWith("ERROR"),
|
|
562
|
-
"report_memory_usefulness(useful: true) succeeds"
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
// Vote not useful
|
|
566
|
-
const voteDownResult = await mcpCall(
|
|
567
|
-
baseUrl,
|
|
568
|
-
sessionId,
|
|
569
|
-
"report_memory_usefulness",
|
|
570
|
-
{ memory_id: voteMemoryId, useful: false }
|
|
571
|
-
);
|
|
572
|
-
assert(
|
|
573
|
-
!voteDownResult.startsWith("ERROR"),
|
|
574
|
-
"report_memory_usefulness(useful: false) succeeds"
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
// ────────────────────────────────────────────────────────────────
|
|
578
|
-
// Section 6: Lockfile Cleanup
|
|
579
|
-
// ────────────────────────────────────────────────────────────────
|
|
580
|
-
console.log("\n§6 Lockfile Cleanup");
|
|
581
|
-
|
|
582
|
-
assert(existsSync(lockfilePath), "Lockfile exists before shutdown");
|
|
583
|
-
|
|
584
|
-
// Send SIGTERM for graceful shutdown
|
|
585
|
-
proc.kill("SIGTERM");
|
|
586
|
-
const exitCode = await proc.exited;
|
|
587
|
-
assert(exitCode === 0, `Server exited with code 0 (got ${exitCode})`);
|
|
588
|
-
|
|
589
|
-
// Give a moment for cleanup
|
|
590
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
591
|
-
assert(!existsSync(lockfilePath), "Lockfile removed after SIGTERM");
|
|
592
|
-
|
|
593
|
-
proc = null; // Mark as cleaned up
|
|
594
|
-
|
|
595
|
-
// ────────────────────────────────────────────────────────────────
|
|
596
|
-
// Section 7: Migration Detection
|
|
597
|
-
// ────────────────────────────────────────────────────────────────
|
|
598
|
-
console.log("\n§7 Migration Detection");
|
|
599
|
-
|
|
600
|
-
// Create a fake LanceDB directory
|
|
601
|
-
const fakeLanceDir = join(tmpDir, "fake-lance.db");
|
|
602
|
-
mkdirSync(join(fakeLanceDir, "memories.lance"), { recursive: true });
|
|
603
|
-
|
|
604
|
-
const migProc = spawn(
|
|
605
|
-
["bun", "run", SERVER_PATH, "--db-file", fakeLanceDir],
|
|
606
|
-
{
|
|
607
|
-
stdin: "pipe",
|
|
608
|
-
stdout: "pipe",
|
|
609
|
-
stderr: "pipe",
|
|
610
|
-
cwd: tmpDir,
|
|
611
|
-
}
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
const migExitCode = await migProc.exited;
|
|
615
|
-
assert(migExitCode === 1, `Server exits with code 1 on LanceDB detection (got ${migExitCode})`);
|
|
616
|
-
|
|
617
|
-
// Read stderr for the error message
|
|
618
|
-
const migStderr = await new Response(migProc.stderr as ReadableStream).text();
|
|
619
|
-
assertContains(
|
|
620
|
-
migStderr,
|
|
621
|
-
"Legacy LanceDB data detected",
|
|
622
|
-
"Stderr contains legacy data warning"
|
|
623
|
-
);
|
|
624
|
-
assertContains(
|
|
625
|
-
migStderr,
|
|
626
|
-
"migrate",
|
|
627
|
-
"Stderr mentions migrate command"
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// ────────────────────────────────────────────────────────────────
|
|
631
|
-
// Section 8: Migrate Subcommand (No Real Data)
|
|
632
|
-
// ────────────────────────────────────────────────────────────────
|
|
633
|
-
console.log("\n§8 Migrate Subcommand (No Real Data)");
|
|
634
|
-
|
|
635
|
-
const nonexistentPath = join(tmpDir, "does-not-exist.db");
|
|
636
|
-
const migCmdProc = spawn(
|
|
637
|
-
["bun", "run", SERVER_PATH, "migrate", "--db-file", nonexistentPath],
|
|
638
|
-
{
|
|
639
|
-
stdin: "pipe",
|
|
640
|
-
stdout: "pipe",
|
|
641
|
-
stderr: "pipe",
|
|
642
|
-
cwd: tmpDir,
|
|
643
|
-
}
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
const migCmdExit = await migCmdProc.exited;
|
|
647
|
-
// Note: main() catches errors with .catch(console.error), so exit code may be 0
|
|
648
|
-
// even on failure. We verify via stderr content instead.
|
|
649
|
-
const migCmdStderr = await new Response(migCmdProc.stderr as ReadableStream).text();
|
|
650
|
-
assert(
|
|
651
|
-
migCmdStderr.includes("Source not found") ||
|
|
652
|
-
migCmdStderr.includes("not a directory") ||
|
|
653
|
-
migCmdStderr.includes("not found") ||
|
|
654
|
-
migCmdStderr.includes("Error"),
|
|
655
|
-
"Migrate with nonexistent source produces error message"
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
} catch (error) {
|
|
659
|
-
failed++;
|
|
660
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
661
|
-
failures.push(`FATAL: ${msg}`);
|
|
662
|
-
console.log(`\n 💥 FATAL ERROR: ${msg}`);
|
|
663
|
-
if (error instanceof Error && error.stack) {
|
|
664
|
-
console.log(` ${error.stack.split("\n").slice(1, 4).join("\n ")}`);
|
|
665
|
-
}
|
|
666
|
-
} finally {
|
|
667
|
-
// Clean up server if still running
|
|
668
|
-
if (proc) {
|
|
669
|
-
try {
|
|
670
|
-
proc.kill("SIGKILL");
|
|
671
|
-
await proc.exited;
|
|
672
|
-
} catch {
|
|
673
|
-
// Already exited
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Clean up temp directory
|
|
678
|
-
try {
|
|
679
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
680
|
-
} catch {
|
|
681
|
-
console.log(` ⚠️ Could not clean up ${tmpDir}`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// ── Summary ───────────────────────────────────────────────────────
|
|
686
|
-
console.log(`\n${"─".repeat(50)}`);
|
|
687
|
-
console.log(` Results: ${passed} passed, ${failed} failed`);
|
|
688
|
-
if (failures.length > 0) {
|
|
689
|
-
console.log(`\n Failures:`);
|
|
690
|
-
for (const f of failures) {
|
|
691
|
-
console.log(` - ${f}`);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
console.log(`${"─".repeat(50)}\n`);
|
|
695
|
-
|
|
696
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
main();
|