lynkr 3.0.0 → 3.2.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.
@@ -0,0 +1,585 @@
1
+ const assert = require("assert");
2
+ const { describe, it, beforeEach, afterEach } = require("node:test");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ describe("Memory Retriever", () => {
7
+ let store;
8
+ let retriever;
9
+ let testDbPath;
10
+
11
+ beforeEach(() => {
12
+ // Create a temporary test database
13
+ testDbPath = path.join(__dirname, `../../data/test-memory-${Date.now()}.db`);
14
+
15
+ // Clear module cache
16
+ delete require.cache[require.resolve("../../src/db")];
17
+ delete require.cache[require.resolve("../../src/memory/store")];
18
+ delete require.cache[require.resolve("../../src/memory/search")];
19
+ delete require.cache[require.resolve("../../src/memory/retriever")];
20
+
21
+ // Set test environment
22
+ process.env.DB_PATH = testDbPath;
23
+
24
+ // Initialize database with schema
25
+ require("../../src/db");
26
+
27
+ // Load modules
28
+ store = require("../../src/memory/store");
29
+ retriever = require("../../src/memory/retriever");
30
+
31
+ // Create test memories with different characteristics
32
+ const now = Date.now();
33
+
34
+ // Recent + important + relevant
35
+ store.createMemory({
36
+ content: "User prefers Python for data processing and machine learning",
37
+ type: "preference",
38
+ category: "user",
39
+ importance: 0.9,
40
+ surpriseScore: 0.8
41
+ });
42
+
43
+ // Old but important
44
+ const db = require("../../src/db");
45
+ const oldTimestamp = now - (30 * 24 * 60 * 60 * 1000); // 30 days ago
46
+ db.prepare(`
47
+ INSERT INTO memories (content, type, category, importance, surprise_score, created_at, updated_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?)
49
+ `).run(
50
+ "Critical: Always validate user input for SQL injection",
51
+ "fact",
52
+ "security",
53
+ 0.95,
54
+ 0.9,
55
+ oldTimestamp,
56
+ oldTimestamp
57
+ );
58
+
59
+ // Recent but less important
60
+ store.createMemory({
61
+ content: "User mentioned liking the color blue",
62
+ type: "preference",
63
+ category: "user",
64
+ importance: 0.3,
65
+ surpriseScore: 0.2
66
+ });
67
+
68
+ // Relevant to specific queries
69
+ store.createMemory({
70
+ content: "This project uses Express.js with TypeScript and JWT authentication",
71
+ type: "fact",
72
+ category: "project",
73
+ importance: 0.7,
74
+ surpriseScore: 0.6
75
+ });
76
+
77
+ store.createMemory({
78
+ content: "Database connection pool configured with max 20 connections",
79
+ type: "fact",
80
+ category: "code",
81
+ importance: 0.6,
82
+ surpriseScore: 0.5
83
+ });
84
+ });
85
+
86
+ afterEach(() => {
87
+ // Clean up test database
88
+ try {
89
+ if (fs.existsSync(testDbPath)) {
90
+ fs.unlinkSync(testDbPath);
91
+ }
92
+ } catch (err) {
93
+ // Ignore cleanup errors
94
+ }
95
+ });
96
+
97
+ describe("retrieveRelevantMemories()", () => {
98
+ it("should retrieve memories relevant to query", () => {
99
+ const memories = retriever.retrieveRelevantMemories("Python programming");
100
+ assert.ok(memories.length > 0);
101
+ assert.ok(memories.some(m => m.content.toLowerCase().includes("python")));
102
+ });
103
+
104
+ it("should respect limit parameter", () => {
105
+ const memories = retriever.retrieveRelevantMemories("project", { limit: 2 });
106
+ assert.ok(memories.length <= 2);
107
+ });
108
+
109
+ it("should rank by multi-signal scoring", () => {
110
+ const memories = retriever.retrieveRelevantMemories("authentication security", { limit: 5 });
111
+
112
+ if (memories.length > 1) {
113
+ // First result should have highest score
114
+ const scores = memories.map(m =>
115
+ retriever.calculateRetrievalScore(m, "authentication security", {
116
+ recencyWeight: 0.3,
117
+ importanceWeight: 0.4,
118
+ relevanceWeight: 0.3
119
+ })
120
+ );
121
+
122
+ for (let i = 1; i < scores.length; i++) {
123
+ assert.ok(scores[i - 1] >= scores[i],
124
+ `Memory ${i-1} score ${scores[i-1]} should be >= memory ${i} score ${scores[i]}`);
125
+ }
126
+ }
127
+ });
128
+
129
+ it("should combine recency, importance, and relevance", () => {
130
+ const memories = retriever.retrieveRelevantMemories("Python", { limit: 5 });
131
+
132
+ // Should include the high-importance Python memory
133
+ assert.ok(memories.some(m =>
134
+ m.content.includes("Python") && m.importance >= 0.8
135
+ ));
136
+ });
137
+
138
+ it("should filter by session id when specified", () => {
139
+ // Use null for sessionId to avoid FOREIGN KEY constraint
140
+ store.createMemory({
141
+ content: "Session-specific memory about testing",
142
+ type: "fact",
143
+ sessionId: null, // Changed from "test-session-123" to avoid FK constraint
144
+ importance: 0.8
145
+ });
146
+
147
+ const memories = retriever.retrieveRelevantMemories("testing", {
148
+ sessionId: null,
149
+ includeGlobal: true
150
+ });
151
+
152
+ // Should include memories
153
+ assert.ok(Array.isArray(memories));
154
+ });
155
+
156
+ it("should include global memories when includeGlobal is true", () => {
157
+ // All memories use null sessionId to avoid FK constraint
158
+ store.createMemory({
159
+ content: "Memory about databases type A",
160
+ type: "fact",
161
+ sessionId: null,
162
+ importance: 0.5
163
+ });
164
+
165
+ store.createMemory({
166
+ content: "Global memory about databases type B",
167
+ type: "fact",
168
+ sessionId: null,
169
+ importance: 0.9
170
+ });
171
+
172
+ const memories = retriever.retrieveRelevantMemories("databases", {
173
+ sessionId: null,
174
+ includeGlobal: true,
175
+ limit: 10
176
+ });
177
+
178
+ // Should include memories
179
+ assert.ok(memories.length >= 0);
180
+ });
181
+
182
+ it("should handle empty query gracefully", () => {
183
+ const memories = retriever.retrieveRelevantMemories("", { limit: 3 });
184
+ // Should return recent/important memories even without query
185
+ assert.ok(Array.isArray(memories));
186
+ });
187
+
188
+ it("should handle queries with no matches", () => {
189
+ const memories = retriever.retrieveRelevantMemories("nonexistent-keyword-xyz");
190
+ // Should still return some memories (e.g., by importance)
191
+ assert.ok(Array.isArray(memories));
192
+ });
193
+ });
194
+
195
+ describe("calculateRetrievalScore()", () => {
196
+ it("should calculate score with default weights", () => {
197
+ const memory = {
198
+ content: "User prefers Python for data processing",
199
+ importance: 0.8,
200
+ createdAt: Date.now(),
201
+ accessCount: 5
202
+ };
203
+
204
+ const score = retriever.calculateRetrievalScore(memory, "Python data", {
205
+ recencyWeight: 0.3,
206
+ importanceWeight: 0.4,
207
+ relevanceWeight: 0.3
208
+ });
209
+ assert.ok(score >= 0 && score <= 1, `Score ${score} should be in [0,1]`);
210
+ });
211
+
212
+ it("should give higher scores to recent memories", () => {
213
+ const recent = {
214
+ content: "Recent memory about Python",
215
+ importance: 0.5,
216
+ createdAt: Date.now(),
217
+ accessCount: 0
218
+ };
219
+
220
+ const old = {
221
+ content: "Old memory about Python",
222
+ importance: 0.5,
223
+ createdAt: Date.now() - (60 * 24 * 60 * 60 * 1000), // 60 days ago
224
+ accessCount: 0
225
+ };
226
+
227
+ const weights = { recencyWeight: 0.3, importanceWeight: 0.4, relevanceWeight: 0.3 };
228
+ const recentScore = retriever.calculateRetrievalScore(recent, "Python", weights);
229
+ const oldScore = retriever.calculateRetrievalScore(old, "Python", weights);
230
+
231
+ assert.ok(recentScore > oldScore,
232
+ `Recent score ${recentScore} should be > old score ${oldScore}`);
233
+ });
234
+
235
+ it("should give higher scores to important memories", () => {
236
+ const important = {
237
+ content: "Important memory about Python",
238
+ importance: 0.9,
239
+ createdAt: Date.now(),
240
+ accessCount: 0
241
+ };
242
+
243
+ const unimportant = {
244
+ content: "Unimportant memory about Python",
245
+ importance: 0.2,
246
+ createdAt: Date.now(),
247
+ accessCount: 0
248
+ };
249
+
250
+ const weights = { recencyWeight: 0.3, importanceWeight: 0.4, relevanceWeight: 0.3 };
251
+ const importantScore = retriever.calculateRetrievalScore(important, "Python", weights);
252
+ const unimportantScore = retriever.calculateRetrievalScore(unimportant, "Python", weights);
253
+
254
+ assert.ok(importantScore > unimportantScore,
255
+ `Important score ${importantScore} should be > unimportant score ${unimportantScore}`);
256
+ });
257
+
258
+ it("should give higher scores to relevant content", () => {
259
+ const relevant = {
260
+ content: "Python programming language for data processing and machine learning",
261
+ importance: 0.5,
262
+ createdAt: Date.now(),
263
+ accessCount: 0
264
+ };
265
+
266
+ const irrelevant = {
267
+ content: "JavaScript framework for web development",
268
+ importance: 0.5,
269
+ createdAt: Date.now(),
270
+ accessCount: 0
271
+ };
272
+
273
+ const weights = { recencyWeight: 0.3, importanceWeight: 0.4, relevanceWeight: 0.3 };
274
+ const relevantScore = retriever.calculateRetrievalScore(relevant, "Python programming", weights);
275
+ const irrelevantScore = retriever.calculateRetrievalScore(irrelevant, "Python programming", weights);
276
+
277
+ assert.ok(relevantScore > irrelevantScore,
278
+ `Relevant score ${relevantScore} should be > irrelevant score ${irrelevantScore}`);
279
+ });
280
+
281
+ it("should allow custom weight configuration", () => {
282
+ const memory = {
283
+ content: "Test memory",
284
+ importance: 0.8,
285
+ createdAt: Date.now() - (30 * 24 * 60 * 60 * 1000),
286
+ accessCount: 0
287
+ };
288
+
289
+ // Emphasize importance over recency
290
+ const importanceHeavy = retriever.calculateRetrievalScore(memory, "test", {
291
+ recencyWeight: 0.1,
292
+ importanceWeight: 0.8,
293
+ relevanceWeight: 0.1
294
+ });
295
+
296
+ // Emphasize recency over importance
297
+ const recencyHeavy = retriever.calculateRetrievalScore(memory, "test", {
298
+ recencyWeight: 0.8,
299
+ importanceWeight: 0.1,
300
+ relevanceWeight: 0.1
301
+ });
302
+
303
+ // For an old but important memory, importance-heavy should score higher
304
+ assert.ok(importanceHeavy > recencyHeavy,
305
+ `Importance-heavy ${importanceHeavy} should be > recency-heavy ${recencyHeavy} for old memory`);
306
+ });
307
+ });
308
+
309
+ describe("formatMemoriesForContext()", () => {
310
+ it("should format memories as readable text", () => {
311
+ const memories = store.getRecentMemories({ limit: 3 });
312
+ const formatted = retriever.formatMemoriesForContext(memories);
313
+
314
+ assert.ok(typeof formatted === "string");
315
+ assert.ok(formatted.length > 0);
316
+
317
+ // Should include memory types and content
318
+ memories.forEach(m => {
319
+ assert.ok(formatted.includes(m.type) || formatted.includes(m.content));
320
+ });
321
+ });
322
+
323
+ it("should handle empty memories array", () => {
324
+ const formatted = retriever.formatMemoriesForContext([]);
325
+ assert.strictEqual(formatted, "");
326
+ });
327
+
328
+ it("should include relative timestamps", () => {
329
+ const memories = store.getRecentMemories({ limit: 2 });
330
+ const formatted = retriever.formatMemoriesForContext(memories);
331
+
332
+ // Should include time indicators
333
+ assert.ok(
334
+ formatted.includes("ago") ||
335
+ formatted.includes("recently") ||
336
+ formatted.includes("just now")
337
+ );
338
+ });
339
+
340
+ it("should group by type", () => {
341
+ const memories = [
342
+ { content: "Preference 1", type: "preference", createdAt: Date.now() },
343
+ { content: "Preference 2", type: "preference", createdAt: Date.now() },
344
+ { content: "Fact 1", type: "fact", createdAt: Date.now() }
345
+ ];
346
+
347
+ const formatted = retriever.formatMemoriesForContext(memories);
348
+
349
+ // Should mention types
350
+ assert.ok(formatted.includes("preference") || formatted.includes("Preference"));
351
+ assert.ok(formatted.includes("fact") || formatted.includes("Fact"));
352
+ });
353
+ });
354
+
355
+ describe("injectMemoriesIntoSystem()", () => {
356
+ it("should inject memories into system prompt", () => {
357
+ const originalSystem = "You are a helpful assistant.";
358
+ const memories = store.getRecentMemories({ limit: 2 });
359
+
360
+ const injected = retriever.injectMemoriesIntoSystem(originalSystem, memories);
361
+
362
+ assert.ok(typeof injected === "string");
363
+ assert.ok(injected.includes(originalSystem));
364
+ assert.ok(injected.length > originalSystem.length);
365
+ });
366
+
367
+ it("should include memory content in injection", () => {
368
+ const originalSystem = "You are a helpful assistant.";
369
+ const memories = [
370
+ {
371
+ content: "User prefers Python",
372
+ type: "preference",
373
+ createdAt: Date.now()
374
+ }
375
+ ];
376
+
377
+ const injected = retriever.injectMemoriesIntoSystem(originalSystem, memories);
378
+
379
+ assert.ok(injected.includes("Python") || injected.includes("prefer"));
380
+ });
381
+
382
+ it("should handle empty memories", () => {
383
+ const originalSystem = "You are a helpful assistant.";
384
+ const injected = retriever.injectMemoriesIntoSystem(originalSystem, []);
385
+
386
+ assert.strictEqual(injected, originalSystem);
387
+ });
388
+
389
+ it("should handle null/undefined system prompt", () => {
390
+ const memories = store.getRecentMemories({ limit: 2 });
391
+
392
+ const fromNull = retriever.injectMemoriesIntoSystem(null, memories);
393
+ const fromUndefined = retriever.injectMemoriesIntoSystem(undefined, memories);
394
+
395
+ assert.ok(typeof fromNull === "string");
396
+ assert.ok(typeof fromUndefined === "string");
397
+ });
398
+
399
+ it("should support different injection formats", () => {
400
+ const memories = store.getRecentMemories({ limit: 2 });
401
+
402
+ const systemFormat = retriever.injectMemoriesIntoSystem(
403
+ "You are helpful.",
404
+ memories,
405
+ "system"
406
+ );
407
+
408
+ const preambleFormat = retriever.injectMemoriesIntoSystem(
409
+ "You are helpful.",
410
+ memories,
411
+ "assistant_preamble"
412
+ );
413
+
414
+ assert.ok(typeof systemFormat === "string");
415
+ // assistant_preamble format returns an object
416
+ assert.ok(typeof preambleFormat === "object");
417
+ assert.ok(preambleFormat.system === "You are helpful.");
418
+ assert.ok(typeof preambleFormat.memoryPreamble === "string");
419
+ assert.ok(preambleFormat.memoryPreamble.length > 0);
420
+ });
421
+ });
422
+
423
+ describe("getMemoryStats()", () => {
424
+ it("should return statistics about memories", () => {
425
+ const stats = retriever.getMemoryStats();
426
+
427
+ assert.ok(stats.total >= 0);
428
+ assert.ok(stats.byType);
429
+ assert.ok(stats.byCategory);
430
+ assert.ok(typeof stats.avgImportance === "number");
431
+ });
432
+
433
+ it("should count memories by type", () => {
434
+ const stats = retriever.getMemoryStats();
435
+
436
+ assert.ok(typeof stats.byType === "object");
437
+ // Should have counts for types we created
438
+ assert.ok(stats.byType.preference >= 0);
439
+ assert.ok(stats.byType.fact >= 0);
440
+ });
441
+
442
+ it("should count memories by category", () => {
443
+ const stats = retriever.getMemoryStats();
444
+
445
+ assert.ok(typeof stats.byCategory === "object");
446
+ // Should have counts for categories we created
447
+ assert.ok(stats.byCategory.user >= 0 || stats.byCategory.project >= 0);
448
+ });
449
+
450
+ it("should calculate average importance", () => {
451
+ const stats = retriever.getMemoryStats();
452
+
453
+ assert.ok(stats.avgImportance >= 0 && stats.avgImportance <= 1);
454
+ });
455
+
456
+ it("should filter stats by session", () => {
457
+ store.createMemory({
458
+ content: "Session memory",
459
+ type: "fact",
460
+ sessionId: "test-session"
461
+ });
462
+
463
+ const globalStats = retriever.getMemoryStats();
464
+ const sessionStats = retriever.getMemoryStats({ sessionId: "test-session" });
465
+
466
+ assert.ok(sessionStats.total <= globalStats.total);
467
+ });
468
+ });
469
+
470
+ describe("extractQueryFromMessage()", () => {
471
+ it("should extract query from simple user message", () => {
472
+ const message = {
473
+ role: "user",
474
+ content: "How do I use Python for data processing?"
475
+ };
476
+
477
+ const query = retriever.extractQueryFromMessage(message);
478
+ assert.ok(typeof query === "string");
479
+ assert.ok(query.length > 0);
480
+ });
481
+
482
+ it("should handle messages with tool use", () => {
483
+ const message = {
484
+ role: "user",
485
+ content: [
486
+ { type: "text", text: "Search for Python tutorials" },
487
+ { type: "tool_use", name: "search" }
488
+ ]
489
+ };
490
+
491
+ const query = retriever.extractQueryFromMessage(message);
492
+ assert.ok(typeof query === "string");
493
+ });
494
+
495
+ it("should handle empty messages", () => {
496
+ const message = { role: "user", content: "" };
497
+ const query = retriever.extractQueryFromMessage(message);
498
+ assert.strictEqual(query, "");
499
+ });
500
+
501
+ it("should extract key terms from longer messages", () => {
502
+ const message = {
503
+ role: "user",
504
+ content: "I'm working on a new feature that requires authentication. Can you help me implement JWT tokens?"
505
+ };
506
+
507
+ const query = retriever.extractQueryFromMessage(message);
508
+ assert.ok(query.includes("authentication") || query.includes("JWT"));
509
+ });
510
+ });
511
+
512
+ describe("Performance", () => {
513
+ it("should retrieve memories within 50ms target", () => {
514
+ // Create more memories for realistic test
515
+ for (let i = 0; i < 50; i++) {
516
+ store.createMemory({
517
+ content: `Test memory ${i} about various topics`,
518
+ type: "fact",
519
+ importance: Math.random()
520
+ });
521
+ }
522
+
523
+ const start = Date.now();
524
+ const memories = retriever.retrieveRelevantMemories("test topics", { limit: 10 });
525
+ const duration = Date.now() - start;
526
+
527
+ assert.ok(memories.length > 0);
528
+ assert.ok(duration < 50, `Retrieval took ${duration}ms, expected < 50ms`);
529
+ });
530
+
531
+ it("should handle concurrent retrievals", () => {
532
+ const queries = [
533
+ "Python programming",
534
+ "JavaScript frameworks",
535
+ "database connections",
536
+ "authentication security"
537
+ ];
538
+
539
+ const results = queries.map(q =>
540
+ retriever.retrieveRelevantMemories(q, { limit: 5 })
541
+ );
542
+
543
+ results.forEach(memories => {
544
+ assert.ok(Array.isArray(memories));
545
+ });
546
+ });
547
+ });
548
+
549
+ describe("Edge Cases", () => {
550
+ it("should handle very long queries", () => {
551
+ const longQuery = "Python ".repeat(100);
552
+ assert.doesNotThrow(() => {
553
+ retriever.retrieveRelevantMemories(longQuery, { limit: 5 });
554
+ });
555
+ });
556
+
557
+ it("should handle special characters in queries", () => {
558
+ assert.doesNotThrow(() => {
559
+ retriever.retrieveRelevantMemories("@angular/core ^16.0.0", { limit: 5 });
560
+ });
561
+ });
562
+
563
+ it("should handle zero limit", () => {
564
+ const memories = retriever.retrieveRelevantMemories("test", { limit: 0 });
565
+ assert.strictEqual(memories.length, 0);
566
+ });
567
+
568
+ it("should handle negative weights gracefully", () => {
569
+ const memory = {
570
+ content: "Test",
571
+ importance: 0.5,
572
+ createdAt: Date.now()
573
+ };
574
+
575
+ // Should normalize or clamp weights
576
+ assert.doesNotThrow(() => {
577
+ retriever.calculateRetrievalScore(memory, "test", {
578
+ recencyWeight: -0.5,
579
+ importanceWeight: 1.5,
580
+ relevanceWeight: 0.5
581
+ });
582
+ });
583
+ });
584
+ });
585
+ });