mcp-memory-keeper 0.10.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,1082 @@
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 RepositoryManager_1 = require("../../repositories/RepositoryManager");
38
+ const database_test_helper_1 = require("../helpers/database-test-helper");
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const uuid_1 = require("uuid");
43
+ const timezone_safe_dates_1 = require("../utils/timezone-safe-dates");
44
+ describe('Enhanced Context Operations Integration Tests', () => {
45
+ let dbManager;
46
+ let repositories;
47
+ let tempDbPath;
48
+ let db;
49
+ let testHelper;
50
+ let testSessionId;
51
+ let testSessionId2;
52
+ beforeEach(() => {
53
+ tempDbPath = path.join(os.tmpdir(), `test-enhanced-context-${Date.now()}.db`);
54
+ dbManager = new database_1.DatabaseManager({
55
+ filename: tempDbPath,
56
+ maxSize: 10 * 1024 * 1024,
57
+ walMode: true,
58
+ });
59
+ db = dbManager.getDatabase();
60
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
61
+ testHelper = new database_test_helper_1.DatabaseTestHelper(db);
62
+ // Create test sessions
63
+ testSessionId = (0, uuid_1.v4)();
64
+ testSessionId2 = (0, uuid_1.v4)();
65
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session 1');
66
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId2, 'Test Session 2');
67
+ });
68
+ afterEach(() => {
69
+ dbManager.close();
70
+ try {
71
+ fs.unlinkSync(tempDbPath);
72
+ fs.unlinkSync(`${tempDbPath}-wal`);
73
+ fs.unlinkSync(`${tempDbPath}-shm`);
74
+ }
75
+ catch (_e) {
76
+ // Ignore
77
+ }
78
+ });
79
+ describe('Enhanced context_get', () => {
80
+ beforeEach(() => {
81
+ // Disable triggers to control timestamps precisely
82
+ testHelper.disableTimestampTriggers();
83
+ // Add test data with varying timestamps
84
+ const baseTime = new Date('2024-01-01T00:00:00Z');
85
+ const items = [
86
+ { key: 'alpha_item', value: 'First value', category: 'task', priority: 'high', offset: 0 },
87
+ {
88
+ key: 'beta_item',
89
+ value: 'Second value',
90
+ category: 'task',
91
+ priority: 'normal',
92
+ offset: 1,
93
+ },
94
+ {
95
+ key: 'gamma_item',
96
+ value: 'Third value',
97
+ category: 'decision',
98
+ priority: 'high',
99
+ offset: 2,
100
+ },
101
+ {
102
+ key: 'delta_item',
103
+ value: 'Fourth value with much longer content to test size calculation',
104
+ category: 'note',
105
+ priority: 'low',
106
+ offset: 3,
107
+ },
108
+ {
109
+ key: 'epsilon_item',
110
+ value: 'Fifth value',
111
+ category: 'progress',
112
+ priority: 'normal',
113
+ offset: 4,
114
+ },
115
+ { key: 'zeta_item', value: 'Sixth value', category: 'task', priority: 'high', offset: 5 },
116
+ { key: 'eta_item', value: 'Seventh value', category: 'error', priority: 'high', offset: 6 },
117
+ {
118
+ key: 'theta_item',
119
+ value: 'Eighth value',
120
+ category: 'warning',
121
+ priority: 'normal',
122
+ offset: 7,
123
+ },
124
+ { key: 'iota_item', value: 'Ninth value', category: 'task', priority: 'low', offset: 8 },
125
+ { key: 'kappa_item', value: 'Tenth value', category: 'note', priority: 'high', offset: 9 },
126
+ ];
127
+ items.forEach(item => {
128
+ const createdAt = new Date(baseTime.getTime() + item.offset * 3600000); // 1 hour intervals
129
+ const updatedAt = new Date(createdAt.getTime() + 1800000); // 30 minutes later
130
+ db.prepare(`
131
+ INSERT INTO context_items (id, session_id, key, value, category, priority, created_at, updated_at)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
133
+ `).run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, createdAt.toISOString(), updatedAt.toISOString());
134
+ });
135
+ // Add some items to second session for multi-session tests
136
+ db.prepare(`
137
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
138
+ VALUES (?, ?, ?, ?, ?, ?)
139
+ `).run((0, uuid_1.v4)(), testSessionId2, 'session2_item', 'Another session value', 'task', 'normal');
140
+ // Re-enable triggers
141
+ testHelper.enableTimestampTriggers();
142
+ });
143
+ describe('includeMetadata parameter', () => {
144
+ it('should return items without metadata by default', () => {
145
+ const items = db
146
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
147
+ .all(testSessionId);
148
+ // Simulate response without metadata
149
+ const response = items.map(item => ({
150
+ key: item.key,
151
+ value: item.value,
152
+ category: item.category,
153
+ priority: item.priority,
154
+ }));
155
+ expect(response).toHaveLength(10);
156
+ expect(response[0]).not.toHaveProperty('created_at');
157
+ expect(response[0]).not.toHaveProperty('updated_at');
158
+ expect(response[0]).not.toHaveProperty('size');
159
+ expect(response[0]).not.toHaveProperty('session_info');
160
+ });
161
+ it('should include metadata when includeMetadata is true', () => {
162
+ const items = db
163
+ .prepare(`
164
+ SELECT ci.*, s.name as session_name, s.description as session_description
165
+ FROM context_items ci
166
+ JOIN sessions s ON ci.session_id = s.id
167
+ WHERE ci.session_id = ?
168
+ `)
169
+ .all(testSessionId);
170
+ // Simulate response with metadata
171
+ const response = items.map(item => ({
172
+ key: item.key,
173
+ value: item.value,
174
+ category: item.category,
175
+ priority: item.priority,
176
+ metadata: {
177
+ created_at: item.created_at,
178
+ updated_at: item.updated_at,
179
+ size: Buffer.byteLength(item.value, 'utf8'),
180
+ session_info: {
181
+ id: item.session_id,
182
+ name: item.session_name,
183
+ description: item.session_description,
184
+ },
185
+ },
186
+ }));
187
+ expect(response).toHaveLength(10);
188
+ expect(response[0].metadata).toBeDefined();
189
+ expect(response[0].metadata.created_at).toBeDefined();
190
+ expect(response[0].metadata.updated_at).toBeDefined();
191
+ expect(response[0].metadata.size).toBeGreaterThan(0);
192
+ expect(response[0].metadata.session_info.name).toBe('Test Session 1');
193
+ });
194
+ it('should calculate correct size for items with metadata', () => {
195
+ const item = db
196
+ .prepare('SELECT * FROM context_items WHERE key = ?')
197
+ .get('delta_item');
198
+ const size = Buffer.byteLength(item.value, 'utf8');
199
+ expect(size).toBe(62); // Byte length of "Fourth value with much longer content to test size calculation"
200
+ });
201
+ });
202
+ describe('sort parameter', () => {
203
+ it('should sort by created_at descending (newest first)', () => {
204
+ const items = db
205
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
206
+ .all(testSessionId);
207
+ expect(items[0].key).toBe('kappa_item'); // Last created
208
+ expect(items[9].key).toBe('alpha_item'); // First created
209
+ });
210
+ it('should sort by created_at ascending (oldest first)', () => {
211
+ const items = db
212
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at ASC')
213
+ .all(testSessionId);
214
+ expect(items[0].key).toBe('alpha_item'); // First created
215
+ expect(items[9].key).toBe('kappa_item'); // Last created
216
+ });
217
+ it('should sort by updated_at descending', () => {
218
+ // Disable triggers to control timestamps precisely
219
+ testHelper.disableTimestampTriggers();
220
+ // Update a specific item to have a more recent updated_at
221
+ const recentUpdate = new Date();
222
+ db.prepare('UPDATE context_items SET updated_at = ? WHERE key = ?').run(recentUpdate.toISOString(), 'beta_item');
223
+ const items = db
224
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY updated_at DESC')
225
+ .all(testSessionId);
226
+ // Re-enable triggers
227
+ testHelper.enableTimestampTriggers();
228
+ expect(items[0].key).toBe('beta_item'); // Most recently updated
229
+ });
230
+ it('should sort by key ascending', () => {
231
+ const items = db
232
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC')
233
+ .all(testSessionId);
234
+ expect(items[0].key).toBe('alpha_item');
235
+ expect(items[1].key).toBe('beta_item');
236
+ expect(items[9].key).toBe('zeta_item');
237
+ });
238
+ it('should sort by key descending', () => {
239
+ const items = db
240
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key DESC')
241
+ .all(testSessionId);
242
+ expect(items[0].key).toBe('zeta_item');
243
+ expect(items[9].key).toBe('alpha_item');
244
+ });
245
+ it('should default to created_at DESC when sort is not specified', () => {
246
+ const items = db
247
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
248
+ .all(testSessionId);
249
+ expect(items[0].key).toBe('kappa_item');
250
+ });
251
+ });
252
+ describe('limit and offset parameters', () => {
253
+ it('should return limited number of items', () => {
254
+ const result = repositories.contexts.queryEnhanced({
255
+ sessionId: testSessionId,
256
+ sort: 'key_asc',
257
+ limit: 5,
258
+ });
259
+ expect(result.items).toHaveLength(5);
260
+ expect(result.items[0].key).toBe('alpha_item');
261
+ expect(result.items[4].key).toBe('eta_item'); // 5th item alphabetically
262
+ });
263
+ it('should apply offset for pagination', () => {
264
+ const result = repositories.contexts.queryEnhanced({
265
+ sessionId: testSessionId,
266
+ sort: 'key_asc',
267
+ limit: 5,
268
+ offset: 5,
269
+ });
270
+ expect(result.items).toHaveLength(5);
271
+ expect(result.items[0].key).toBe('gamma_item'); // 6th item alphabetically (offset 5)
272
+ expect(result.items[4].key).toBe('theta_item'); // Last item in the result set
273
+ });
274
+ it('should handle limit larger than available items', () => {
275
+ const items = db
276
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 20')
277
+ .all(testSessionId);
278
+ expect(items).toHaveLength(10); // Only 10 items available
279
+ });
280
+ it('should handle offset beyond available items', () => {
281
+ const items = db
282
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 5 OFFSET 15')
283
+ .all(testSessionId);
284
+ expect(items).toHaveLength(0);
285
+ });
286
+ it('should return total count for pagination info', () => {
287
+ const totalCount = db
288
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
289
+ .get(testSessionId);
290
+ const items = db
291
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 5')
292
+ .all(testSessionId);
293
+ expect(totalCount.count).toBe(10);
294
+ expect(items).toHaveLength(5);
295
+ });
296
+ });
297
+ describe('createdAfter and createdBefore parameters', () => {
298
+ it('should filter items created after a specific date', () => {
299
+ const afterDate = new Date('2024-01-01T03:00:00Z'); // After first 3 items
300
+ const result = repositories.contexts.queryEnhanced({
301
+ sessionId: testSessionId,
302
+ createdAfter: afterDate.toISOString(),
303
+ sort: 'created_at_asc',
304
+ });
305
+ expect(result.items).toHaveLength(7); // Items created after 03:00 (includes public items from other sessions)
306
+ expect(result.items[0].key).toBe('epsilon_item'); // First item after 03:00 is at 04:00
307
+ });
308
+ it('should filter items created before a specific date', () => {
309
+ const beforeDate = new Date('2024-01-01T05:00:00Z'); // Before last 5 items
310
+ const items = db
311
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at < ? ORDER BY created_at ASC')
312
+ .all(testSessionId, beforeDate.toISOString());
313
+ expect(items).toHaveLength(5);
314
+ expect(items[4].key).toBe('epsilon_item');
315
+ });
316
+ it('should filter items within a date range', () => {
317
+ const afterDate = new Date('2024-01-01T02:00:00Z');
318
+ const beforeDate = new Date('2024-01-01T06:00:00Z');
319
+ const result = repositories.contexts.queryEnhanced({
320
+ sessionId: testSessionId,
321
+ createdAfter: afterDate.toISOString(),
322
+ createdBefore: beforeDate.toISOString(),
323
+ sort: 'created_at_asc',
324
+ });
325
+ expect(result.items).toHaveLength(3); // Items with 02:00 < created_at < 06:00
326
+ expect(result.items[0].key).toBe('delta_item'); // 03:00
327
+ expect(result.items[2].key).toBe('zeta_item'); // 05:00
328
+ });
329
+ it('should return empty when no items match date filters', () => {
330
+ const afterDate = new Date('2025-01-01T00:00:00Z'); // Future date
331
+ const items = db
332
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
333
+ .all(testSessionId, afterDate.toISOString());
334
+ expect(items).toHaveLength(0);
335
+ });
336
+ it('should handle invalid date formats gracefully', () => {
337
+ // This test expects the implementation to validate date formats
338
+ const invalidDate = 'not-a-date';
339
+ // The implementation should either throw an error or return all items
340
+ // For now, we'll test that it doesn't crash
341
+ expect(() => {
342
+ db.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?').all(testSessionId, invalidDate);
343
+ }).not.toThrow();
344
+ });
345
+ });
346
+ describe('keyPattern parameter', () => {
347
+ it('should match items by simple pattern', () => {
348
+ const items = db
349
+ .prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE '%_item' ORDER BY key ASC")
350
+ .all(testSessionId);
351
+ expect(items).toHaveLength(10); // All items end with '_item'
352
+ });
353
+ it('should match items by prefix pattern', () => {
354
+ // Simulate regex ^alpha.* as SQL LIKE
355
+ const items = db
356
+ .prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE 'alpha%' ORDER BY key ASC")
357
+ .all(testSessionId);
358
+ expect(items).toHaveLength(1);
359
+ expect(items[0].key).toBe('alpha_item');
360
+ });
361
+ it('should match items by complex pattern', () => {
362
+ // Simulate regex that matches Greek letter names
363
+ const items = db
364
+ .prepare("SELECT * FROM context_items WHERE session_id = ? AND (key LIKE 'alpha%' OR key LIKE 'beta%' OR key LIKE 'gamma%') ORDER BY key ASC")
365
+ .all(testSessionId);
366
+ expect(items).toHaveLength(3);
367
+ expect(items.map(i => i.key)).toEqual(['alpha_item', 'beta_item', 'gamma_item']);
368
+ });
369
+ it('should return empty when pattern matches nothing', () => {
370
+ const items = db
371
+ .prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE 'nonexistent%'")
372
+ .all(testSessionId);
373
+ expect(items).toHaveLength(0);
374
+ });
375
+ it('should handle special regex characters', () => {
376
+ // Add item with special characters in key
377
+ db.prepare(`
378
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
379
+ VALUES (?, ?, ?, ?, ?, ?)
380
+ `).run((0, uuid_1.v4)(), testSessionId, 'item.with.dots', 'Special value', 'note', 'normal');
381
+ // Pattern should escape dots when used literally
382
+ const items = db
383
+ .prepare("SELECT * FROM context_items WHERE session_id = ? AND key = 'item.with.dots'")
384
+ .all(testSessionId);
385
+ expect(items).toHaveLength(1);
386
+ expect(items[0].key).toBe('item.with.dots');
387
+ });
388
+ });
389
+ describe('priorities parameter', () => {
390
+ it('should filter by single priority', () => {
391
+ const items = db
392
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ? ORDER BY key ASC')
393
+ .all(testSessionId, 'high');
394
+ expect(items).toHaveLength(5);
395
+ expect(items.every(i => i.priority === 'high')).toBe(true);
396
+ });
397
+ it('should filter by multiple priorities', () => {
398
+ const items = db
399
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND priority IN (?, ?) ORDER BY key ASC')
400
+ .all(testSessionId, 'high', 'normal');
401
+ expect(items).toHaveLength(8); // 5 high + 3 normal
402
+ expect(items.every(i => ['high', 'normal'].includes(i.priority))).toBe(true);
403
+ });
404
+ it('should return empty when priorities array is empty', () => {
405
+ // Simulate empty priorities filter - should return all items
406
+ const items = db
407
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
408
+ .all(testSessionId);
409
+ expect(items).toHaveLength(10);
410
+ });
411
+ it('should handle invalid priority values', () => {
412
+ const items = db
413
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ?')
414
+ .all(testSessionId, 'invalid_priority');
415
+ expect(items).toHaveLength(0);
416
+ });
417
+ });
418
+ describe('Combining multiple parameters', () => {
419
+ it('should combine category filter with sort and limit', () => {
420
+ const items = db
421
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ? ORDER BY key DESC LIMIT 2')
422
+ .all(testSessionId, 'task');
423
+ expect(items).toHaveLength(2);
424
+ expect(items[0].key).toBe('zeta_item');
425
+ expect(items[1].key).toBe('iota_item');
426
+ expect(items.every(i => i.category === 'task')).toBe(true);
427
+ });
428
+ it('should combine date filters with priority and pagination', () => {
429
+ const afterDate = new Date('2024-01-01T01:00:00Z');
430
+ const beforeDate = new Date('2024-01-01T07:00:00Z');
431
+ const items = db
432
+ .prepare(`
433
+ SELECT * FROM context_items
434
+ WHERE session_id = ?
435
+ AND created_at > ?
436
+ AND created_at < ?
437
+ AND priority = ?
438
+ ORDER BY created_at ASC
439
+ LIMIT 3 OFFSET 1
440
+ `)
441
+ .all(testSessionId, afterDate.toISOString(), beforeDate.toISOString(), 'high');
442
+ expect(items).toHaveLength(2); // Only 3 high priority items in range, offset 1
443
+ });
444
+ it('should combine keyPattern with category and sort', () => {
445
+ // Pattern matching items starting with vowels (a, e, i)
446
+ const result = repositories.contexts.queryEnhanced({
447
+ sessionId: testSessionId,
448
+ keyPattern: '[aei]*', // SQLite GLOB pattern for starts with a, e, or i
449
+ category: 'task',
450
+ sort: 'key_asc',
451
+ });
452
+ expect(result.items).toHaveLength(2); // alpha (task), iota (task)
453
+ expect(result.items[0].key).toBe('alpha_item');
454
+ expect(result.items[1].key).toBe('iota_item');
455
+ });
456
+ it('should include metadata with all filters applied', () => {
457
+ const items = db
458
+ .prepare(`
459
+ SELECT ci.*, s.name as session_name, LENGTH(ci.value) as value_size
460
+ FROM context_items ci
461
+ JOIN sessions s ON ci.session_id = s.id
462
+ WHERE ci.session_id = ?
463
+ AND ci.priority IN (?, ?)
464
+ AND ci.category = ?
465
+ ORDER BY ci.created_at DESC
466
+ LIMIT 2
467
+ `)
468
+ .all(testSessionId, 'high', 'normal', 'task');
469
+ expect(items).toHaveLength(2);
470
+ expect(items[0]).toHaveProperty('value_size');
471
+ expect(items[0]).toHaveProperty('session_name');
472
+ expect(items[0].session_name).toBe('Test Session 1');
473
+ });
474
+ });
475
+ describe('Backward compatibility', () => {
476
+ it('should work with only key parameter as before', () => {
477
+ const item = db
478
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
479
+ .get(testSessionId, 'alpha_item');
480
+ expect(item).toBeDefined();
481
+ expect(item.value).toBe('First value');
482
+ });
483
+ it('should work with only category parameter as before', () => {
484
+ const items = db
485
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
486
+ .all(testSessionId, 'task');
487
+ expect(items).toHaveLength(4);
488
+ expect(items.every(i => i.category === 'task')).toBe(true);
489
+ });
490
+ it('should work with session_id parameter as before', () => {
491
+ const items = db
492
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
493
+ .all(testSessionId2);
494
+ expect(items).toHaveLength(1);
495
+ expect(items[0].key).toBe('session2_item');
496
+ });
497
+ it('should return empty array when no matches as before', () => {
498
+ const items = db
499
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
500
+ .all(testSessionId, 'nonexistent');
501
+ expect(items).toHaveLength(0);
502
+ });
503
+ });
504
+ describe('Edge cases', () => {
505
+ it('should handle empty session', () => {
506
+ const emptySessionId = (0, uuid_1.v4)();
507
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
508
+ const items = db
509
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
510
+ .all(emptySessionId);
511
+ expect(items).toHaveLength(0);
512
+ });
513
+ it('should handle very large limit values', () => {
514
+ const items = db
515
+ .prepare('SELECT * FROM context_items WHERE session_id = ? LIMIT 999999')
516
+ .all(testSessionId);
517
+ expect(items).toHaveLength(10); // Still returns only available items
518
+ });
519
+ it('should handle negative offset gracefully', () => {
520
+ // SQLite treats negative offset as 0
521
+ const items = db
522
+ .prepare('SELECT * FROM context_items WHERE session_id = ? LIMIT 5 OFFSET -5')
523
+ .all(testSessionId);
524
+ expect(items).toHaveLength(5);
525
+ });
526
+ it('should handle concurrent access with proper isolation', () => {
527
+ // Simulate concurrent reads
528
+ const promises = Array(5)
529
+ .fill(null)
530
+ .map(() => new Promise(resolve => {
531
+ const items = db
532
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
533
+ .all(testSessionId);
534
+ resolve(items.length);
535
+ }));
536
+ return Promise.all(promises).then(results => {
537
+ expect(results).toEqual([10, 10, 10, 10, 10]);
538
+ });
539
+ });
540
+ });
541
+ });
542
+ describe('Enhanced context_timeline', () => {
543
+ beforeEach(() => {
544
+ // TIMEZONE-SAFE: Use fixed UTC dates to ensure consistent behavior across all environments
545
+ (0, timezone_safe_dates_1.validateTimezoneSafety)();
546
+ const { today: _today, yesterday: _yesterday } = (0, timezone_safe_dates_1.createTimelineTestDates)();
547
+ // Create timezone-safe timestamps at specific hours
548
+ const yesterdayMorning = (0, timezone_safe_dates_1.createUTCDate)(-1, 10, 0, 0); // 10 AM yesterday UTC
549
+ const yesterdayAfternoon = (0, timezone_safe_dates_1.createUTCDate)(-1, 15, 0, 0); // 3 PM yesterday UTC
550
+ const todayEarlyMorning = (0, timezone_safe_dates_1.createUTCDate)(0, 5, 0, 0); // 5 AM today UTC
551
+ const todayEarlyMorning2 = (0, timezone_safe_dates_1.createUTCDate)(0, 7, 0, 0); // 7 AM today UTC
552
+ const todayMorning = (0, timezone_safe_dates_1.createUTCDate)(0, 9, 0, 0); // 9 AM today UTC
553
+ const todayAfternoon = (0, timezone_safe_dates_1.createUTCDate)(0, 14, 0, 0); // 2 PM today UTC
554
+ const timeOffsets = [
555
+ // Use timezone-safe absolute timestamps
556
+ { timestamp: todayMorning, key: 'recent_1', category: 'task' }, // 9 AM today
557
+ { timestamp: todayAfternoon, key: 'recent_2', category: 'note' }, // 2 PM today
558
+ { timestamp: todayEarlyMorning2, key: 'today_1', category: 'task' }, // 7 AM today
559
+ { timestamp: todayEarlyMorning, key: 'today_2', category: 'decision' }, // 5 AM today
560
+ { timestamp: yesterdayMorning, key: 'yesterday_1', category: 'task' }, // Yesterday 10 AM
561
+ { timestamp: yesterdayAfternoon, key: 'yesterday_2', category: 'note' }, // Yesterday 3 PM
562
+ { timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-72), key: 'days_ago_1', category: 'progress' }, // 3 days ago
563
+ { timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-168), key: 'week_ago_1', category: 'task' }, // 1 week ago
564
+ { timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-336), key: 'weeks_ago_1', category: 'error' }, // 2 weeks ago
565
+ ];
566
+ timeOffsets.forEach(({ timestamp, key, category }) => {
567
+ const createdAt = timestamp;
568
+ db.prepare(`
569
+ INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
570
+ VALUES (?, ?, ?, ?, ?, ?, ?)
571
+ `).run((0, uuid_1.v4)(), testSessionId, key, `Value for ${key}`, category, 'normal', createdAt.toISOString());
572
+ });
573
+ // Add journal entries
574
+ const journalEntries = [
575
+ { hours: -3, entry: 'Completed major refactoring', mood: 'accomplished' },
576
+ { hours: -24, entry: 'Started new feature branch', mood: 'excited' },
577
+ { hours: -48, entry: 'Fixed critical bug', mood: 'relieved' },
578
+ ];
579
+ journalEntries.forEach(({ hours, entry, mood }) => {
580
+ const createdAt = (0, timezone_safe_dates_1.createUTCDateByHours)(hours);
581
+ db.prepare(`
582
+ INSERT INTO journal_entries (id, session_id, entry, mood, created_at)
583
+ VALUES (?, ?, ?, ?, ?)
584
+ `).run((0, uuid_1.v4)(), testSessionId, entry, mood, createdAt.toISOString());
585
+ });
586
+ });
587
+ describe('Basic timeline functionality', () => {
588
+ it('should group items by time period', () => {
589
+ // TIMEZONE-SAFE: Use UTC-based date calculations
590
+ const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
591
+ const oneDayAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-24);
592
+ const oneWeekAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-7 * 24);
593
+ // Get items from different periods
594
+ const todayItems = db
595
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
596
+ .all(testSessionId, oneDayAgo.toISOString());
597
+ const thisWeekItems = db
598
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ? AND created_at <= ?')
599
+ .all(testSessionId, oneWeekAgo.toISOString(), oneDayAgo.toISOString());
600
+ expect(todayItems.length).toBeGreaterThan(0);
601
+ expect(thisWeekItems.length).toBeGreaterThan(0);
602
+ });
603
+ it('should order timeline entries by date descending', () => {
604
+ const items = db
605
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
606
+ .all(testSessionId);
607
+ // Verify ordering
608
+ for (let i = 1; i < items.length; i++) {
609
+ const prevDate = new Date(items[i - 1].created_at);
610
+ const currDate = new Date(items[i].created_at);
611
+ expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime());
612
+ }
613
+ });
614
+ });
615
+ describe('includeItems parameter', () => {
616
+ it('should return timeline without item details by default', () => {
617
+ // Simulate timeline response without items
618
+ const periods = db
619
+ .prepare(`
620
+ SELECT
621
+ DATE(created_at) as period,
622
+ COUNT(*) as item_count
623
+ FROM context_items
624
+ WHERE session_id = ?
625
+ GROUP BY DATE(created_at)
626
+ ORDER BY period DESC
627
+ `)
628
+ .all(testSessionId);
629
+ expect(periods.length).toBeGreaterThan(0);
630
+ expect(periods[0]).toHaveProperty('period');
631
+ expect(periods[0]).toHaveProperty('item_count');
632
+ expect(periods[0]).not.toHaveProperty('items');
633
+ });
634
+ it('should include item details when includeItems is true', () => {
635
+ // Get timeline with items
636
+ const periods = db
637
+ .prepare(`
638
+ SELECT DATE(created_at) as period
639
+ FROM context_items
640
+ WHERE session_id = ?
641
+ GROUP BY DATE(created_at)
642
+ ORDER BY period DESC
643
+ `)
644
+ .all(testSessionId);
645
+ const timeline = periods.map(period => {
646
+ const items = db
647
+ .prepare(`
648
+ SELECT * FROM context_items
649
+ WHERE session_id = ? AND DATE(created_at) = ?
650
+ ORDER BY created_at DESC
651
+ `)
652
+ .all(testSessionId, period.period);
653
+ return {
654
+ period: period.period,
655
+ item_count: items.length,
656
+ items: items.map(item => ({
657
+ key: item.key,
658
+ value: item.value,
659
+ category: item.category,
660
+ priority: item.priority,
661
+ created_at: item.created_at,
662
+ })),
663
+ };
664
+ });
665
+ expect(timeline.length).toBeGreaterThan(0);
666
+ expect(timeline[0].items).toBeDefined();
667
+ expect(timeline[0].items.length).toBe(timeline[0].item_count);
668
+ });
669
+ it('should include journal entries in timeline', () => {
670
+ const journals = db
671
+ .prepare('SELECT * FROM journal_entries WHERE session_id = ? ORDER BY created_at DESC')
672
+ .all(testSessionId);
673
+ expect(journals).toHaveLength(3);
674
+ expect(journals[0]).toHaveProperty('entry');
675
+ expect(journals[0]).toHaveProperty('mood');
676
+ });
677
+ });
678
+ describe('categories parameter', () => {
679
+ it('should filter timeline by single category', () => {
680
+ const taskItems = db
681
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
682
+ .all(testSessionId, 'task');
683
+ expect(taskItems.length).toBeGreaterThan(0);
684
+ expect(taskItems.every(item => item.category === 'task')).toBe(true);
685
+ });
686
+ it('should filter timeline by multiple categories', () => {
687
+ const items = db
688
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category IN (?, ?)')
689
+ .all(testSessionId, 'task', 'note');
690
+ expect(items.length).toBeGreaterThan(0);
691
+ expect(items.every(item => ['task', 'note'].includes(item.category))).toBe(true);
692
+ });
693
+ it('should return empty timeline when no items match categories', () => {
694
+ const items = db
695
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
696
+ .all(testSessionId, 'nonexistent_category');
697
+ expect(items).toHaveLength(0);
698
+ });
699
+ it('should include all categories when parameter is empty', () => {
700
+ const allItems = db
701
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
702
+ .all(testSessionId);
703
+ const categories = [...new Set(allItems.map(item => item.category))];
704
+ expect(categories.length).toBeGreaterThan(3);
705
+ });
706
+ });
707
+ describe('relativeTime parameter', () => {
708
+ it('should handle "today" relative time', () => {
709
+ // TIMEZONE-SAFE: Use UTC start of day
710
+ const todayStart = (0, timezone_safe_dates_1.createUTCDate)(0, 0, 0, 0);
711
+ const items = db
712
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
713
+ .all(testSessionId, todayStart.toISOString());
714
+ expect(items.length).toBeGreaterThan(0);
715
+ // Check for items we know are created "today" based on our test setup
716
+ // These items use specific timestamps that should be within today
717
+ const todayKeys = items.map(i => i.key);
718
+ // At minimum, we should have items created with relative hours that fall within today
719
+ expect(todayKeys.some(key => key.includes('recent') || key.includes('today'))).toBe(true);
720
+ });
721
+ it('should handle "yesterday" relative time', () => {
722
+ // TIMEZONE-SAFE: Use UTC dates for day boundaries
723
+ const yesterdayStart = (0, timezone_safe_dates_1.createUTCDate)(-1, 0, 0, 0);
724
+ const todayStart = (0, timezone_safe_dates_1.createUTCDate)(0, 0, 0, 0);
725
+ const items = db
726
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at < ?')
727
+ .all(testSessionId, yesterdayStart.toISOString(), todayStart.toISOString());
728
+ expect(items.some(i => i.key.includes('yesterday'))).toBe(true);
729
+ });
730
+ it('should handle "X hours ago" format', () => {
731
+ // TIMEZONE-SAFE: Use UTC-based hour calculations
732
+ const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
733
+ const oneHourAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-1);
734
+ const twoHoursAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-2);
735
+ const fiveHoursAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-5);
736
+ // Add test items with specific relative timestamps
737
+ db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
738
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_1h_ago', 'One hour ago', 'test', 'normal', oneHourAgo.toISOString());
739
+ db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
740
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_2h_ago', 'Two hours ago', 'test', 'normal', twoHoursAgo.toISOString());
741
+ db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
742
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_5h_ago', 'Five hours ago', 'test', 'normal', fiveHoursAgo.toISOString());
743
+ // Query for items created 2 hours ago or less
744
+ const queryTime = (0, timezone_safe_dates_1.createUTCDateByHours)(-2.1); // 2.1 hours ago to ensure we catch 2 hour old items
745
+ const items = db
746
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
747
+ .all(testSessionId, queryTime.toISOString());
748
+ // Check what items we actually got
749
+ const itemKeys = items.map(i => i.key);
750
+ // Should include items created 2 hours ago or less
751
+ expect(itemKeys).toContain('test_1h_ago');
752
+ expect(itemKeys).toContain('test_2h_ago');
753
+ // Should not include items created more than 2 hours ago
754
+ expect(itemKeys).not.toContain('test_5h_ago');
755
+ });
756
+ it('should handle "X days ago" format', () => {
757
+ // TIMEZONE-SAFE: Use UTC date arithmetic
758
+ const threeDaysAgo = (0, timezone_safe_dates_1.createUTCDate)(-3);
759
+ const items = db
760
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
761
+ .all(testSessionId, threeDaysAgo.toISOString());
762
+ expect(items.length).toBeGreaterThan(0);
763
+ // Should include items created within the last 3 days
764
+ expect(items.some(i => i.key === 'recent_1' ||
765
+ i.key === 'recent_2' ||
766
+ i.key === 'today_1' ||
767
+ i.key === 'today_2')).toBe(true);
768
+ });
769
+ it('should handle "this week" relative time', () => {
770
+ // TIMEZONE-SAFE: Calculate start of week in UTC
771
+ const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
772
+ const dayOfWeek = _today.getUTCDay();
773
+ const startOfWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek, 0, 0, 0);
774
+ const items = db
775
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
776
+ .all(testSessionId, startOfWeek.toISOString());
777
+ expect(items.length).toBeGreaterThan(0);
778
+ });
779
+ it('should handle "last week" relative time', () => {
780
+ // TIMEZONE-SAFE: Calculate last week's boundaries in UTC
781
+ const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
782
+ const dayOfWeek = _today.getUTCDay();
783
+ const startOfLastWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek - 7, 0, 0, 0);
784
+ const endOfLastWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek, 0, 0, 0);
785
+ const items = db
786
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at < ?')
787
+ .all(testSessionId, startOfLastWeek.toISOString(), endOfLastWeek.toISOString());
788
+ expect(items.some(i => i.key === 'week_ago_1')).toBe(true);
789
+ });
790
+ it('should default to all time when relativeTime is invalid', () => {
791
+ // Invalid relative time should return all items
792
+ const items = db
793
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
794
+ .all(testSessionId);
795
+ expect(items).toHaveLength(9);
796
+ });
797
+ });
798
+ describe('itemsPerPeriod parameter', () => {
799
+ it('should limit items per time period', () => {
800
+ // Get periods with limited items
801
+ const periods = db
802
+ .prepare(`
803
+ SELECT DATE(created_at) as period
804
+ FROM context_items
805
+ WHERE session_id = ?
806
+ GROUP BY DATE(created_at)
807
+ ORDER BY period DESC
808
+ `)
809
+ .all(testSessionId);
810
+ const timeline = periods.map(period => {
811
+ const items = db
812
+ .prepare(`
813
+ SELECT * FROM context_items
814
+ WHERE session_id = ? AND DATE(created_at) = ?
815
+ ORDER BY created_at DESC
816
+ LIMIT 2
817
+ `)
818
+ .all(testSessionId, period.period);
819
+ return {
820
+ period: period.period,
821
+ items: items,
822
+ hasMore: db
823
+ .prepare(`
824
+ SELECT COUNT(*) as total FROM context_items
825
+ WHERE session_id = ? AND DATE(created_at) = ?
826
+ `)
827
+ .get(testSessionId, period.period).total > 2,
828
+ };
829
+ });
830
+ timeline.forEach(period => {
831
+ expect(period.items.length).toBeLessThanOrEqual(2);
832
+ });
833
+ });
834
+ it('should indicate when more items exist in period', () => {
835
+ // Add many items to today
836
+ const now = new Date();
837
+ for (let i = 0; i < 10; i++) {
838
+ db.prepare(`
839
+ INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
840
+ VALUES (?, ?, ?, ?, ?, ?, ?)
841
+ `).run((0, uuid_1.v4)(), testSessionId, `extra_item_${i}`, `Extra value ${i}`, 'note', 'normal', now.toISOString());
842
+ }
843
+ const todayCount = db
844
+ .prepare(`
845
+ SELECT COUNT(*) as count FROM context_items
846
+ WHERE session_id = ? AND DATE(created_at) = DATE('now')
847
+ `)
848
+ .get(testSessionId);
849
+ expect(todayCount.count).toBeGreaterThan(5);
850
+ });
851
+ it('should show most recent items first in each period', () => {
852
+ const periods = db
853
+ .prepare(`
854
+ SELECT DATE(created_at) as period
855
+ FROM context_items
856
+ WHERE session_id = ?
857
+ GROUP BY DATE(created_at)
858
+ `)
859
+ .all(testSessionId);
860
+ periods.forEach(period => {
861
+ const items = db
862
+ .prepare(`
863
+ SELECT * FROM context_items
864
+ WHERE session_id = ? AND DATE(created_at) = ?
865
+ ORDER BY created_at DESC
866
+ `)
867
+ .all(testSessionId, period.period);
868
+ if (items.length > 1) {
869
+ for (let i = 1; i < items.length; i++) {
870
+ const prevTime = new Date(items[i - 1].created_at).getTime();
871
+ const currTime = new Date(items[i].created_at).getTime();
872
+ expect(prevTime).toBeGreaterThanOrEqual(currTime);
873
+ }
874
+ }
875
+ });
876
+ });
877
+ });
878
+ describe('groupBy parameter', () => {
879
+ it('should group by hour', () => {
880
+ const hourlyGroups = db
881
+ .prepare(`
882
+ SELECT
883
+ strftime('%Y-%m-%d %H:00', created_at) as period,
884
+ COUNT(*) as count
885
+ FROM context_items
886
+ WHERE session_id = ?
887
+ GROUP BY strftime('%Y-%m-%d %H:00', created_at)
888
+ ORDER BY period DESC
889
+ `)
890
+ .all(testSessionId);
891
+ expect(hourlyGroups.length).toBeGreaterThan(0);
892
+ expect(hourlyGroups[0].period).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:00/);
893
+ });
894
+ it('should group by day (default)', () => {
895
+ const dailyGroups = db
896
+ .prepare(`
897
+ SELECT
898
+ DATE(created_at) as period,
899
+ COUNT(*) as count
900
+ FROM context_items
901
+ WHERE session_id = ?
902
+ GROUP BY DATE(created_at)
903
+ ORDER BY period DESC
904
+ `)
905
+ .all(testSessionId);
906
+ expect(dailyGroups.length).toBeGreaterThan(0);
907
+ expect(dailyGroups[0].period).toMatch(/\d{4}-\d{2}-\d{2}/);
908
+ });
909
+ it('should group by week', () => {
910
+ const weeklyGroups = db
911
+ .prepare(`
912
+ SELECT
913
+ strftime('%Y-W%W', created_at) as period,
914
+ COUNT(*) as count
915
+ FROM context_items
916
+ WHERE session_id = ?
917
+ GROUP BY strftime('%Y-W%W', created_at)
918
+ ORDER BY period DESC
919
+ `)
920
+ .all(testSessionId);
921
+ expect(weeklyGroups.length).toBeGreaterThan(0);
922
+ expect(weeklyGroups[0].period).toMatch(/\d{4}-W\d{2}/);
923
+ });
924
+ });
925
+ describe('Combining timeline parameters', () => {
926
+ it('should combine categories and date filters', () => {
927
+ // TIMEZONE-SAFE: Use UTC date calculations
928
+ const twoDaysAgo = (0, timezone_safe_dates_1.createUTCDate)(-2);
929
+ const items = db
930
+ .prepare(`
931
+ SELECT * FROM context_items
932
+ WHERE session_id = ?
933
+ AND category IN (?, ?)
934
+ AND created_at >= ?
935
+ ORDER BY created_at DESC
936
+ `)
937
+ .all(testSessionId, 'task', 'note', twoDaysAgo.toISOString());
938
+ expect(items.length).toBeGreaterThan(0);
939
+ expect(items.every(i => ['task', 'note'].includes(i.category))).toBe(true);
940
+ });
941
+ it('should combine includeItems with itemsPerPeriod', () => {
942
+ const periods = db
943
+ .prepare(`
944
+ SELECT DATE(created_at) as period
945
+ FROM context_items
946
+ WHERE session_id = ?
947
+ GROUP BY DATE(created_at)
948
+ ORDER BY period DESC
949
+ `)
950
+ .all(testSessionId);
951
+ const timeline = periods.map(period => {
952
+ const allItems = db
953
+ .prepare(`
954
+ SELECT COUNT(*) as total FROM context_items
955
+ WHERE session_id = ? AND DATE(created_at) = ?
956
+ `)
957
+ .get(testSessionId, period.period);
958
+ const items = db
959
+ .prepare(`
960
+ SELECT * FROM context_items
961
+ WHERE session_id = ? AND DATE(created_at) = ?
962
+ ORDER BY created_at DESC
963
+ LIMIT 3
964
+ `)
965
+ .all(testSessionId, period.period);
966
+ return {
967
+ period: period.period,
968
+ items: items,
969
+ total_count: allItems.total,
970
+ hasMore: allItems.total > 3,
971
+ };
972
+ });
973
+ timeline.forEach(period => {
974
+ expect(period.items.length).toBeLessThanOrEqual(3);
975
+ expect(period.hasMore).toBe(period.total_count > 3);
976
+ });
977
+ });
978
+ });
979
+ describe('Backward compatibility', () => {
980
+ it('should work with no parameters as before', () => {
981
+ const items = db
982
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
983
+ .all(testSessionId);
984
+ expect(items.length).toBeGreaterThan(0);
985
+ });
986
+ it('should work with only startDate and endDate as before', () => {
987
+ // TIMEZONE-SAFE: Use UTC date range
988
+ const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
989
+ const startDate = (0, timezone_safe_dates_1.createUTCDate)(-7);
990
+ const endDate = _today;
991
+ const items = db
992
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at <= ?')
993
+ .all(testSessionId, startDate.toISOString(), endDate.toISOString());
994
+ expect(items.length).toBeGreaterThan(0);
995
+ });
996
+ });
997
+ describe('Edge cases', () => {
998
+ it('should handle empty timeline gracefully', () => {
999
+ const emptySessionId = (0, uuid_1.v4)();
1000
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
1001
+ const items = db
1002
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
1003
+ .all(emptySessionId);
1004
+ expect(items).toHaveLength(0);
1005
+ });
1006
+ it('should handle future dates in relativeTime', () => {
1007
+ const tomorrow = new Date();
1008
+ tomorrow.setDate(tomorrow.getDate() + 1);
1009
+ const items = db
1010
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
1011
+ .all(testSessionId, tomorrow.toISOString());
1012
+ expect(items).toHaveLength(0);
1013
+ });
1014
+ it('should handle very large itemsPerPeriod values', () => {
1015
+ const periods = db
1016
+ .prepare(`
1017
+ SELECT DATE(created_at) as period
1018
+ FROM context_items
1019
+ WHERE session_id = ?
1020
+ GROUP BY DATE(created_at)
1021
+ `)
1022
+ .all(testSessionId);
1023
+ periods.forEach(period => {
1024
+ const items = db
1025
+ .prepare(`
1026
+ SELECT * FROM context_items
1027
+ WHERE session_id = ? AND DATE(created_at) = ?
1028
+ ORDER BY created_at DESC
1029
+ LIMIT 99999
1030
+ `)
1031
+ .all(testSessionId, period.period);
1032
+ const totalCount = db
1033
+ .prepare(`
1034
+ SELECT COUNT(*) as count FROM context_items
1035
+ WHERE session_id = ? AND DATE(created_at) = ?
1036
+ `)
1037
+ .get(testSessionId, period.period);
1038
+ expect(items).toHaveLength(totalCount.count);
1039
+ });
1040
+ });
1041
+ });
1042
+ });
1043
+ describe('Performance considerations', () => {
1044
+ it('should handle large datasets efficiently', () => {
1045
+ // Add many items
1046
+ const startTime = Date.now();
1047
+ db.transaction(() => {
1048
+ for (let i = 0; i < 1000; i++) {
1049
+ db.prepare(`
1050
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
1051
+ VALUES (?, ?, ?, ?, ?, ?)
1052
+ `).run((0, uuid_1.v4)(), testSessionId, `perf_test_${i}`, `Performance test value ${i}`, i % 2 === 0 ? 'task' : 'note', i % 3 === 0 ? 'high' : 'normal');
1053
+ }
1054
+ })();
1055
+ const insertTime = Date.now() - startTime;
1056
+ expect(insertTime).toBeLessThan(1000); // Should complete within 1 second
1057
+ // Test query performance
1058
+ const queryStartTime = Date.now();
1059
+ const items = db
1060
+ .prepare(`
1061
+ SELECT * FROM context_items
1062
+ WHERE session_id = ?
1063
+ AND category = ?
1064
+ AND priority = ?
1065
+ ORDER BY created_at DESC
1066
+ LIMIT 50 OFFSET 100
1067
+ `)
1068
+ .all(testSessionId, 'task', 'high');
1069
+ const queryTime = Date.now() - queryStartTime;
1070
+ expect(queryTime).toBeLessThan(100); // Should complete within 100ms
1071
+ expect(items.length).toBeLessThanOrEqual(50);
1072
+ });
1073
+ it('should use indexes effectively', () => {
1074
+ // Check that indexes exist
1075
+ const indexes = db
1076
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'context_items'")
1077
+ .all();
1078
+ expect(indexes.length).toBeGreaterThan(0);
1079
+ expect(indexes.some(idx => idx.name.includes('session'))).toBe(true);
1080
+ });
1081
+ });
1082
+ });