@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,716 @@
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 globals_1 = require("@jest/globals");
37
+ const database_1 = require("../../utils/database");
38
+ const ContextRepository_1 = require("../../repositories/ContextRepository");
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
+ (0, globals_1.describe)('Enhanced Context Timeline Handler Integration Tests', () => {
44
+ let dbManager;
45
+ let tempDbPath;
46
+ let db;
47
+ let contextRepo;
48
+ let testSessionId;
49
+ (0, globals_1.beforeEach)(() => {
50
+ tempDbPath = path.join(os.tmpdir(), `test-enhanced-timeline-${Date.now()}.db`);
51
+ dbManager = new database_1.DatabaseManager({
52
+ filename: tempDbPath,
53
+ maxSize: 10 * 1024 * 1024,
54
+ walMode: true,
55
+ });
56
+ db = dbManager.getDatabase();
57
+ contextRepo = new ContextRepository_1.ContextRepository(dbManager);
58
+ // Create test session
59
+ testSessionId = (0, uuid_1.v4)();
60
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
61
+ });
62
+ (0, globals_1.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
+ function createTestDataWithTimeline() {
74
+ // TIMEZONE-SAFE PATTERN: Use fixed UTC dates to ensure consistent behavior
75
+ // across all environments regardless of system timezone
76
+ // Create a fixed reference point in UTC (2025-06-20 12:00:00 UTC)
77
+ // This ensures consistent timeline grouping in all environments
78
+ const baseDate = new Date('2025-06-20T12:00:00.000Z');
79
+ // Create calendar-day-aligned dates in UTC by using UTC methods
80
+ const today = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate(), 12, 0, 0));
81
+ const yesterday = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 1, 12, 0, 0));
82
+ const threeDaysAgo = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 3, 12, 0, 0));
83
+ const fiveDaysAgo = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 5, 12, 0, 0));
84
+ const sevenDaysAgo = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 7, 12, 0, 0));
85
+ // Create items across different time periods
86
+ const items = [
87
+ // Today - 6 items (all at noon on same day)
88
+ { time: new Date(today.getTime() + 1 * 60 * 60 * 1000), category: 'task', priority: 'high' },
89
+ {
90
+ time: new Date(today.getTime() + 2 * 60 * 60 * 1000),
91
+ category: 'task',
92
+ priority: 'normal',
93
+ },
94
+ {
95
+ time: new Date(today.getTime() + 3 * 60 * 60 * 1000),
96
+ category: 'note',
97
+ priority: 'normal',
98
+ },
99
+ {
100
+ time: new Date(today.getTime() + 4 * 60 * 60 * 1000),
101
+ category: 'decision',
102
+ priority: 'high',
103
+ },
104
+ {
105
+ time: new Date(today.getTime() + 5 * 60 * 60 * 1000),
106
+ category: 'progress',
107
+ priority: 'normal',
108
+ },
109
+ { time: new Date(today.getTime() + 6 * 60 * 60 * 1000), category: 'task', priority: 'low' },
110
+ // Yesterday - 3 items (all on previous day)
111
+ {
112
+ time: new Date(yesterday.getTime() + 1 * 60 * 60 * 1000),
113
+ category: 'task',
114
+ priority: 'high',
115
+ },
116
+ {
117
+ time: new Date(yesterday.getTime() + 2 * 60 * 60 * 1000),
118
+ category: 'note',
119
+ priority: 'normal',
120
+ },
121
+ {
122
+ time: new Date(yesterday.getTime() + 3 * 60 * 60 * 1000),
123
+ category: 'progress',
124
+ priority: 'low',
125
+ },
126
+ // 3 days ago - 1 item
127
+ {
128
+ time: new Date(threeDaysAgo.getTime() + 1 * 60 * 60 * 1000),
129
+ category: 'decision',
130
+ priority: 'high',
131
+ },
132
+ // 5 days ago - 2 items
133
+ {
134
+ time: new Date(fiveDaysAgo.getTime() + 1 * 60 * 60 * 1000),
135
+ category: 'task',
136
+ priority: 'normal',
137
+ },
138
+ {
139
+ time: new Date(fiveDaysAgo.getTime() + 2 * 60 * 60 * 1000),
140
+ category: 'note',
141
+ priority: 'normal',
142
+ },
143
+ // 7 days ago - 4 items (all on same day)
144
+ {
145
+ time: new Date(sevenDaysAgo.getTime() + 1 * 60 * 60 * 1000),
146
+ category: 'progress',
147
+ priority: 'high',
148
+ },
149
+ {
150
+ time: new Date(sevenDaysAgo.getTime() + 2 * 60 * 60 * 1000),
151
+ category: 'task',
152
+ priority: 'normal',
153
+ },
154
+ {
155
+ time: new Date(sevenDaysAgo.getTime() + 3 * 60 * 60 * 1000),
156
+ category: 'decision',
157
+ priority: 'low',
158
+ },
159
+ {
160
+ time: new Date(sevenDaysAgo.getTime() + 4 * 60 * 60 * 1000),
161
+ category: 'note',
162
+ priority: 'normal',
163
+ },
164
+ ];
165
+ const stmt = db.prepare(`
166
+ INSERT INTO context_items (
167
+ id, session_id, key, value, category, priority, channel, created_at, updated_at, size
168
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
+ `);
170
+ items.forEach((item, index) => {
171
+ const key = `item.${item.time.toISOString().split('T')[0]}.${index}`;
172
+ const value = `Test item created at ${item.time.toISOString()}`;
173
+ stmt.run((0, uuid_1.v4)(), testSessionId, key, value, item.category, item.priority, 'test-channel', item.time.toISOString(), item.time.toISOString(), Buffer.byteLength(value, 'utf8'));
174
+ });
175
+ return items;
176
+ }
177
+ (0, globals_1.describe)('minItemsPerPeriod Tests', () => {
178
+ (0, globals_1.beforeEach)(() => {
179
+ createTestDataWithTimeline();
180
+ });
181
+ (0, globals_1.it)('should filter periods with fewer items than minItemsPerPeriod', () => {
182
+ const timeline = contextRepo.getTimelineData({
183
+ sessionId: testSessionId,
184
+ groupBy: 'day',
185
+ minItemsPerPeriod: 3,
186
+ });
187
+ // Only today (6 items), yesterday (3 items), and 7 days ago (4 items) should appear
188
+ (0, globals_1.expect)(timeline.length).toBe(3);
189
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 3)).toBe(true);
190
+ });
191
+ (0, globals_1.it)('should include all periods when minItemsPerPeriod is 0', () => {
192
+ const timeline = contextRepo.getTimelineData({
193
+ sessionId: testSessionId,
194
+ groupBy: 'day',
195
+ minItemsPerPeriod: 0,
196
+ });
197
+ // Should include all 5 periods that have data:
198
+ // - Today (2025-06-20): 6 items
199
+ // - Yesterday (2025-06-19): 3 items
200
+ // - 3 days ago (2025-06-17): 1 item
201
+ // - 5 days ago (2025-06-15): 2 items
202
+ // - 7 days ago (2025-06-13): 4 items
203
+ (0, globals_1.expect)(timeline.length).toBe(5);
204
+ });
205
+ (0, globals_1.it)('should handle negative minItemsPerPeriod by treating as 0', () => {
206
+ const timeline = contextRepo.getTimelineData({
207
+ sessionId: testSessionId,
208
+ groupBy: 'day',
209
+ minItemsPerPeriod: -5,
210
+ });
211
+ // Should include all periods (same as 0)
212
+ (0, globals_1.expect)(timeline.length).toBe(5);
213
+ });
214
+ (0, globals_1.it)('should work with category filters and minItemsPerPeriod', () => {
215
+ const timeline = contextRepo.getTimelineData({
216
+ sessionId: testSessionId,
217
+ groupBy: 'day',
218
+ categories: ['task'],
219
+ minItemsPerPeriod: 2,
220
+ });
221
+ // Only today (3 task items) and 7 days ago (1 task, but need to verify) should qualify
222
+ // Actually checking: today has 3 tasks, yesterday has 1 task, 7 days ago has 1 task
223
+ // So only today qualifies with 2+ task items
224
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 2)).toBe(true);
225
+ });
226
+ (0, globals_1.it)('should work with hour grouping and minItemsPerPeriod', () => {
227
+ const timeline = contextRepo.getTimelineData({
228
+ sessionId: testSessionId,
229
+ groupBy: 'hour',
230
+ minItemsPerPeriod: 1,
231
+ relativeTime: 'today',
232
+ });
233
+ // Should only show hours that have at least 1 item
234
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 1)).toBe(true);
235
+ });
236
+ (0, globals_1.it)('should include item details when requested with minItemsPerPeriod filter', () => {
237
+ const timeline = contextRepo.getTimelineData({
238
+ sessionId: testSessionId,
239
+ groupBy: 'day',
240
+ minItemsPerPeriod: 3,
241
+ includeItems: true,
242
+ itemsPerPeriod: 5,
243
+ });
244
+ // Check that periods have items attached
245
+ timeline.forEach((period) => {
246
+ (0, globals_1.expect)(period.items).toBeDefined();
247
+ (0, globals_1.expect)(Array.isArray(period.items)).toBe(true);
248
+ (0, globals_1.expect)(period.items.length).toBeLessThanOrEqual(5);
249
+ (0, globals_1.expect)(period.count).toBeGreaterThanOrEqual(3);
250
+ });
251
+ });
252
+ (0, globals_1.it)('should work with relative time and minItemsPerPeriod', () => {
253
+ const timeline = contextRepo.getTimelineData({
254
+ sessionId: testSessionId,
255
+ relativeTime: '7 days ago',
256
+ groupBy: 'day',
257
+ minItemsPerPeriod: 2,
258
+ });
259
+ // Should include periods from last 7 days with 2+ items
260
+ timeline.forEach((period) => {
261
+ (0, globals_1.expect)(period.count).toBeGreaterThanOrEqual(2);
262
+ });
263
+ });
264
+ });
265
+ (0, globals_1.describe)('showEmpty Tests', () => {
266
+ (0, globals_1.beforeEach)(() => {
267
+ createTestDataWithTimeline();
268
+ });
269
+ (0, globals_1.it)('should generate empty periods when showEmpty is true', () => {
270
+ // Use UTC-based dates for consistent behavior across timezones
271
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
272
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 10, 12, 0, 0));
273
+ const timeline = contextRepo.getTimelineData({
274
+ sessionId: testSessionId,
275
+ startDate: startDate.toISOString(),
276
+ endDate: endDate.toISOString(),
277
+ groupBy: 'day',
278
+ showEmpty: true,
279
+ });
280
+ // Should have 11 periods (10 days + today)
281
+ (0, globals_1.expect)(timeline.length).toBe(11);
282
+ // Check that empty periods have count = 0
283
+ const emptyPeriods = timeline.filter((p) => p.count === 0);
284
+ (0, globals_1.expect)(emptyPeriods.length).toBeGreaterThan(0);
285
+ emptyPeriods.forEach((period) => {
286
+ (0, globals_1.expect)(period.count).toBe(0);
287
+ (0, globals_1.expect)(period.items).toEqual([]);
288
+ });
289
+ });
290
+ (0, globals_1.it)('should handle showEmpty with hour grouping', () => {
291
+ // Use UTC-based dates for consistent behavior across timezones
292
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
293
+ const startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);
294
+ const timeline = contextRepo.getTimelineData({
295
+ sessionId: testSessionId,
296
+ startDate: startDate.toISOString(),
297
+ endDate: endDate.toISOString(),
298
+ groupBy: 'hour',
299
+ showEmpty: true,
300
+ });
301
+ // Should have 25 periods (24 hours + current hour)
302
+ (0, globals_1.expect)(timeline.length).toBe(25);
303
+ });
304
+ (0, globals_1.it)('should handle showEmpty with week grouping', () => {
305
+ // Use UTC-based dates for consistent behavior across timezones
306
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
307
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 28, 12, 0, 0));
308
+ const timeline = contextRepo.getTimelineData({
309
+ sessionId: testSessionId,
310
+ startDate: startDate.toISOString(),
311
+ endDate: endDate.toISOString(),
312
+ groupBy: 'week',
313
+ showEmpty: true,
314
+ });
315
+ // Should have 5 periods (4 full weeks + current week)
316
+ (0, globals_1.expect)(timeline.length).toBeGreaterThanOrEqual(4);
317
+ (0, globals_1.expect)(timeline.length).toBeLessThanOrEqual(5);
318
+ });
319
+ (0, globals_1.it)('should include empty periods with categories filter', () => {
320
+ // Use UTC-based dates for consistent behavior across timezones
321
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
322
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 5, 12, 0, 0));
323
+ const timeline = contextRepo.getTimelineData({
324
+ sessionId: testSessionId,
325
+ startDate: startDate.toISOString(),
326
+ endDate: endDate.toISOString(),
327
+ groupBy: 'day',
328
+ categories: ['task'],
329
+ showEmpty: true,
330
+ });
331
+ // Should have 6 periods regardless of task items
332
+ (0, globals_1.expect)(timeline.length).toBe(6);
333
+ });
334
+ (0, globals_1.it)('should handle showEmpty with relative time', () => {
335
+ const timeline = contextRepo.getTimelineData({
336
+ sessionId: testSessionId,
337
+ relativeTime: '3 days ago',
338
+ groupBy: 'day',
339
+ showEmpty: true,
340
+ });
341
+ // Should have 4 periods (3 days ago to today)
342
+ (0, globals_1.expect)(timeline.length).toBe(4);
343
+ });
344
+ (0, globals_1.it)('should enforce reasonable limits on empty period generation', () => {
345
+ // Use UTC-based dates for consistent behavior across timezones
346
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
347
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate(), 12, 0, 0));
348
+ const timeline = contextRepo.getTimelineData({
349
+ sessionId: testSessionId,
350
+ startDate: startDate.toISOString(),
351
+ endDate: endDate.toISOString(),
352
+ groupBy: 'day',
353
+ showEmpty: true,
354
+ });
355
+ // Should enforce a reasonable limit (e.g., 365 days max)
356
+ (0, globals_1.expect)(timeline.length).toBeLessThanOrEqual(365);
357
+ });
358
+ (0, globals_1.it)('should include items in non-empty periods when showEmpty is true', () => {
359
+ // Use UTC-based dates for consistent behavior across timezones
360
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
361
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 3, 12, 0, 0));
362
+ const timeline = contextRepo.getTimelineData({
363
+ sessionId: testSessionId,
364
+ startDate: startDate.toISOString(),
365
+ endDate: endDate.toISOString(),
366
+ groupBy: 'day',
367
+ showEmpty: true,
368
+ includeItems: true,
369
+ });
370
+ // Find periods with data
371
+ const nonEmptyPeriods = timeline.filter((p) => p.count > 0);
372
+ (0, globals_1.expect)(nonEmptyPeriods.length).toBeGreaterThan(0);
373
+ nonEmptyPeriods.forEach((period) => {
374
+ (0, globals_1.expect)(period.items).toBeDefined();
375
+ (0, globals_1.expect)(period.items.length).toBeGreaterThan(0);
376
+ });
377
+ // Empty periods should have empty items array
378
+ const emptyPeriods = timeline.filter((p) => p.count === 0);
379
+ emptyPeriods.forEach((period) => {
380
+ (0, globals_1.expect)(period.items).toEqual([]);
381
+ });
382
+ });
383
+ });
384
+ (0, globals_1.describe)('Parameter Interaction Tests', () => {
385
+ (0, globals_1.beforeEach)(() => {
386
+ createTestDataWithTimeline();
387
+ });
388
+ (0, globals_1.it)('should have showEmpty override minItemsPerPeriod', () => {
389
+ // Use UTC-based dates for consistent behavior across timezones
390
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
391
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 5, 12, 0, 0));
392
+ const timeline = contextRepo.getTimelineData({
393
+ sessionId: testSessionId,
394
+ startDate: startDate.toISOString(),
395
+ endDate: endDate.toISOString(),
396
+ groupBy: 'day',
397
+ showEmpty: true,
398
+ minItemsPerPeriod: 3,
399
+ });
400
+ // Should show all 6 days, including empty ones
401
+ (0, globals_1.expect)(timeline.length).toBe(6);
402
+ // Should include periods with 0 items despite minItemsPerPeriod
403
+ const emptyPeriods = timeline.filter((p) => p.count === 0);
404
+ (0, globals_1.expect)(emptyPeriods.length).toBeGreaterThan(0);
405
+ });
406
+ (0, globals_1.it)('should work with all parameters combined', () => {
407
+ // Use UTC-based dates for consistent behavior across timezones
408
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
409
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 7, 12, 0, 0));
410
+ const timeline = contextRepo.getTimelineData({
411
+ sessionId: testSessionId,
412
+ startDate: startDate.toISOString(),
413
+ endDate: endDate.toISOString(),
414
+ groupBy: 'day',
415
+ categories: ['task', 'progress'],
416
+ showEmpty: true,
417
+ minItemsPerPeriod: 2,
418
+ includeItems: true,
419
+ itemsPerPeriod: 3,
420
+ });
421
+ // Should have 8 days total
422
+ (0, globals_1.expect)(timeline.length).toBe(8);
423
+ // Check various aspects
424
+ timeline.forEach((period) => {
425
+ (0, globals_1.expect)(period).toHaveProperty('period');
426
+ (0, globals_1.expect)(period).toHaveProperty('count');
427
+ (0, globals_1.expect)(period).toHaveProperty('items');
428
+ if (period.count > 0) {
429
+ // Non-empty periods should respect itemsPerPeriod
430
+ (0, globals_1.expect)(period.items.length).toBeLessThanOrEqual(3);
431
+ // Items should only be from specified categories
432
+ period.items.forEach((item) => {
433
+ (0, globals_1.expect)(['task', 'progress']).toContain(item.category);
434
+ });
435
+ }
436
+ });
437
+ });
438
+ (0, globals_1.it)('should handle showEmpty false with minItemsPerPeriod', () => {
439
+ const timeline = contextRepo.getTimelineData({
440
+ sessionId: testSessionId,
441
+ groupBy: 'day',
442
+ showEmpty: false,
443
+ minItemsPerPeriod: 2,
444
+ });
445
+ // Should only show periods with 2+ items
446
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 2)).toBe(true);
447
+ // Should not include any empty periods
448
+ (0, globals_1.expect)(timeline.every((period) => period.count > 0)).toBe(true);
449
+ });
450
+ (0, globals_1.it)('should respect date ranges with both new parameters', () => {
451
+ // Use UTC-based dates for consistent behavior across timezones
452
+ const baseDate = new Date('2025-06-20T12:00:00.000Z');
453
+ const startDate = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 2, 0, 0, 0));
454
+ const endDate = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - 1, 23, 59, 59, 999));
455
+ const timeline = contextRepo.getTimelineData({
456
+ sessionId: testSessionId,
457
+ startDate: startDate.toISOString(),
458
+ endDate: endDate.toISOString(),
459
+ groupBy: 'day',
460
+ showEmpty: true,
461
+ minItemsPerPeriod: 5,
462
+ });
463
+ // Should show exactly 2 days
464
+ (0, globals_1.expect)(timeline.length).toBe(2);
465
+ });
466
+ });
467
+ (0, globals_1.describe)('Edge Cases and Validation Tests', () => {
468
+ (0, globals_1.beforeEach)(() => {
469
+ createTestDataWithTimeline();
470
+ });
471
+ (0, globals_1.it)('should handle minItemsPerPeriod larger than any period count', () => {
472
+ const timeline = contextRepo.getTimelineData({
473
+ sessionId: testSessionId,
474
+ groupBy: 'day',
475
+ minItemsPerPeriod: 100,
476
+ });
477
+ // Should return empty array since no period has 100+ items
478
+ (0, globals_1.expect)(timeline).toEqual([]);
479
+ });
480
+ (0, globals_1.it)('should handle showEmpty with no date range specified', () => {
481
+ // Without date range, showEmpty should be ignored
482
+ const timeline = contextRepo.getTimelineData({
483
+ sessionId: testSessionId,
484
+ groupBy: 'day',
485
+ showEmpty: true,
486
+ });
487
+ // Should only return periods with data
488
+ (0, globals_1.expect)(timeline.every((period) => period.count > 0)).toBe(true);
489
+ });
490
+ (0, globals_1.it)('should handle invalid date ranges gracefully', () => {
491
+ const timeline = contextRepo.getTimelineData({
492
+ sessionId: testSessionId,
493
+ startDate: '2025-01-01',
494
+ endDate: '2024-01-01', // End before start
495
+ groupBy: 'day',
496
+ showEmpty: true,
497
+ });
498
+ // Should return empty array or handle gracefully
499
+ (0, globals_1.expect)(timeline).toEqual([]);
500
+ });
501
+ (0, globals_1.it)('should handle very large minItemsPerPeriod values', () => {
502
+ const timeline = contextRepo.getTimelineData({
503
+ sessionId: testSessionId,
504
+ groupBy: 'day',
505
+ minItemsPerPeriod: Number.MAX_SAFE_INTEGER,
506
+ });
507
+ // Should return empty array
508
+ (0, globals_1.expect)(timeline).toEqual([]);
509
+ });
510
+ (0, globals_1.it)('should handle fractional minItemsPerPeriod by rounding', () => {
511
+ const timeline = contextRepo.getTimelineData({
512
+ sessionId: testSessionId,
513
+ groupBy: 'day',
514
+ minItemsPerPeriod: 2.7, // Should be treated as 3
515
+ });
516
+ // Periods should have at least 3 items
517
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 3)).toBe(true);
518
+ });
519
+ (0, globals_1.it)('should handle showEmpty with very narrow time windows', () => {
520
+ // Use UTC-based dates for consistent behavior across timezones
521
+ const startDate = new Date('2025-06-20T12:00:00.000Z');
522
+ const endDate = new Date(startDate.getTime() + 60 * 60 * 1000); // 1 hour later
523
+ const timeline = contextRepo.getTimelineData({
524
+ sessionId: testSessionId,
525
+ startDate: startDate.toISOString(),
526
+ endDate: endDate.toISOString(),
527
+ groupBy: 'hour',
528
+ showEmpty: true,
529
+ });
530
+ // Should have 2 hour periods
531
+ (0, globals_1.expect)(timeline.length).toBe(2);
532
+ });
533
+ (0, globals_1.it)('should maintain sort order with new parameters', () => {
534
+ const timeline = contextRepo.getTimelineData({
535
+ sessionId: testSessionId,
536
+ groupBy: 'day',
537
+ showEmpty: false,
538
+ minItemsPerPeriod: 1,
539
+ });
540
+ // Verify descending order by period
541
+ for (let i = 1; i < timeline.length; i++) {
542
+ (0, globals_1.expect)(timeline[i - 1].period >= timeline[i].period).toBe(true);
543
+ }
544
+ });
545
+ });
546
+ (0, globals_1.describe)('Performance Tests', () => {
547
+ (0, globals_1.it)('should handle large datasets efficiently with minItemsPerPeriod', () => {
548
+ // Create many items
549
+ const stmt = db.prepare(`
550
+ INSERT INTO context_items (
551
+ id, session_id, key, value, category, channel, created_at, size
552
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
553
+ `);
554
+ const batchSize = 1000;
555
+ const baseDate = new Date('2025-06-20T12:00:00.000Z');
556
+ for (let i = 0; i < batchSize; i++) {
557
+ const daysAgo = Math.floor(Math.random() * 30);
558
+ const date = new Date(Date.UTC(baseDate.getUTCFullYear(), baseDate.getUTCMonth(), baseDate.getUTCDate() - daysAgo, 12, 0, 0));
559
+ stmt.run((0, uuid_1.v4)(), testSessionId, `perf.test.${i}`, `Performance test item ${i}`, 'performance', 'test-channel', date.toISOString(), 20);
560
+ }
561
+ const startTime = Date.now();
562
+ const timeline = contextRepo.getTimelineData({
563
+ sessionId: testSessionId,
564
+ groupBy: 'day',
565
+ minItemsPerPeriod: 10,
566
+ });
567
+ const endTime = Date.now();
568
+ // Should complete within reasonable time
569
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(500);
570
+ // Should only show days with 10+ items
571
+ (0, globals_1.expect)(timeline.every((period) => period.count >= 10)).toBe(true);
572
+ });
573
+ (0, globals_1.it)('should handle showEmpty efficiently for large date ranges', () => {
574
+ // Use UTC-based dates for consistent behavior across timezones
575
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
576
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 90, 12, 0, 0));
577
+ const startTime = Date.now();
578
+ const timeline = contextRepo.getTimelineData({
579
+ sessionId: testSessionId,
580
+ startDate: startDate.toISOString(),
581
+ endDate: endDate.toISOString(),
582
+ groupBy: 'day',
583
+ showEmpty: true,
584
+ });
585
+ const endTime = Date.now();
586
+ // Should complete within reasonable time
587
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(1000);
588
+ // Should have ~91 periods
589
+ (0, globals_1.expect)(timeline.length).toBeGreaterThanOrEqual(90);
590
+ (0, globals_1.expect)(timeline.length).toBeLessThanOrEqual(92);
591
+ });
592
+ });
593
+ (0, globals_1.describe)('Backward Compatibility Tests', () => {
594
+ (0, globals_1.beforeEach)(() => {
595
+ createTestDataWithTimeline();
596
+ });
597
+ (0, globals_1.it)('should work without new parameters (existing behavior)', () => {
598
+ const timeline = contextRepo.getTimelineData({
599
+ sessionId: testSessionId,
600
+ groupBy: 'day',
601
+ });
602
+ // Should return all periods with data
603
+ (0, globals_1.expect)(timeline.length).toBeGreaterThan(0);
604
+ (0, globals_1.expect)(timeline.every((period) => period.count > 0)).toBe(true);
605
+ });
606
+ (0, globals_1.it)('should maintain existing parameter functionality', () => {
607
+ const timeline = contextRepo.getTimelineData({
608
+ sessionId: testSessionId,
609
+ groupBy: 'day',
610
+ categories: ['task'],
611
+ includeItems: true,
612
+ itemsPerPeriod: 2,
613
+ });
614
+ // Should filter by category and limit items
615
+ timeline.forEach((period) => {
616
+ if (period.items && period.items.length > 0) {
617
+ (0, globals_1.expect)(period.items.every((item) => item.category === 'task')).toBe(true);
618
+ (0, globals_1.expect)(period.items.length).toBeLessThanOrEqual(2);
619
+ }
620
+ });
621
+ });
622
+ (0, globals_1.it)('should handle undefined new parameters gracefully', () => {
623
+ const timeline = contextRepo.getTimelineData({
624
+ sessionId: testSessionId,
625
+ groupBy: 'day',
626
+ minItemsPerPeriod: undefined,
627
+ showEmpty: undefined,
628
+ });
629
+ // Should behave as if parameters weren't provided
630
+ (0, globals_1.expect)(timeline.length).toBeGreaterThan(0);
631
+ (0, globals_1.expect)(timeline.every((period) => period.count > 0)).toBe(true);
632
+ });
633
+ (0, globals_1.it)('should maintain response format compatibility', () => {
634
+ const timeline = contextRepo.getTimelineData({
635
+ sessionId: testSessionId,
636
+ groupBy: 'day',
637
+ includeItems: true,
638
+ });
639
+ // Verify expected structure
640
+ timeline.forEach((period) => {
641
+ (0, globals_1.expect)(period).toHaveProperty('period');
642
+ (0, globals_1.expect)(period).toHaveProperty('count');
643
+ (0, globals_1.expect)(period).toHaveProperty('items');
644
+ (0, globals_1.expect)(typeof period.period).toBe('string');
645
+ (0, globals_1.expect)(typeof period.count).toBe('number');
646
+ (0, globals_1.expect)(Array.isArray(period.items)).toBe(true);
647
+ });
648
+ });
649
+ });
650
+ (0, globals_1.describe)('Handler Response Format Tests', () => {
651
+ (0, globals_1.beforeEach)(() => {
652
+ createTestDataWithTimeline();
653
+ });
654
+ (0, globals_1.it)('should format timeline response correctly', () => {
655
+ const timeline = contextRepo.getTimelineData({
656
+ sessionId: testSessionId,
657
+ groupBy: 'day',
658
+ minItemsPerPeriod: 2,
659
+ });
660
+ // Simulate handler formatting
661
+ const formattedPeriods = timeline
662
+ .map((p) => `${p.period}: ${p.count} items${p.hasMore ? ` (showing ${p.items?.length || 0} of ${p.totalCount})` : ''}`)
663
+ .join('\n');
664
+ const handlerResponse = {
665
+ content: [
666
+ {
667
+ type: 'text',
668
+ text: `Timeline (${timeline.length} periods):\n\n${formattedPeriods}`,
669
+ },
670
+ ],
671
+ };
672
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('Timeline');
673
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('periods');
674
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('items');
675
+ });
676
+ (0, globals_1.it)('should include empty periods in response when showEmpty is true', () => {
677
+ // Use UTC-based dates for consistent behavior across timezones
678
+ const endDate = new Date('2025-06-20T12:00:00.000Z');
679
+ const startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate() - 3, 12, 0, 0));
680
+ const timeline = contextRepo.getTimelineData({
681
+ sessionId: testSessionId,
682
+ startDate: startDate.toISOString(),
683
+ endDate: endDate.toISOString(),
684
+ groupBy: 'day',
685
+ showEmpty: true,
686
+ });
687
+ // Handler should indicate empty periods
688
+ const formattedPeriods = timeline
689
+ .map((p) => `${p.period}: ${p.count === 0 ? 'No items' : `${p.count} items`}`)
690
+ .join('\n');
691
+ (0, globals_1.expect)(formattedPeriods).toContain('No items');
692
+ });
693
+ (0, globals_1.it)('should format response with journal entries integration', () => {
694
+ // Add journal entries
695
+ db.prepare(`
696
+ INSERT INTO journal_entries (id, session_id, entry, created_at)
697
+ VALUES (?, ?, ?, ?)
698
+ `).run((0, uuid_1.v4)(), testSessionId, 'Test journal entry', new Date('2025-06-20T12:00:00.000Z').toISOString());
699
+ const timeline = contextRepo.getTimelineData({
700
+ sessionId: testSessionId,
701
+ groupBy: 'day',
702
+ includeItems: true,
703
+ });
704
+ // Handler would merge context items and journal entries
705
+ const handlerResponse = {
706
+ content: [
707
+ {
708
+ type: 'text',
709
+ text: `Timeline with ${timeline.length} periods and journal entries`,
710
+ },
711
+ ],
712
+ };
713
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('journal entries');
714
+ });
715
+ });
716
+ });