@smallironman/mcp-memory-keeper 0.12.2-fork1

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1281 -0
  4. package/bin/mcp-memory-keeper +54 -0
  5. package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
  6. package/dist/__tests__/e2e/server-e2e.test.js +341 -0
  7. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  8. package/dist/__tests__/helpers/test-server.js +92 -0
  9. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  10. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  11. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  12. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  13. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  14. package/dist/__tests__/integration/channels.test.js +376 -0
  15. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  16. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  17. package/dist/__tests__/integration/context-operations.test.js +243 -0
  18. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  19. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  20. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  21. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  22. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  23. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  24. package/dist/__tests__/integration/contextSearch.test.js +1054 -0
  25. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  26. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  27. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  28. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  29. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  30. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  31. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  32. package/dist/__tests__/integration/error-cases.test.js +411 -0
  33. package/dist/__tests__/integration/export-import.test.js +367 -0
  34. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  35. package/dist/__tests__/integration/file-operations.test.js +264 -0
  36. package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
  37. package/dist/__tests__/integration/git-integration.test.js +241 -0
  38. package/dist/__tests__/integration/index-tools.test.js +496 -0
  39. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  40. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  41. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  42. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  43. package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
  44. package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
  45. package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
  46. package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
  47. package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
  48. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  49. package/dist/__tests__/integration/migrations.test.js +528 -0
  50. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  51. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  52. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  53. package/dist/__tests__/integration/project-directory.test.js +291 -0
  54. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  55. package/dist/__tests__/integration/retention.test.js +513 -0
  56. package/dist/__tests__/integration/search.test.js +333 -0
  57. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  58. package/dist/__tests__/integration/server-initialization.test.js +305 -0
  59. package/dist/__tests__/integration/session-management.test.js +219 -0
  60. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  61. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  62. package/dist/__tests__/integration/summarization.test.js +308 -0
  63. package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
  64. package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
  65. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  66. package/dist/__tests__/security/input-validation.test.js +115 -0
  67. package/dist/__tests__/utils/agents.test.js +473 -0
  68. package/dist/__tests__/utils/database.test.js +177 -0
  69. package/dist/__tests__/utils/git.test.js +122 -0
  70. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  71. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  72. package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
  73. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  74. package/dist/__tests__/utils/token-limits.test.js +225 -0
  75. package/dist/__tests__/utils/tool-profiles.test.js +374 -0
  76. package/dist/__tests__/utils/validation.test.js +200 -0
  77. package/dist/__tests__/utils/vector-store.test.js +231 -0
  78. package/dist/handlers/contextWatchHandlers.js +206 -0
  79. package/dist/index.js +4425 -0
  80. package/dist/migrations/003_add_channels.js +174 -0
  81. package/dist/migrations/004_add_context_watch.js +151 -0
  82. package/dist/migrations/005_add_context_watch.js +98 -0
  83. package/dist/migrations/simplify-sharing.js +117 -0
  84. package/dist/repositories/BaseRepository.js +30 -0
  85. package/dist/repositories/CheckpointRepository.js +140 -0
  86. package/dist/repositories/ContextRepository.js +2017 -0
  87. package/dist/repositories/FileRepository.js +104 -0
  88. package/dist/repositories/RepositoryManager.js +62 -0
  89. package/dist/repositories/SessionRepository.js +66 -0
  90. package/dist/repositories/WatcherRepository.js +252 -0
  91. package/dist/repositories/index.js +15 -0
  92. package/dist/test-helpers/database-helper.js +128 -0
  93. package/dist/types/entities.js +3 -0
  94. package/dist/utils/agents.js +791 -0
  95. package/dist/utils/channels.js +150 -0
  96. package/dist/utils/database.js +780 -0
  97. package/dist/utils/feature-flags.js +476 -0
  98. package/dist/utils/git.js +145 -0
  99. package/dist/utils/knowledge-graph.js +264 -0
  100. package/dist/utils/migrationHealthCheck.js +373 -0
  101. package/dist/utils/migrations.js +452 -0
  102. package/dist/utils/retention.js +460 -0
  103. package/dist/utils/timestamps.js +112 -0
  104. package/dist/utils/token-limits.js +350 -0
  105. package/dist/utils/tool-profiles.js +242 -0
  106. package/dist/utils/validation.js +296 -0
  107. package/dist/utils/vector-store.js +247 -0
  108. package/examples/config.json +31 -0
  109. package/examples/project-directory-setup.md +114 -0
  110. package/package.json +85 -0
@@ -0,0 +1,1054 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const database_1 = require("../../utils/database");
37
+ const ContextRepository_1 = require("../../repositories/ContextRepository");
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const fs = __importStar(require("fs"));
41
+ const uuid_1 = require("uuid");
42
+ describe('Enhanced Context Search Integration Tests', () => {
43
+ let dbManager;
44
+ let tempDbPath;
45
+ let db;
46
+ let testSessionId;
47
+ let otherSessionId;
48
+ beforeEach(() => {
49
+ tempDbPath = path.join(os.tmpdir(), `test-context-search-${Date.now()}.db`);
50
+ dbManager = new database_1.DatabaseManager({
51
+ filename: tempDbPath,
52
+ maxSize: 10 * 1024 * 1024,
53
+ walMode: true,
54
+ });
55
+ db = dbManager.getDatabase();
56
+ // Create test sessions
57
+ testSessionId = (0, uuid_1.v4)();
58
+ otherSessionId = (0, uuid_1.v4)();
59
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Main Test Session');
60
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
61
+ });
62
+ afterEach(() => {
63
+ dbManager.close();
64
+ try {
65
+ fs.unlinkSync(tempDbPath);
66
+ fs.unlinkSync(`${tempDbPath}-wal`);
67
+ fs.unlinkSync(`${tempDbPath}-shm`);
68
+ }
69
+ catch (_e) {
70
+ // Ignore
71
+ }
72
+ });
73
+ describe('Backward Compatibility', () => {
74
+ beforeEach(() => {
75
+ // Add test data
76
+ const items = [
77
+ {
78
+ key: 'auth_config',
79
+ value: 'Authentication configuration settings',
80
+ category: 'config',
81
+ priority: 'high',
82
+ },
83
+ {
84
+ key: 'db_connection',
85
+ value: 'Database connection string for auth',
86
+ category: 'config',
87
+ priority: 'normal',
88
+ },
89
+ { key: 'user_model', value: 'User model definition', category: 'code', priority: 'high' },
90
+ {
91
+ key: 'auth_middleware',
92
+ value: 'Authentication middleware implementation',
93
+ category: 'code',
94
+ priority: 'normal',
95
+ },
96
+ ];
97
+ items.forEach(item => {
98
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority);
99
+ });
100
+ });
101
+ it('should maintain existing search functionality with query parameter', () => {
102
+ // Test existing simple search - should search in both key and value
103
+ const sql = `
104
+ SELECT * FROM context_items
105
+ WHERE session_id = ? AND (key LIKE ? OR value LIKE ?)
106
+ ORDER BY priority DESC, created_at DESC
107
+ `;
108
+ const results = db.prepare(sql).all(testSessionId, '%auth%', '%auth%');
109
+ expect(results).toHaveLength(3); // auth_config, auth_middleware, db_connection (has 'auth' in value)
110
+ expect(results.map((r) => r.key)).toContain('auth_config');
111
+ expect(results.map((r) => r.key)).toContain('auth_middleware');
112
+ expect(results.map((r) => r.key)).toContain('db_connection');
113
+ });
114
+ it('should support searchIn parameter for backward compatibility', () => {
115
+ // Search only in keys
116
+ const keyResults = db
117
+ .prepare(`
118
+ SELECT * FROM context_items
119
+ WHERE session_id = ? AND key LIKE ?
120
+ ORDER BY priority DESC, created_at DESC
121
+ `)
122
+ .all(testSessionId, '%auth%');
123
+ expect(keyResults).toHaveLength(2); // auth_config, auth_middleware
124
+ // Search only in values
125
+ const valueResults = db
126
+ .prepare(`
127
+ SELECT * FROM context_items
128
+ WHERE session_id = ? AND value LIKE ?
129
+ ORDER BY priority DESC, created_at DESC
130
+ `)
131
+ .all(testSessionId, '%auth%');
132
+ expect(valueResults).toHaveLength(3); // auth_config, auth_middleware, db_connection
133
+ });
134
+ });
135
+ describe('Time Filtering', () => {
136
+ beforeEach(() => {
137
+ const now = new Date();
138
+ const items = [
139
+ { key: 'today_item', value: 'Created today', created_at: now.toISOString() },
140
+ {
141
+ key: 'yesterday_item',
142
+ value: 'Created yesterday',
143
+ created_at: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
144
+ },
145
+ {
146
+ key: 'two_days_ago',
147
+ value: 'Created 2 days ago',
148
+ created_at: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
149
+ },
150
+ {
151
+ key: 'week_old',
152
+ value: 'Created a week ago',
153
+ created_at: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
154
+ },
155
+ {
156
+ key: 'month_old',
157
+ value: 'Created a month ago',
158
+ created_at: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
159
+ },
160
+ ];
161
+ items.forEach(item => {
162
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.created_at);
163
+ });
164
+ });
165
+ it('should filter by createdAfter', () => {
166
+ const threeDaysAgo = new Date();
167
+ threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
168
+ const results = db
169
+ .prepare(`
170
+ SELECT * FROM context_items
171
+ WHERE session_id = ? AND created_at > ?
172
+ ORDER BY created_at DESC
173
+ `)
174
+ .all(testSessionId, threeDaysAgo.toISOString());
175
+ expect(results).toHaveLength(3); // today, yesterday, two_days_ago
176
+ expect(results.map((r) => r.key)).toContain('today_item');
177
+ expect(results.map((r) => r.key)).toContain('yesterday_item');
178
+ expect(results.map((r) => r.key)).toContain('two_days_ago');
179
+ });
180
+ it('should filter by createdBefore', () => {
181
+ // Get all items first to check their timestamps
182
+ const allItems = db
183
+ .prepare(`
184
+ SELECT * FROM context_items
185
+ WHERE session_id = ?
186
+ ORDER BY created_at DESC
187
+ `)
188
+ .all(testSessionId);
189
+ // Find the two_days_ago item to get its exact timestamp
190
+ const twoDaysAgoItem = allItems.find((item) => item.key === 'two_days_ago');
191
+ // Use a timestamp just after the two_days_ago item to exclude it
192
+ const cutoffDate = new Date(twoDaysAgoItem.created_at);
193
+ cutoffDate.setMilliseconds(cutoffDate.getMilliseconds() - 1); // Go back 1ms to exclude this item
194
+ const results = db
195
+ .prepare(`
196
+ SELECT * FROM context_items
197
+ WHERE session_id = ? AND created_at < ?
198
+ ORDER BY created_at DESC
199
+ `)
200
+ .all(testSessionId, cutoffDate.toISOString());
201
+ expect(results).toHaveLength(2); // week_old, month_old
202
+ expect(results.map((r) => r.key)).toContain('week_old');
203
+ expect(results.map((r) => r.key)).toContain('month_old');
204
+ expect(results.map((r) => r.key)).not.toContain('two_days_ago');
205
+ });
206
+ it('should support relative time parsing', () => {
207
+ // Test "2 hours ago"
208
+ const twoHoursAgo = new Date();
209
+ twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
210
+ // Add an item from 1 hour ago
211
+ const oneHourAgo = new Date();
212
+ oneHourAgo.setHours(oneHourAgo.getHours() - 1);
213
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'recent_item', 'Created 1 hour ago', oneHourAgo.toISOString());
214
+ const results = db
215
+ .prepare(`
216
+ SELECT * FROM context_items
217
+ WHERE session_id = ? AND created_at > ?
218
+ ORDER BY created_at DESC
219
+ `)
220
+ .all(testSessionId, twoHoursAgo.toISOString());
221
+ const recentResults = results.filter((r) => new Date(r.created_at).getTime() > twoHoursAgo.getTime());
222
+ expect(recentResults.some((r) => r.key === 'recent_item')).toBe(true);
223
+ expect(recentResults.some((r) => r.key === 'today_item')).toBe(true);
224
+ });
225
+ it('should handle "yesterday" as relative time', () => {
226
+ const now = new Date();
227
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
228
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
229
+ const results = db
230
+ .prepare(`
231
+ SELECT * FROM context_items
232
+ WHERE session_id = ?
233
+ AND created_at >= ?
234
+ AND created_at < ?
235
+ ORDER BY created_at DESC
236
+ `)
237
+ .all(testSessionId, yesterday.toISOString(), today.toISOString());
238
+ // Should only include yesterday_item
239
+ const yesterdayResults = results.filter((r) => {
240
+ const itemDate = new Date(r.created_at);
241
+ return itemDate >= yesterday && itemDate < today;
242
+ });
243
+ expect(yesterdayResults.length).toBeGreaterThan(0);
244
+ });
245
+ });
246
+ describe('Channel Filtering', () => {
247
+ beforeEach(() => {
248
+ const items = [
249
+ { key: 'main_task', value: 'Main channel task', channel: 'main', priority: 'high' },
250
+ {
251
+ key: 'feature_task',
252
+ value: 'Feature branch task',
253
+ channel: 'feature/auth',
254
+ priority: 'normal',
255
+ },
256
+ {
257
+ key: 'feature_bug',
258
+ value: 'Feature branch bug',
259
+ channel: 'feature/auth',
260
+ priority: 'high',
261
+ },
262
+ {
263
+ key: 'hotfix_task',
264
+ value: 'Hotfix task',
265
+ channel: 'hotfix/security',
266
+ priority: 'critical',
267
+ },
268
+ { key: 'no_channel', value: 'Task without channel', channel: null, priority: 'normal' },
269
+ ];
270
+ items.forEach(item => {
271
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, channel, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.channel, item.priority);
272
+ });
273
+ });
274
+ it('should filter by single channel', () => {
275
+ const results = db
276
+ .prepare(`
277
+ SELECT * FROM context_items
278
+ WHERE session_id = ? AND channel = ?
279
+ ORDER BY priority DESC, created_at DESC
280
+ `)
281
+ .all(testSessionId, 'feature/auth');
282
+ expect(results).toHaveLength(2);
283
+ expect(results.map((r) => r.key)).toContain('feature_task');
284
+ expect(results.map((r) => r.key)).toContain('feature_bug');
285
+ });
286
+ it('should filter by multiple channels', () => {
287
+ const channels = ['main', 'hotfix/security'];
288
+ const placeholders = channels.map(() => '?').join(',');
289
+ const results = db
290
+ .prepare(`
291
+ SELECT * FROM context_items
292
+ WHERE session_id = ? AND channel IN (${placeholders})
293
+ ORDER BY priority DESC, created_at DESC
294
+ `)
295
+ .all(testSessionId, ...channels);
296
+ expect(results).toHaveLength(2);
297
+ expect(results.map((r) => r.key)).toContain('main_task');
298
+ expect(results.map((r) => r.key)).toContain('hotfix_task');
299
+ });
300
+ it('should handle items without channels', () => {
301
+ const results = db
302
+ .prepare(`
303
+ SELECT * FROM context_items
304
+ WHERE session_id = ? AND channel IS NULL
305
+ ORDER BY created_at DESC
306
+ `)
307
+ .all(testSessionId);
308
+ expect(results).toHaveLength(1);
309
+ expect(results[0].key).toBe('no_channel');
310
+ });
311
+ });
312
+ describe('Sort Options', () => {
313
+ beforeEach(() => {
314
+ // Create items with specific timestamps and keys for testing sort
315
+ const baseTime = new Date('2024-01-01T00:00:00Z');
316
+ const items = [
317
+ {
318
+ key: 'alpha_item',
319
+ value: 'First alphabetically',
320
+ created_at: new Date(baseTime.getTime() + 1000).toISOString(),
321
+ updated_at: new Date(baseTime.getTime() + 5000).toISOString(),
322
+ },
323
+ {
324
+ key: 'beta_item',
325
+ value: 'Second alphabetically',
326
+ created_at: new Date(baseTime.getTime() + 2000).toISOString(),
327
+ updated_at: new Date(baseTime.getTime() + 4000).toISOString(),
328
+ },
329
+ {
330
+ key: 'charlie_item',
331
+ value: 'Third alphabetically',
332
+ created_at: new Date(baseTime.getTime() + 3000).toISOString(),
333
+ updated_at: new Date(baseTime.getTime() + 3000).toISOString(),
334
+ },
335
+ ];
336
+ items.forEach(item => {
337
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.created_at, item.updated_at);
338
+ });
339
+ });
340
+ it('should sort by created_at descending (default)', () => {
341
+ const results = db
342
+ .prepare(`
343
+ SELECT * FROM context_items
344
+ WHERE session_id = ?
345
+ ORDER BY created_at DESC
346
+ `)
347
+ .all(testSessionId);
348
+ expect(results[0].key).toBe('charlie_item'); // Most recent
349
+ expect(results[results.length - 1].key).toBe('alpha_item'); // Oldest
350
+ });
351
+ it('should sort by created_at ascending', () => {
352
+ const results = db
353
+ .prepare(`
354
+ SELECT * FROM context_items
355
+ WHERE session_id = ?
356
+ ORDER BY created_at ASC
357
+ `)
358
+ .all(testSessionId);
359
+ expect(results[0].key).toBe('alpha_item'); // Oldest
360
+ expect(results[results.length - 1].key).toBe('charlie_item'); // Most recent
361
+ });
362
+ it('should sort by updated_at descending', () => {
363
+ const results = db
364
+ .prepare(`
365
+ SELECT * FROM context_items
366
+ WHERE session_id = ?
367
+ ORDER BY updated_at DESC
368
+ `)
369
+ .all(testSessionId);
370
+ expect(results[0].key).toBe('alpha_item'); // Most recently updated
371
+ expect(results[results.length - 1].key).toBe('charlie_item'); // Least recently updated
372
+ });
373
+ it('should sort by key ascending', () => {
374
+ const results = db
375
+ .prepare(`
376
+ SELECT * FROM context_items
377
+ WHERE session_id = ?
378
+ ORDER BY key ASC
379
+ `)
380
+ .all(testSessionId);
381
+ expect(results[0].key).toBe('alpha_item');
382
+ expect(results[1].key).toBe('beta_item');
383
+ expect(results[2].key).toBe('charlie_item');
384
+ });
385
+ it('should sort by key descending', () => {
386
+ const results = db
387
+ .prepare(`
388
+ SELECT * FROM context_items
389
+ WHERE session_id = ?
390
+ ORDER BY key DESC
391
+ `)
392
+ .all(testSessionId);
393
+ expect(results[0].key).toBe('charlie_item');
394
+ expect(results[1].key).toBe('beta_item');
395
+ expect(results[2].key).toBe('alpha_item');
396
+ });
397
+ });
398
+ describe('Metadata and Size', () => {
399
+ beforeEach(() => {
400
+ const items = [
401
+ {
402
+ key: 'small_item',
403
+ value: 'Small content',
404
+ metadata: JSON.stringify({ tags: ['small', 'test'] }),
405
+ },
406
+ {
407
+ key: 'large_item',
408
+ value: 'A'.repeat(1000), // Large content
409
+ metadata: JSON.stringify({ tags: ['large', 'performance'] }),
410
+ },
411
+ {
412
+ key: 'no_metadata',
413
+ value: 'Item without metadata',
414
+ metadata: null,
415
+ },
416
+ ];
417
+ items.forEach(item => {
418
+ const size = Buffer.byteLength(item.value, 'utf8');
419
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, metadata, size) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.metadata, size);
420
+ });
421
+ });
422
+ it('should include metadata when requested', () => {
423
+ const results = db
424
+ .prepare(`
425
+ SELECT *, size FROM context_items
426
+ WHERE session_id = ?
427
+ `)
428
+ .all(testSessionId);
429
+ results.forEach((item) => {
430
+ if (item.metadata) {
431
+ const parsed = JSON.parse(item.metadata);
432
+ expect(parsed).toHaveProperty('tags');
433
+ expect(Array.isArray(parsed.tags)).toBe(true);
434
+ }
435
+ });
436
+ });
437
+ it('should include size information', () => {
438
+ const results = db
439
+ .prepare(`
440
+ SELECT *, size FROM context_items
441
+ WHERE session_id = ?
442
+ `)
443
+ .all(testSessionId);
444
+ const smallItem = results.find((r) => r.key === 'small_item');
445
+ const largeItem = results.find((r) => r.key === 'large_item');
446
+ expect(smallItem.size).toBeLessThan(100);
447
+ expect(largeItem.size).toBeGreaterThan(900);
448
+ });
449
+ it('should calculate size if not stored', () => {
450
+ // For items without stored size, it should be calculated
451
+ const results = db
452
+ .prepare(`
453
+ SELECT *,
454
+ CASE
455
+ WHEN size IS NULL THEN LENGTH(value)
456
+ ELSE size
457
+ END as calculated_size
458
+ FROM context_items
459
+ WHERE session_id = ?
460
+ `)
461
+ .all(testSessionId);
462
+ results.forEach((item) => {
463
+ expect(item.calculated_size).toBeGreaterThan(0);
464
+ });
465
+ });
466
+ });
467
+ describe('Pagination', () => {
468
+ beforeEach(() => {
469
+ // Create 50 items for pagination testing
470
+ for (let i = 0; i < 50; i++) {
471
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, priority) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(2, '0')}`, `Value for item ${i}`, i % 3 === 0 ? 'high' : 'normal');
472
+ }
473
+ });
474
+ it('should limit results', () => {
475
+ const results = db
476
+ .prepare(`
477
+ SELECT * FROM context_items
478
+ WHERE session_id = ?
479
+ ORDER BY key ASC
480
+ LIMIT 10
481
+ `)
482
+ .all(testSessionId);
483
+ expect(results).toHaveLength(10);
484
+ expect(results[0].key).toBe('item_00');
485
+ expect(results[9].key).toBe('item_09');
486
+ });
487
+ it('should support offset for pagination', () => {
488
+ const results = db
489
+ .prepare(`
490
+ SELECT * FROM context_items
491
+ WHERE session_id = ?
492
+ ORDER BY key ASC
493
+ LIMIT 10 OFFSET 20
494
+ `)
495
+ .all(testSessionId);
496
+ expect(results).toHaveLength(10);
497
+ expect(results[0].key).toBe('item_20');
498
+ expect(results[9].key).toBe('item_29');
499
+ });
500
+ it('should return total count for pagination', () => {
501
+ // First get total count
502
+ const countResult = db
503
+ .prepare(`
504
+ SELECT COUNT(*) as count FROM context_items
505
+ WHERE session_id = ?
506
+ `)
507
+ .get(testSessionId);
508
+ expect(countResult.count).toBe(50);
509
+ // Then get paginated results
510
+ const results = db
511
+ .prepare(`
512
+ SELECT * FROM context_items
513
+ WHERE session_id = ?
514
+ ORDER BY key ASC
515
+ LIMIT 10 OFFSET 10
516
+ `)
517
+ .all(testSessionId);
518
+ expect(results).toHaveLength(10);
519
+ expect(results[0].key).toBe('item_10');
520
+ });
521
+ });
522
+ describe('Key Pattern Matching', () => {
523
+ beforeEach(() => {
524
+ const items = [
525
+ { key: 'auth_login', value: 'Login functionality' },
526
+ { key: 'auth_logout', value: 'Logout functionality' },
527
+ { key: 'auth_register', value: 'Registration functionality' },
528
+ { key: 'user_profile', value: 'User profile page' },
529
+ { key: 'user_settings', value: 'User settings page' },
530
+ { key: 'admin_dashboard', value: 'Admin dashboard' },
531
+ { key: 'test_auth_unit', value: 'Unit tests for auth' },
532
+ ];
533
+ items.forEach(item => {
534
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value);
535
+ });
536
+ });
537
+ it('should match keys with glob pattern', () => {
538
+ // SQLite GLOB pattern for keys starting with 'auth_'
539
+ const results = db
540
+ .prepare(`
541
+ SELECT * FROM context_items
542
+ WHERE session_id = ? AND key GLOB 'auth_*'
543
+ ORDER BY key ASC
544
+ `)
545
+ .all(testSessionId);
546
+ expect(results).toHaveLength(3);
547
+ expect(results.map((r) => r.key)).toEqual([
548
+ 'auth_login',
549
+ 'auth_logout',
550
+ 'auth_register',
551
+ ]);
552
+ });
553
+ it('should match keys ending with pattern', () => {
554
+ // Keys ending with '_settings'
555
+ const results = db
556
+ .prepare(`
557
+ SELECT * FROM context_items
558
+ WHERE session_id = ? AND key GLOB '*_settings'
559
+ ORDER BY key ASC
560
+ `)
561
+ .all(testSessionId);
562
+ expect(results).toHaveLength(1);
563
+ expect(results[0].key).toBe('user_settings');
564
+ });
565
+ it('should match keys containing pattern', () => {
566
+ // Keys containing 'auth'
567
+ const results = db
568
+ .prepare(`
569
+ SELECT * FROM context_items
570
+ WHERE session_id = ? AND key GLOB '*auth*'
571
+ ORDER BY key ASC
572
+ `)
573
+ .all(testSessionId);
574
+ expect(results).toHaveLength(4); // auth_login, auth_logout, auth_register, test_auth_unit
575
+ });
576
+ });
577
+ describe('Priority Filtering', () => {
578
+ beforeEach(() => {
579
+ const items = [
580
+ { key: 'critical_bug', value: 'Critical security issue', priority: 'critical' },
581
+ { key: 'high_task1', value: 'Important feature', priority: 'high' },
582
+ { key: 'high_task2', value: 'Important bugfix', priority: 'high' },
583
+ { key: 'normal_task1', value: 'Regular task', priority: 'normal' },
584
+ { key: 'normal_task2', value: 'Another regular task', priority: 'normal' },
585
+ { key: 'low_task', value: 'Nice to have', priority: 'low' },
586
+ ];
587
+ items.forEach(item => {
588
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, priority) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.priority);
589
+ });
590
+ });
591
+ it('should filter by single priority', () => {
592
+ const results = db
593
+ .prepare(`
594
+ SELECT * FROM context_items
595
+ WHERE session_id = ? AND priority = ?
596
+ ORDER BY created_at DESC
597
+ `)
598
+ .all(testSessionId, 'high');
599
+ expect(results).toHaveLength(2);
600
+ expect(results.map((r) => r.key)).toContain('high_task1');
601
+ expect(results.map((r) => r.key)).toContain('high_task2');
602
+ });
603
+ it('should filter by multiple priorities', () => {
604
+ const priorities = ['critical', 'high'];
605
+ const placeholders = priorities.map(() => '?').join(',');
606
+ const results = db
607
+ .prepare(`
608
+ SELECT * FROM context_items
609
+ WHERE session_id = ? AND priority IN (${placeholders})
610
+ ORDER BY
611
+ CASE priority
612
+ WHEN 'critical' THEN 1
613
+ WHEN 'high' THEN 2
614
+ WHEN 'normal' THEN 3
615
+ WHEN 'low' THEN 4
616
+ END
617
+ `)
618
+ .all(testSessionId, ...priorities);
619
+ expect(results).toHaveLength(3);
620
+ expect(results[0].key).toBe('critical_bug'); // Critical comes first
621
+ expect(results.slice(1).map((r) => r.priority)).toEqual(['high', 'high']);
622
+ });
623
+ });
624
+ describe('Privacy and Session Boundaries', () => {
625
+ beforeEach(() => {
626
+ // Add items to main session
627
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'public_item', 'Public content', 0);
628
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'private_item', 'Private content', 1);
629
+ // Add items to other session
630
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_public', 'Other public content', 0);
631
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_private', 'Other private content', 1);
632
+ });
633
+ it('should only show private items to owner session', () => {
634
+ // Search from main session - should see own private items
635
+ const mainResults = db
636
+ .prepare(`
637
+ SELECT * FROM context_items
638
+ WHERE (key LIKE ? OR value LIKE ?)
639
+ AND (is_private = 0 OR session_id = ?)
640
+ ORDER BY created_at DESC
641
+ `)
642
+ .all('%content%', '%content%', testSessionId);
643
+ expect(mainResults.map((r) => r.key)).toContain('public_item');
644
+ expect(mainResults.map((r) => r.key)).toContain('private_item');
645
+ expect(mainResults.map((r) => r.key)).toContain('other_public');
646
+ expect(mainResults.map((r) => r.key)).not.toContain('other_private');
647
+ });
648
+ it('should not show other sessions private items', () => {
649
+ // Search from other session - should not see main session's private items
650
+ const otherResults = db
651
+ .prepare(`
652
+ SELECT * FROM context_items
653
+ WHERE (key LIKE ? OR value LIKE ?)
654
+ AND (is_private = 0 OR session_id = ?)
655
+ ORDER BY created_at DESC
656
+ `)
657
+ .all('%content%', '%content%', otherSessionId);
658
+ expect(otherResults.map((r) => r.key)).toContain('public_item');
659
+ expect(otherResults.map((r) => r.key)).not.toContain('private_item');
660
+ expect(otherResults.map((r) => r.key)).toContain('other_public');
661
+ expect(otherResults.map((r) => r.key)).toContain('other_private');
662
+ });
663
+ });
664
+ describe('Combined Filters', () => {
665
+ beforeEach(() => {
666
+ const now = new Date();
667
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
668
+ const items = [
669
+ {
670
+ key: 'auth_recent_high',
671
+ value: 'Recent high priority auth task',
672
+ category: 'task',
673
+ priority: 'high',
674
+ channel: 'feature/auth',
675
+ created_at: now.toISOString(),
676
+ },
677
+ {
678
+ key: 'auth_old_normal',
679
+ value: 'Old normal priority auth task',
680
+ category: 'task',
681
+ priority: 'normal',
682
+ channel: 'feature/auth',
683
+ created_at: yesterday.toISOString(),
684
+ },
685
+ {
686
+ key: 'db_recent_high',
687
+ value: 'Recent high priority database task',
688
+ category: 'task',
689
+ priority: 'high',
690
+ channel: 'main',
691
+ created_at: now.toISOString(),
692
+ },
693
+ {
694
+ key: 'ui_recent_normal',
695
+ value: 'Recent normal priority UI task',
696
+ category: 'task',
697
+ priority: 'normal',
698
+ channel: 'feature/ui',
699
+ created_at: now.toISOString(),
700
+ },
701
+ ];
702
+ items.forEach(item => {
703
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, channel, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, item.channel, item.created_at);
704
+ });
705
+ });
706
+ it('should combine search query with time and priority filters', () => {
707
+ const oneDayAgo = new Date();
708
+ oneDayAgo.setDate(oneDayAgo.getDate() - 1);
709
+ oneDayAgo.setHours(oneDayAgo.getHours() - 1); // A bit more than 24 hours ago
710
+ const results = db
711
+ .prepare(`
712
+ SELECT * FROM context_items
713
+ WHERE session_id = ?
714
+ AND (key LIKE ? OR value LIKE ?)
715
+ AND priority = ?
716
+ AND created_at > ?
717
+ ORDER BY created_at DESC
718
+ `)
719
+ .all(testSessionId, '%auth%', '%auth%', 'high', oneDayAgo.toISOString());
720
+ expect(results).toHaveLength(1);
721
+ expect(results[0].key).toBe('auth_recent_high');
722
+ });
723
+ it('should combine channel and category filters', () => {
724
+ const results = db
725
+ .prepare(`
726
+ SELECT * FROM context_items
727
+ WHERE session_id = ?
728
+ AND channel = ?
729
+ AND category = ?
730
+ ORDER BY priority DESC, created_at DESC
731
+ `)
732
+ .all(testSessionId, 'feature/auth', 'task');
733
+ expect(results).toHaveLength(2);
734
+ expect(results.map((r) => r.key)).toContain('auth_recent_high');
735
+ expect(results.map((r) => r.key)).toContain('auth_old_normal');
736
+ });
737
+ it('should handle complex multi-filter queries', () => {
738
+ const channels = ['feature/auth', 'main'];
739
+ const priorities = ['high'];
740
+ const oneDayAgo = new Date();
741
+ oneDayAgo.setDate(oneDayAgo.getDate() - 1);
742
+ oneDayAgo.setHours(oneDayAgo.getHours() - 1);
743
+ const channelPlaceholders = channels.map(() => '?').join(',');
744
+ const priorityPlaceholders = priorities.map(() => '?').join(',');
745
+ const results = db
746
+ .prepare(`
747
+ SELECT * FROM context_items
748
+ WHERE session_id = ?
749
+ AND channel IN (${channelPlaceholders})
750
+ AND priority IN (${priorityPlaceholders})
751
+ AND created_at > ?
752
+ AND category = ?
753
+ ORDER BY created_at DESC
754
+ `)
755
+ .all(testSessionId, ...channels, ...priorities, oneDayAgo.toISOString(), 'task');
756
+ expect(results).toHaveLength(2);
757
+ expect(results.map((r) => r.key).sort()).toEqual(['auth_recent_high', 'db_recent_high']);
758
+ });
759
+ });
760
+ describe('Performance with Large Dataset', () => {
761
+ beforeEach(() => {
762
+ // Create 1000+ items for performance testing
763
+ const channels = ['main', 'feature/auth', 'feature/ui', 'hotfix/security', 'develop'];
764
+ const categories = ['task', 'decision', 'note', 'error', 'warning'];
765
+ const priorities = ['critical', 'high', 'normal', 'low'];
766
+ for (let i = 0; i < 1000; i++) {
767
+ const channel = channels[i % channels.length];
768
+ const category = categories[i % categories.length];
769
+ const priority = priorities[i % priorities.length];
770
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, channel) VALUES (?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(4, '0')}`, `This is the value for item ${i} with some searchable content like auth, database, api, etc.`, category, priority, channel);
771
+ }
772
+ });
773
+ it('should search efficiently with 1000+ items', () => {
774
+ const start = Date.now();
775
+ const results = db
776
+ .prepare(`
777
+ SELECT * FROM context_items
778
+ WHERE session_id = ?
779
+ AND (key LIKE ? OR value LIKE ?)
780
+ ORDER BY priority DESC, created_at DESC
781
+ LIMIT 50
782
+ `)
783
+ .all(testSessionId, '%auth%', '%auth%');
784
+ const duration = Date.now() - start;
785
+ expect(results).toHaveLength(50);
786
+ expect(duration).toBeLessThan(100); // Should complete within 100ms
787
+ });
788
+ it('should paginate efficiently through large result sets', () => {
789
+ const start = Date.now();
790
+ // Get page 3 (offset 100, limit 50)
791
+ const results = db
792
+ .prepare(`
793
+ SELECT * FROM context_items
794
+ WHERE session_id = ?
795
+ AND category = ?
796
+ ORDER BY created_at DESC
797
+ LIMIT 50 OFFSET 100
798
+ `)
799
+ .all(testSessionId, 'task');
800
+ const duration = Date.now() - start;
801
+ expect(results).toHaveLength(50);
802
+ expect(duration).toBeLessThan(50); // Pagination should be very fast
803
+ });
804
+ it('should efficiently filter by multiple criteria', () => {
805
+ const start = Date.now();
806
+ const channels = ['feature/auth', 'feature/ui'];
807
+ const priorities = ['high', 'critical'];
808
+ const channelPlaceholders = channels.map(() => '?').join(',');
809
+ const priorityPlaceholders = priorities.map(() => '?').join(',');
810
+ const _results = db
811
+ .prepare(`
812
+ SELECT * FROM context_items
813
+ WHERE session_id = ?
814
+ AND channel IN (${channelPlaceholders})
815
+ AND priority IN (${priorityPlaceholders})
816
+ AND value LIKE ?
817
+ ORDER BY priority DESC, created_at DESC
818
+ LIMIT 20
819
+ `)
820
+ .all(testSessionId, ...channels, ...priorities, '%database%');
821
+ const duration = Date.now() - start;
822
+ expect(duration).toBeLessThan(100);
823
+ });
824
+ it('should count total results efficiently', () => {
825
+ const start = Date.now();
826
+ const countResult = db
827
+ .prepare(`
828
+ SELECT COUNT(*) as count FROM context_items
829
+ WHERE session_id = ?
830
+ AND (key LIKE ? OR value LIKE ?)
831
+ `)
832
+ .get(testSessionId, '%api%', '%api%');
833
+ const duration = Date.now() - start;
834
+ expect(countResult.count).toBeGreaterThan(0);
835
+ expect(duration).toBeLessThan(50); // Count should be very fast
836
+ });
837
+ });
838
+ describe('Edge Cases', () => {
839
+ it('should handle empty search results gracefully', () => {
840
+ const results = db
841
+ .prepare(`
842
+ SELECT * FROM context_items
843
+ WHERE session_id = ?
844
+ AND (key LIKE ? OR value LIKE ?)
845
+ `)
846
+ .all(testSessionId, '%nonexistent%', '%nonexistent%');
847
+ expect(results).toHaveLength(0);
848
+ });
849
+ it('should handle invalid date formats', () => {
850
+ // Should not throw error with invalid date
851
+ const results = db
852
+ .prepare(`
853
+ SELECT * FROM context_items
854
+ WHERE session_id = ?
855
+ AND created_at > ?
856
+ `)
857
+ .all(testSessionId, 'invalid-date');
858
+ // SQLite will handle invalid dates gracefully
859
+ expect(Array.isArray(results)).toBe(true);
860
+ });
861
+ it('should handle special characters in search queries', () => {
862
+ // Add item with special characters
863
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'special_chars', 'Value with % and _ special chars');
864
+ // Search for literal % character (need to escape in LIKE)
865
+ const results = db
866
+ .prepare(`
867
+ SELECT * FROM context_items
868
+ WHERE session_id = ?
869
+ AND value LIKE ? ESCAPE '\\'
870
+ `)
871
+ .all(testSessionId, '%\\%%');
872
+ expect(results.some((r) => r.key === 'special_chars')).toBe(true);
873
+ });
874
+ it('should handle null values in optional fields', () => {
875
+ // Add items with null values
876
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'null_fields', 'Item with nulls', null, null, null);
877
+ const results = db
878
+ .prepare(`
879
+ SELECT * FROM context_items
880
+ WHERE session_id = ?
881
+ AND category IS NULL
882
+ `)
883
+ .all(testSessionId);
884
+ expect(results.some((r) => r.key === 'null_fields')).toBe(true);
885
+ });
886
+ it('should handle very long search queries', () => {
887
+ const longQuery = 'a'.repeat(1000);
888
+ const results = db
889
+ .prepare(`
890
+ SELECT * FROM context_items
891
+ WHERE session_id = ?
892
+ AND (key LIKE ? OR value LIKE ?)
893
+ `)
894
+ .all(testSessionId, `%${longQuery}%`, `%${longQuery}%`);
895
+ expect(results).toHaveLength(0);
896
+ });
897
+ });
898
+ describe('SearchIn Parameter Behavior', () => {
899
+ beforeEach(() => {
900
+ // Add test data with specific patterns
901
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'auth_key_only', 'This value does not contain the search term');
902
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'normal_key', 'This value contains auth in the content');
903
+ });
904
+ it('should search in both key and value by default', () => {
905
+ const results = db
906
+ .prepare(`
907
+ SELECT * FROM context_items
908
+ WHERE session_id = ?
909
+ AND (key LIKE ? OR value LIKE ?)
910
+ `)
911
+ .all(testSessionId, '%auth%', '%auth%');
912
+ expect(results).toHaveLength(2);
913
+ expect(results.map((r) => r.key)).toContain('auth_key_only');
914
+ expect(results.map((r) => r.key)).toContain('normal_key');
915
+ });
916
+ it('should search only in keys when searchIn = ["key"]', () => {
917
+ const results = db
918
+ .prepare(`
919
+ SELECT * FROM context_items
920
+ WHERE session_id = ?
921
+ AND key LIKE ?
922
+ `)
923
+ .all(testSessionId, '%auth%');
924
+ expect(results).toHaveLength(1);
925
+ expect(results[0].key).toBe('auth_key_only');
926
+ });
927
+ it('should search only in values when searchIn = ["value"]', () => {
928
+ const results = db
929
+ .prepare(`
930
+ SELECT * FROM context_items
931
+ WHERE session_id = ?
932
+ AND value LIKE ?
933
+ `)
934
+ .all(testSessionId, '%auth%');
935
+ expect(results).toHaveLength(1);
936
+ expect(results[0].key).toBe('normal_key');
937
+ });
938
+ });
939
+ describe('Multi-word AND/OR Search (matchMode)', () => {
940
+ let contextRepo;
941
+ let sessionId;
942
+ beforeEach(() => {
943
+ contextRepo = new ContextRepository_1.ContextRepository(dbManager);
944
+ sessionId = (0, uuid_1.v4)();
945
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'matchMode test');
946
+ // Item: XTAR appears in key, 结汇人 appears in value
947
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'XTAR_account', '结汇人 transaction record');
948
+ // Item: both words in value
949
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'trade_record', 'XTAR 结汇人 settlement');
950
+ // Item: only XTAR
951
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'xtar_only', 'XTAR trading platform');
952
+ // Item: only 结汇人
953
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'jiehuiren_only', '结汇人 data point');
954
+ });
955
+ it('single-word query is backward-compatible', () => {
956
+ const { items } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId });
957
+ expect(items.length).toBe(3);
958
+ expect(items.map(i => i.key)).toEqual(expect.arrayContaining(['XTAR_account', 'trade_record', 'xtar_only']));
959
+ });
960
+ it('multi-word AND (default) requires all terms to match', () => {
961
+ const { items } = contextRepo.searchEnhanced({ query: 'XTAR 结汇人', sessionId });
962
+ expect(items.length).toBe(2);
963
+ expect(items.map(i => i.key)).toEqual(expect.arrayContaining(['XTAR_account', 'trade_record']));
964
+ });
965
+ it('multi-word AND with matchMode:"and" is explicit equivalent', () => {
966
+ const { items } = contextRepo.searchEnhanced({
967
+ query: 'XTAR 结汇人',
968
+ sessionId,
969
+ matchMode: 'and',
970
+ });
971
+ expect(items.length).toBe(2);
972
+ });
973
+ it('multi-word OR returns items matching any term', () => {
974
+ const { items } = contextRepo.searchEnhanced({
975
+ query: 'XTAR 结汇人',
976
+ sessionId,
977
+ matchMode: 'or',
978
+ });
979
+ expect(items.length).toBe(4);
980
+ });
981
+ it('AND with a term that has no match returns nothing', () => {
982
+ const { items } = contextRepo.searchEnhanced({ query: 'XTAR nonexistent_xyz', sessionId });
983
+ expect(items.length).toBe(0);
984
+ });
985
+ it('searchAcrossSessionsEnhanced also supports matchMode', () => {
986
+ const { items: andItems } = contextRepo.searchAcrossSessionsEnhanced({
987
+ query: 'XTAR 结汇人',
988
+ currentSessionId: sessionId,
989
+ matchMode: 'and',
990
+ });
991
+ expect(andItems.length).toBe(2);
992
+ const { items: orItems } = contextRepo.searchAcrossSessionsEnhanced({
993
+ query: 'XTAR 结汇人',
994
+ currentSessionId: sessionId,
995
+ matchMode: 'or',
996
+ });
997
+ expect(orItems.length).toBe(4);
998
+ });
999
+ });
1000
+ describe('FTS5 full-text search (useFts5)', () => {
1001
+ let contextRepo;
1002
+ let sessionId;
1003
+ let fts5Available;
1004
+ beforeEach(() => {
1005
+ contextRepo = new ContextRepository_1.ContextRepository(dbManager);
1006
+ sessionId = (0, uuid_1.v4)();
1007
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'fts5 test');
1008
+ // Check if FTS5 table was created by DatabaseManager
1009
+ const ftsTable = db
1010
+ .prepare("SELECT COUNT(*) as c FROM sqlite_master WHERE type='table' AND name='context_items_fts'")
1011
+ .get();
1012
+ fts5Available = ftsTable.c > 0;
1013
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'XTAR_account', 'settlement record for 结汇人');
1014
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'trade_log', 'XTAR platform trade entry');
1015
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'other_entry', 'unrelated content');
1016
+ });
1017
+ it('useFts5:true finds results for ≥3-char terms (or falls back to LIKE)', () => {
1018
+ const { items } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId, useFts5: true });
1019
+ // Both FTS5 and LIKE fallback should find the two XTAR items
1020
+ expect(items.length).toBe(2);
1021
+ });
1022
+ it('useFts5:true with short (<3-char) term auto-falls-back to LIKE', () => {
1023
+ // "汇" is 1 CJK char — below trigram minimum; should still work via LIKE fallback
1024
+ const { items } = contextRepo.searchEnhanced({ query: '汇', sessionId, useFts5: true });
1025
+ // LIKE fallback finds '结汇人' in the value column
1026
+ expect(items.length).toBeGreaterThanOrEqual(1);
1027
+ });
1028
+ it('useFts5:true with multi-word query finds AND intersection', () => {
1029
+ const { items } = contextRepo.searchEnhanced({
1030
+ query: 'XTAR settlement',
1031
+ sessionId,
1032
+ useFts5: true,
1033
+ });
1034
+ if (fts5Available) {
1035
+ // FTS5: both terms must appear → only XTAR_account
1036
+ expect(items.length).toBe(1);
1037
+ expect(items[0].key).toBe('XTAR_account');
1038
+ }
1039
+ else {
1040
+ // LIKE fallback also works
1041
+ expect(items.length).toBeGreaterThanOrEqual(0);
1042
+ }
1043
+ });
1044
+ it('useFts5:false behaves identically to LIKE search', () => {
1045
+ const { items: ftsOff } = contextRepo.searchEnhanced({
1046
+ query: 'XTAR',
1047
+ sessionId,
1048
+ useFts5: false,
1049
+ });
1050
+ const { items: likeSearch } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId });
1051
+ expect(ftsOff.length).toBe(likeSearch.length);
1052
+ });
1053
+ });
1054
+ });