@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,852 @@
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 timestamps_1 = require("../../utils/timestamps");
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const uuid_1 = require("uuid");
44
+ const timezone_safe_dates_1 = require("../utils/timezone-safe-dates");
45
+ describe('Context Diff Integration Tests', () => {
46
+ let dbManager;
47
+ let tempDbPath;
48
+ let db;
49
+ let testHelper;
50
+ let testSessionId;
51
+ let otherSessionId;
52
+ let repositories;
53
+ beforeEach(() => {
54
+ tempDbPath = path.join(os.tmpdir(), `test-context-diff-${Date.now()}.db`);
55
+ dbManager = new database_1.DatabaseManager({
56
+ filename: tempDbPath,
57
+ maxSize: 10 * 1024 * 1024,
58
+ walMode: true,
59
+ });
60
+ db = dbManager.getDatabase();
61
+ testHelper = new database_test_helper_1.DatabaseTestHelper(db);
62
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
63
+ // Create test sessions
64
+ testSessionId = (0, uuid_1.v4)();
65
+ otherSessionId = (0, uuid_1.v4)();
66
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
67
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
68
+ });
69
+ afterEach(() => {
70
+ dbManager.close();
71
+ try {
72
+ fs.unlinkSync(tempDbPath);
73
+ fs.unlinkSync(`${tempDbPath}-wal`);
74
+ fs.unlinkSync(`${tempDbPath}-shm`);
75
+ }
76
+ catch (_e) {
77
+ // Ignore
78
+ }
79
+ });
80
+ describe('Basic Diff Operations', () => {
81
+ it('should detect added items since timestamp', () => {
82
+ // TIMEZONE-SAFE: Use UTC-based date calculations
83
+ (0, timezone_safe_dates_1.validateTimezoneSafety)();
84
+ const baseTime = (0, timezone_safe_dates_1.createUTCDateByHours)(-2);
85
+ // Add items at different times
86
+ const oldItem = {
87
+ id: (0, uuid_1.v4)(),
88
+ key: 'old_item',
89
+ value: 'This existed before',
90
+ created_at: (0, timezone_safe_dates_1.createUTCDateByHours)(-2.1).toISOString(), // Slightly before base time
91
+ };
92
+ const newItem = {
93
+ id: (0, uuid_1.v4)(),
94
+ key: 'new_item',
95
+ value: 'This was added recently',
96
+ created_at: (0, timezone_safe_dates_1.createUTCDateByHours)(0).toISOString(), // Now
97
+ };
98
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(oldItem.id, testSessionId, oldItem.key, oldItem.value, (0, timestamps_1.toSQLiteTimestamp)(oldItem.created_at), (0, timestamps_1.toSQLiteTimestamp)(oldItem.created_at));
99
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(newItem.id, testSessionId, newItem.key, newItem.value, (0, timestamps_1.toSQLiteTimestamp)(newItem.created_at), (0, timestamps_1.toSQLiteTimestamp)(newItem.created_at));
100
+ // Query for items added since baseTime
101
+ const addedItems = db
102
+ .prepare(`
103
+ SELECT * FROM context_items
104
+ WHERE session_id = ? AND created_at > ?
105
+ ORDER BY created_at DESC
106
+ `)
107
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
108
+ expect(addedItems).toHaveLength(1);
109
+ expect(addedItems[0].key).toBe('new_item');
110
+ });
111
+ it('should detect modified items', () => {
112
+ const baseTime = new Date();
113
+ baseTime.setHours(baseTime.getHours() - 2);
114
+ // Create an item
115
+ const itemId = (0, uuid_1.v4)();
116
+ const createTime = (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 1000).toISOString());
117
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(itemId, testSessionId, 'changing_item', 'Original value', createTime, createTime);
118
+ // Update the item
119
+ const newUpdateTime = (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString());
120
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ? WHERE id = ?').run('Modified value', newUpdateTime, itemId);
121
+ // Query for items modified since baseTime
122
+ const modifiedItems = db
123
+ .prepare(`
124
+ SELECT * FROM context_items
125
+ WHERE session_id = ?
126
+ AND created_at <= ?
127
+ AND updated_at > ?
128
+ ORDER BY updated_at DESC
129
+ `)
130
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
131
+ expect(modifiedItems).toHaveLength(1);
132
+ expect(modifiedItems[0].key).toBe('changing_item');
133
+ expect(modifiedItems[0].value).toBe('Modified value');
134
+ });
135
+ it('should handle no changes scenario', () => {
136
+ const baseTime = new Date();
137
+ // Add items before the base time
138
+ const oldTime = new Date(baseTime.getTime() - 60000).toISOString();
139
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Old value', (0, timestamps_1.toSQLiteTimestamp)(oldTime), (0, timestamps_1.toSQLiteTimestamp)(oldTime));
140
+ // Query for changes since baseTime (should be empty)
141
+ const addedItems = db
142
+ .prepare(`
143
+ SELECT * FROM context_items
144
+ WHERE session_id = ? AND created_at > ?
145
+ `)
146
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
147
+ const modifiedItems = db
148
+ .prepare(`
149
+ SELECT * FROM context_items
150
+ WHERE session_id = ?
151
+ AND created_at <= ?
152
+ AND updated_at > ?
153
+ `)
154
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
155
+ expect(addedItems).toHaveLength(0);
156
+ expect(modifiedItems).toHaveLength(0);
157
+ });
158
+ });
159
+ describe('Checkpoint-based Diff', () => {
160
+ it('should compare against checkpoint using repository method', () => {
161
+ // Disable triggers to control timestamps precisely
162
+ testHelper.disableTimestampTriggers();
163
+ const checkpointTime = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago
164
+ // Create items at checkpoint time
165
+ const items = [
166
+ { key: 'item1', value: 'Value 1' },
167
+ { key: 'item2', value: 'Value 2' },
168
+ { key: 'item3', value: 'Value 3' },
169
+ ];
170
+ items.forEach(item => {
171
+ 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, (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
172
+ });
173
+ // Wait a bit to ensure timestamp difference
174
+ const afterCheckpoint = new Date(checkpointTime.getTime() + 2000); // 2 seconds later
175
+ // Make changes after checkpoint
176
+ // 1. Add new item
177
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item4', 'Value 4', (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()));
178
+ // 2. Modify existing item
179
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ? WHERE session_id = ? AND key = ?').run('Modified Value 2', (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()), testSessionId, 'item2');
180
+ // 3. Delete an item
181
+ db.prepare('DELETE FROM context_items WHERE session_id = ? AND key = ?').run(testSessionId, 'item3');
182
+ // Re-enable triggers
183
+ testHelper.enableTimestampTriggers();
184
+ // Use repository method to get diff
185
+ const sinceTime = new Date(checkpointTime.getTime() + 1000); // 1 second after checkpoint
186
+ const diff = repositories.contexts.getDiff({
187
+ sessionId: testSessionId,
188
+ sinceTimestamp: sinceTime.toISOString(),
189
+ });
190
+ expect(diff.added).toHaveLength(1);
191
+ expect(diff.added[0].key).toBe('item4');
192
+ expect(diff.modified).toHaveLength(1);
193
+ expect(diff.modified[0].key).toBe('item2');
194
+ expect(diff.modified[0].value).toBe('Modified Value 2');
195
+ // Note: deleted items are only detected via checkpoint comparison, not timestamp diff
196
+ });
197
+ it('should compare against checkpoint', () => {
198
+ // Create initial state
199
+ const checkpointTime = new Date();
200
+ const items = [
201
+ { key: 'item1', value: 'Value 1' },
202
+ { key: 'item2', value: 'Value 2' },
203
+ { key: 'item3', value: 'Value 3' },
204
+ ];
205
+ const itemIds = [];
206
+ items.forEach(item => {
207
+ const id = (0, uuid_1.v4)();
208
+ itemIds.push(id);
209
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, testSessionId, item.key, item.value, (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
210
+ });
211
+ // Create checkpoint slightly after items (to ensure proper comparison)
212
+ const actualCheckpointTime = new Date(checkpointTime.getTime() + 1000); // 1 second later
213
+ const checkpointId = (0, uuid_1.v4)();
214
+ db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'test-checkpoint', (0, timestamps_1.toSQLiteTimestamp)(actualCheckpointTime.toISOString()));
215
+ // Link items to checkpoint
216
+ itemIds.forEach(itemId => {
217
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
218
+ });
219
+ // Make changes after checkpoint
220
+ // 1. Add new item
221
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item4', 'Value 4');
222
+ // Get checkpoint items BEFORE making changes
223
+ const checkpointItemsBefore = db
224
+ .prepare(`
225
+ SELECT ci.* FROM context_items ci
226
+ JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
227
+ WHERE cpi.checkpoint_id = ?
228
+ `)
229
+ .all(checkpointId);
230
+ // Store checkpoint state
231
+ const checkpointState = new Map(checkpointItemsBefore.map((item) => [item.key, item]));
232
+ // 2. Modify existing item
233
+ db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ? AND key = ?').run('Modified Value 2', testSessionId, 'item2');
234
+ // 3. Delete an item
235
+ db.prepare('DELETE FROM context_items WHERE session_id = ? AND key = ?').run(testSessionId, 'item3');
236
+ // Get current items after changes
237
+ const currentItems = db
238
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
239
+ .all(testSessionId);
240
+ // Calculate diff using stored checkpoint state
241
+ const checkpointKeys = new Set(checkpointState.keys());
242
+ const currentKeys = new Set(currentItems.map((i) => i.key));
243
+ // Added: in current but not in checkpoint
244
+ const added = currentItems.filter((i) => !checkpointKeys.has(i.key));
245
+ expect(added).toHaveLength(1);
246
+ expect(added[0].key).toBe('item4');
247
+ // Modified: in both but values differ
248
+ const modified = currentItems.filter((i) => {
249
+ const checkpointItem = checkpointState.get(i.key);
250
+ return checkpointItem && checkpointItem.value !== i.value;
251
+ });
252
+ expect(modified).toHaveLength(1);
253
+ expect(modified[0].key).toBe('item2');
254
+ // Deleted: in checkpoint but not in current
255
+ const deleted = Array.from(checkpointKeys)
256
+ .filter(key => !currentKeys.has(key))
257
+ .map(key => checkpointState.get(key));
258
+ expect(deleted).toHaveLength(1);
259
+ expect(deleted[0].key).toBe('item3');
260
+ });
261
+ it('should handle non-existent checkpoint', () => {
262
+ const checkpoint = db
263
+ .prepare('SELECT * FROM checkpoints WHERE name = ?')
264
+ .get('non-existent-checkpoint');
265
+ expect(checkpoint).toBeUndefined();
266
+ });
267
+ });
268
+ describe('Relative Time Parsing', () => {
269
+ it('should parse "2 hours ago"', () => {
270
+ const now = new Date();
271
+ const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
272
+ // Add items at different times
273
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'recent_item', 'Added 1 hour ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 60 * 60 * 1000).toISOString()));
274
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Added 3 hours ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString()));
275
+ // Query items added since "2 hours ago"
276
+ const items = db
277
+ .prepare(`
278
+ SELECT * FROM context_items
279
+ WHERE session_id = ? AND created_at > ?
280
+ ORDER BY created_at DESC
281
+ `)
282
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(twoHoursAgo.toISOString()));
283
+ expect(items).toHaveLength(1);
284
+ expect(items[0].key).toBe('recent_item');
285
+ });
286
+ it('should parse "yesterday"', () => {
287
+ const now = new Date();
288
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
289
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
290
+ // Add items at different times
291
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'today_item', 'Added today', (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString()));
292
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'yesterday_item', 'Added yesterday', (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() + 12 * 60 * 60 * 1000).toISOString()), // Noon yesterday
293
+ (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() + 12 * 60 * 60 * 1000).toISOString()));
294
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Added 2 days ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()));
295
+ // Query items added since yesterday
296
+ const items = db
297
+ .prepare(`
298
+ SELECT * FROM context_items
299
+ WHERE session_id = ? AND created_at >= ?
300
+ ORDER BY created_at DESC
301
+ `)
302
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(yesterday.toISOString()));
303
+ expect(items).toHaveLength(2);
304
+ expect(items.map((i) => i.key)).toContain('today_item');
305
+ expect(items.map((i) => i.key)).toContain('yesterday_item');
306
+ expect(items.map((i) => i.key)).not.toContain('old_item');
307
+ });
308
+ it('should parse "3 days ago"', () => {
309
+ const now = new Date();
310
+ const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
311
+ // Add items
312
+ for (let i = 0; i < 5; i++) {
313
+ const daysAgo = i;
314
+ const itemTime = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
315
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${daysAgo}d_ago`, `Added ${daysAgo} days ago`, (0, timestamps_1.toSQLiteTimestamp)(itemTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(itemTime.toISOString()));
316
+ }
317
+ // Query items since 3 days ago
318
+ const items = db
319
+ .prepare(`
320
+ SELECT * FROM context_items
321
+ WHERE session_id = ? AND created_at > ?
322
+ ORDER BY created_at DESC
323
+ `)
324
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(threeDaysAgo.toISOString()));
325
+ expect(items).toHaveLength(3); // 0, 1, 2 days ago
326
+ expect(items.map((i) => i.key)).toContain('item_0d_ago');
327
+ expect(items.map((i) => i.key)).toContain('item_1d_ago');
328
+ expect(items.map((i) => i.key)).toContain('item_2d_ago');
329
+ expect(items.map((i) => i.key)).not.toContain('item_3d_ago');
330
+ expect(items.map((i) => i.key)).not.toContain('item_4d_ago');
331
+ });
332
+ it('should handle invalid relative time formats', () => {
333
+ // Test that invalid formats don't crash
334
+ const invalidFormats = [
335
+ 'invalid time',
336
+ '2 minutes ago', // Not supported
337
+ 'next week',
338
+ '',
339
+ null,
340
+ ];
341
+ invalidFormats.forEach(format => {
342
+ expect(() => {
343
+ db.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?').all(testSessionId, format);
344
+ }).not.toThrow();
345
+ });
346
+ });
347
+ });
348
+ describe('Filtering Options', () => {
349
+ beforeEach(() => {
350
+ const baseTime = new Date();
351
+ baseTime.setHours(baseTime.getHours() - 2);
352
+ // Create diverse items for filtering tests
353
+ const items = [
354
+ {
355
+ key: 'task_new_high',
356
+ value: 'New high priority task',
357
+ category: 'task',
358
+ priority: 'high',
359
+ channel: 'main',
360
+ created_at: new Date().toISOString(),
361
+ },
362
+ {
363
+ key: 'task_old_normal',
364
+ value: 'Old normal priority task',
365
+ category: 'task',
366
+ priority: 'normal',
367
+ channel: 'main',
368
+ created_at: new Date(baseTime.getTime() - 1000).toISOString(),
369
+ },
370
+ {
371
+ key: 'note_new_low',
372
+ value: 'New low priority note',
373
+ category: 'note',
374
+ priority: 'low',
375
+ channel: 'feature/docs',
376
+ created_at: new Date().toISOString(),
377
+ },
378
+ {
379
+ key: 'decision_modified',
380
+ value: 'Modified decision',
381
+ category: 'decision',
382
+ priority: 'high',
383
+ channel: 'main',
384
+ created_at: new Date(baseTime.getTime() - 2000).toISOString(),
385
+ updated_at: new Date().toISOString(),
386
+ },
387
+ ];
388
+ items.forEach(item => {
389
+ db.prepare(`INSERT INTO context_items
390
+ (id, session_id, key, value, category, priority, channel, created_at, updated_at)
391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, item.channel, (0, timestamps_1.toSQLiteTimestamp)(item.created_at), (0, timestamps_1.toSQLiteTimestamp)(item.updated_at || item.created_at));
392
+ });
393
+ });
394
+ it('should filter diff by category', () => {
395
+ const baseTime = new Date();
396
+ baseTime.setHours(baseTime.getHours() - 2);
397
+ // Get added items filtered by category
398
+ const addedTasks = db
399
+ .prepare(`
400
+ SELECT * FROM context_items
401
+ WHERE session_id = ?
402
+ AND created_at > ?
403
+ AND category = ?
404
+ ORDER BY created_at DESC
405
+ `)
406
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'task');
407
+ expect(addedTasks).toHaveLength(1);
408
+ expect(addedTasks[0].key).toBe('task_new_high');
409
+ });
410
+ it('should filter diff by channel', () => {
411
+ const baseTime = new Date();
412
+ baseTime.setHours(baseTime.getHours() - 2);
413
+ // Get all changes in 'main' channel
414
+ const mainChannelAdded = db
415
+ .prepare(`
416
+ SELECT * FROM context_items
417
+ WHERE session_id = ?
418
+ AND created_at > ?
419
+ AND channel = ?
420
+ ORDER BY created_at DESC
421
+ `)
422
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'main');
423
+ expect(mainChannelAdded).toHaveLength(1);
424
+ expect(mainChannelAdded[0].key).toBe('task_new_high');
425
+ });
426
+ it('should filter diff by multiple channels', () => {
427
+ const baseTime = new Date();
428
+ baseTime.setHours(baseTime.getHours() - 2);
429
+ const channels = ['main', 'feature/docs'];
430
+ const placeholders = channels.map(() => '?').join(',');
431
+ const multiChannelItems = db
432
+ .prepare(`
433
+ SELECT * FROM context_items
434
+ WHERE session_id = ?
435
+ AND created_at > ?
436
+ AND channel IN (${placeholders})
437
+ ORDER BY created_at DESC
438
+ `)
439
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), ...channels);
440
+ expect(multiChannelItems).toHaveLength(2);
441
+ expect(multiChannelItems.map((i) => i.key)).toContain('task_new_high');
442
+ expect(multiChannelItems.map((i) => i.key)).toContain('note_new_low');
443
+ });
444
+ });
445
+ describe('Include Values Option', () => {
446
+ it('should include full values when requested', () => {
447
+ const longValue = 'A'.repeat(1000);
448
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'long_item', longValue);
449
+ const item = db
450
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
451
+ .get(testSessionId, 'long_item');
452
+ expect(item.value).toBe(longValue);
453
+ expect(item.value.length).toBe(1000);
454
+ });
455
+ it('should be able to exclude values for summary', () => {
456
+ // Add items
457
+ for (let i = 0; i < 5; i++) {
458
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i}`, `Value ${i} with lots of text...`);
459
+ }
460
+ // Query without values (just keys and metadata)
461
+ const items = db
462
+ .prepare('SELECT id, session_id, key, category, priority, channel, created_at FROM context_items WHERE session_id = ?')
463
+ .all(testSessionId);
464
+ expect(items).toHaveLength(5);
465
+ items.forEach((item) => {
466
+ expect(item).not.toHaveProperty('value');
467
+ expect(item).toHaveProperty('key');
468
+ expect(item).toHaveProperty('created_at');
469
+ });
470
+ });
471
+ });
472
+ describe('Privacy and Session Boundaries', () => {
473
+ it('should respect privacy boundaries in diff', () => {
474
+ const baseTime = new Date();
475
+ baseTime.setHours(baseTime.getHours() - 1);
476
+ // Add items to both sessions
477
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_public', 'Public item', 0);
478
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_private', 'Private item', 1);
479
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_public', 'Other public', 0);
480
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_private', 'Other private', 1);
481
+ // Query from testSessionId perspective
482
+ const visibleItems = db
483
+ .prepare(`
484
+ SELECT * FROM context_items
485
+ WHERE created_at > ?
486
+ AND (is_private = 0 OR session_id = ?)
487
+ ORDER BY created_at DESC
488
+ `)
489
+ .all((0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), testSessionId);
490
+ expect(visibleItems.map((i) => i.key)).toContain('my_public');
491
+ expect(visibleItems.map((i) => i.key)).toContain('my_private');
492
+ expect(visibleItems.map((i) => i.key)).toContain('other_public');
493
+ expect(visibleItems.map((i) => i.key)).not.toContain('other_private');
494
+ });
495
+ it('should only show session-specific diff by default', () => {
496
+ const baseTime = new Date();
497
+ baseTime.setHours(baseTime.getHours() - 1);
498
+ // Add items to different sessions
499
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_item', 'My value');
500
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_item', 'Other value');
501
+ // Query for specific session
502
+ const sessionItems = db
503
+ .prepare(`
504
+ SELECT * FROM context_items
505
+ WHERE session_id = ? AND created_at > ?
506
+ `)
507
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
508
+ expect(sessionItems).toHaveLength(1);
509
+ expect(sessionItems[0].key).toBe('my_item');
510
+ });
511
+ });
512
+ describe('Deleted Items Detection', () => {
513
+ it('should detect deleted items using checkpoint comparison', () => {
514
+ // Create initial items
515
+ const item1Id = (0, uuid_1.v4)();
516
+ const item2Id = (0, uuid_1.v4)();
517
+ const item3Id = (0, uuid_1.v4)();
518
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item1Id, testSessionId, 'keep_item', 'Will remain');
519
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item2Id, testSessionId, 'delete_item1', 'Will be deleted');
520
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item3Id, testSessionId, 'delete_item2', 'Will also be deleted');
521
+ // Create checkpoint
522
+ const checkpointId = (0, uuid_1.v4)();
523
+ db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, testSessionId, 'before-deletion');
524
+ // Link all items to checkpoint
525
+ [item1Id, item2Id, item3Id].forEach(itemId => {
526
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
527
+ });
528
+ // Get checkpoint items BEFORE deletion
529
+ const checkpointItemsBefore = db
530
+ .prepare(`
531
+ SELECT ci.key FROM context_items ci
532
+ JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
533
+ WHERE cpi.checkpoint_id = ?
534
+ `)
535
+ .all(checkpointId);
536
+ // Store keys before deletion
537
+ const keysBeforeDeletion = new Set(checkpointItemsBefore.map((i) => i.key));
538
+ // Delete some items
539
+ db.prepare('DELETE FROM context_items WHERE session_id = ? AND key IN (?, ?)').run(testSessionId, 'delete_item1', 'delete_item2');
540
+ // Get current items after deletion
541
+ const currentItems = db
542
+ .prepare('SELECT key FROM context_items WHERE session_id = ?')
543
+ .all(testSessionId);
544
+ const currentKeys = new Set(currentItems.map((i) => i.key));
545
+ // Find deleted items by comparing before and after
546
+ const deletedKeys = Array.from(keysBeforeDeletion).filter(key => !currentKeys.has(key));
547
+ expect(deletedKeys).toHaveLength(2);
548
+ expect(deletedKeys).toContain('delete_item1');
549
+ expect(deletedKeys).toContain('delete_item2');
550
+ expect(deletedKeys).not.toContain('keep_item');
551
+ });
552
+ it('should handle deletion time tracking if available', () => {
553
+ // Create audit log table for tracking deletions (if implemented)
554
+ // This is a potential enhancement for the actual implementation
555
+ db.prepare(`
556
+ CREATE TABLE IF NOT EXISTS audit_log (
557
+ id TEXT PRIMARY KEY,
558
+ session_id TEXT NOT NULL,
559
+ action TEXT NOT NULL,
560
+ item_key TEXT,
561
+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP
562
+ )
563
+ `).run();
564
+ const itemId = (0, uuid_1.v4)();
565
+ const itemKey = 'tracked_item';
566
+ // Create and then delete an item
567
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, itemKey, 'Will be deleted with tracking');
568
+ // Delete and log
569
+ const deleteTime = new Date().toISOString();
570
+ db.prepare('DELETE FROM context_items WHERE id = ?').run(itemId);
571
+ db.prepare('INSERT INTO audit_log (id, session_id, action, item_key, timestamp) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'delete', itemKey, deleteTime);
572
+ // Query audit log for deletions
573
+ const deletions = db
574
+ .prepare(`
575
+ SELECT * FROM audit_log
576
+ WHERE session_id = ? AND action = 'delete'
577
+ ORDER BY timestamp DESC
578
+ `)
579
+ .all(testSessionId);
580
+ expect(deletions).toHaveLength(1);
581
+ expect(deletions[0].item_key).toBe(itemKey);
582
+ expect(deletions[0].timestamp).toBe(deleteTime);
583
+ });
584
+ });
585
+ describe('Edge Cases', () => {
586
+ it('should handle future timestamps gracefully', () => {
587
+ const futureTime = new Date();
588
+ futureTime.setDate(futureTime.getDate() + 1);
589
+ // Add current item
590
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'current_item', 'Current value');
591
+ // Query with future timestamp (should return nothing)
592
+ const items = db
593
+ .prepare(`
594
+ SELECT * FROM context_items
595
+ WHERE session_id = ? AND created_at > ?
596
+ `)
597
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(futureTime.toISOString()));
598
+ expect(items).toHaveLength(0);
599
+ });
600
+ it('should handle very old timestamps', () => {
601
+ const veryOldTime = (0, timestamps_1.toSQLiteTimestamp)(new Date('1970-01-01').toISOString());
602
+ // Add items
603
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item1', 'Value 1');
604
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item2', 'Value 2');
605
+ // Query since very old time (should return all items)
606
+ const items = db
607
+ .prepare(`
608
+ SELECT * FROM context_items
609
+ WHERE session_id = ? AND created_at > ?
610
+ `)
611
+ .all(testSessionId, veryOldTime);
612
+ expect(items).toHaveLength(2);
613
+ });
614
+ it('should handle invalid checkpoint names', () => {
615
+ const checkpoint = db
616
+ .prepare('SELECT * FROM checkpoints WHERE session_id = ? AND name = ?')
617
+ .get(testSessionId, 'non-existent-checkpoint');
618
+ expect(checkpoint).toBeUndefined();
619
+ });
620
+ it('should handle empty diff results', () => {
621
+ const baseTime = new Date().toISOString();
622
+ // Query for changes (should be empty)
623
+ const added = db
624
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
625
+ .all(testSessionId, baseTime);
626
+ const modified = db
627
+ .prepare(`
628
+ SELECT * FROM context_items
629
+ WHERE session_id = ?
630
+ AND created_at <= ?
631
+ AND updated_at > ?
632
+ `)
633
+ .all(testSessionId, baseTime, baseTime);
634
+ expect(added).toHaveLength(0);
635
+ expect(modified).toHaveLength(0);
636
+ });
637
+ });
638
+ describe('Performance with Large Datasets', () => {
639
+ it('should efficiently diff large numbers of changes', () => {
640
+ const baseTime = new Date();
641
+ baseTime.setMinutes(baseTime.getMinutes() - 30);
642
+ // Create many items before base time
643
+ for (let i = 0; i < 500; i++) {
644
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `old_item_${i}`, `Old value ${i}`, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
645
+ }
646
+ // Create many new items
647
+ for (let i = 0; i < 500; i++) {
648
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `new_item_${i}`, `New value ${i}`);
649
+ }
650
+ const start = Date.now();
651
+ // Query for changes
652
+ const added = db
653
+ .prepare(`
654
+ SELECT COUNT(*) as count FROM context_items
655
+ WHERE session_id = ? AND created_at > ?
656
+ `)
657
+ .get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
658
+ const duration = Date.now() - start;
659
+ expect(added.count).toBe(500);
660
+ expect(duration).toBeLessThan(100); // Should be fast
661
+ });
662
+ it('should paginate large diff results', () => {
663
+ const baseTime = new Date();
664
+ baseTime.setMinutes(baseTime.getMinutes() - 30);
665
+ // Create many new items
666
+ for (let i = 0; i < 100; i++) {
667
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(3, '0')}`, `Value ${i}`);
668
+ }
669
+ // Get paginated results
670
+ const page1 = db
671
+ .prepare(`
672
+ SELECT * FROM context_items
673
+ WHERE session_id = ? AND created_at > ?
674
+ ORDER BY key ASC
675
+ LIMIT 20 OFFSET 0
676
+ `)
677
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
678
+ const page2 = db
679
+ .prepare(`
680
+ SELECT * FROM context_items
681
+ WHERE session_id = ? AND created_at > ?
682
+ ORDER BY key ASC
683
+ LIMIT 20 OFFSET 20
684
+ `)
685
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
686
+ expect(page1).toHaveLength(20);
687
+ expect(page2).toHaveLength(20);
688
+ expect(page1[0].key).toBe('item_000');
689
+ expect(page1[19].key).toBe('item_019');
690
+ expect(page2[0].key).toBe('item_020');
691
+ expect(page2[19].key).toBe('item_039');
692
+ });
693
+ });
694
+ describe('Complex Scenarios', () => {
695
+ it('should handle item recreation (delete then add with same key)', () => {
696
+ const checkpointTime = new Date();
697
+ // Create initial item
698
+ const originalId = (0, uuid_1.v4)();
699
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(originalId, testSessionId, 'recreated_item', 'Original value', (0, timestamps_1.toSQLiteTimestamp)(new Date(checkpointTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(checkpointTime.getTime() - 60000).toISOString()));
700
+ // Create checkpoint
701
+ const checkpointId = (0, uuid_1.v4)();
702
+ db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'before-recreation', (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
703
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, originalId);
704
+ // Get checkpoint item BEFORE deletion
705
+ const checkpointItemBefore = db
706
+ .prepare(`
707
+ SELECT ci.* FROM context_items ci
708
+ JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
709
+ WHERE cpi.checkpoint_id = ? AND ci.key = ?
710
+ `)
711
+ .get(checkpointId, 'recreated_item');
712
+ // Store the original data
713
+ const originalData = { ...checkpointItemBefore };
714
+ // Delete the item
715
+ db.prepare('DELETE FROM context_items WHERE id = ?').run(originalId);
716
+ // Recreate with same key but different value
717
+ const newId = (0, uuid_1.v4)();
718
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(newId, testSessionId, 'recreated_item', 'New value after recreation');
719
+ // Get current item
720
+ const currentItem = db
721
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
722
+ .get(testSessionId, 'recreated_item');
723
+ // Should be treated as modified (different id, different value)
724
+ expect(originalData).toBeDefined();
725
+ expect(currentItem).toBeDefined();
726
+ expect(originalData.id).not.toBe(currentItem.id);
727
+ expect(originalData.value).not.toBe(currentItem.value);
728
+ });
729
+ it('should handle mixed changes across categories and channels', () => {
730
+ const baseTime = new Date();
731
+ baseTime.setHours(baseTime.getHours() - 1);
732
+ // Create diverse changes
733
+ const changes = [
734
+ // Added items
735
+ { action: 'add', key: 'task_new_1', category: 'task', channel: 'main' },
736
+ { action: 'add', key: 'note_new_1', category: 'note', channel: 'feature/ui' },
737
+ // Modified items (created before, updated after)
738
+ { action: 'modify', key: 'task_mod_1', category: 'task', channel: 'main' },
739
+ { action: 'modify', key: 'decision_mod_1', category: 'decision', channel: 'hotfix' },
740
+ ];
741
+ // Process changes
742
+ changes.forEach(change => {
743
+ if (change.action === 'add') {
744
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, change.key, `Value for ${change.key}`, change.category, change.channel);
745
+ }
746
+ else if (change.action === 'modify') {
747
+ // Create before base time
748
+ const id = (0, uuid_1.v4)();
749
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, testSessionId, change.key, `Original value for ${change.key}`, change.category, change.channel, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
750
+ // Update after base time
751
+ db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(`Modified value for ${change.key}`, id);
752
+ }
753
+ });
754
+ // Query different combinations
755
+ // 1. All added in 'main' channel
756
+ const mainAdded = db
757
+ .prepare(`
758
+ SELECT * FROM context_items
759
+ WHERE session_id = ?
760
+ AND created_at > ?
761
+ AND channel = ?
762
+ `)
763
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'main');
764
+ expect(mainAdded).toHaveLength(1);
765
+ expect(mainAdded[0].key).toBe('task_new_1');
766
+ // 2. All modified tasks
767
+ const modifiedTasks = db
768
+ .prepare(`
769
+ SELECT * FROM context_items
770
+ WHERE session_id = ?
771
+ AND created_at <= ?
772
+ AND updated_at > ?
773
+ AND category = ?
774
+ `)
775
+ .all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'task');
776
+ expect(modifiedTasks).toHaveLength(1);
777
+ expect(modifiedTasks[0].key).toBe('task_mod_1');
778
+ // 3. All changes (added + modified) summary
779
+ const allAdded = db
780
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND created_at > ?')
781
+ .get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
782
+ const allModified = db
783
+ .prepare(`
784
+ SELECT COUNT(*) as count FROM context_items
785
+ WHERE session_id = ?
786
+ AND created_at <= ?
787
+ AND updated_at > ?
788
+ `)
789
+ .get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
790
+ expect(allAdded.count).toBe(2);
791
+ expect(allModified.count).toBe(2);
792
+ });
793
+ });
794
+ describe('Summary Generation', () => {
795
+ it('should generate accurate diff summary', () => {
796
+ const baseTime = new Date();
797
+ baseTime.setHours(baseTime.getHours() - 1);
798
+ // Create scenario: 5 added, 3 modified, 2 deleted
799
+ // Add old items
800
+ const oldItemIds = [];
801
+ for (let i = 0; i < 5; i++) {
802
+ const id = (0, uuid_1.v4)();
803
+ oldItemIds.push(id);
804
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, testSessionId, `old_item_${i}`, `Original value ${i}`, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
805
+ }
806
+ // Create checkpoint for deletion tracking
807
+ const checkpointId = (0, uuid_1.v4)();
808
+ db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'summary-test', (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
809
+ oldItemIds.forEach(id => {
810
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, id);
811
+ });
812
+ // Count checkpoint items before any modifications
813
+ const originalCheckpointCount = db
814
+ .prepare('SELECT COUNT(*) as count FROM checkpoint_items WHERE checkpoint_id = ?')
815
+ .get(checkpointId);
816
+ // Modify 3 items
817
+ for (let i = 0; i < 3; i++) {
818
+ db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ? AND key = ?').run(`Modified value ${i}`, testSessionId, `old_item_${i}`);
819
+ }
820
+ // Delete 2 items
821
+ db.prepare('DELETE FROM context_items WHERE session_id = ? AND key IN (?, ?)').run(testSessionId, 'old_item_3', 'old_item_4');
822
+ // Add 5 new items
823
+ for (let i = 0; i < 5; i++) {
824
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `new_item_${i}`, `New value ${i}`);
825
+ }
826
+ // Calculate summary
827
+ const added = db
828
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND created_at > ?')
829
+ .get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
830
+ const modified = db
831
+ .prepare(`
832
+ SELECT COUNT(*) as count FROM context_items
833
+ WHERE session_id = ?
834
+ AND created_at <= ?
835
+ AND updated_at > ?
836
+ `)
837
+ .get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
838
+ // Get deleted count from checkpoint comparison
839
+ const currentItemCount = db
840
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
841
+ .get(testSessionId);
842
+ // deletedCount = original items - (current items - newly added items)
843
+ const deletedCount = originalCheckpointCount.count - (currentItemCount.count - added.count);
844
+ expect(added.count).toBe(5);
845
+ expect(modified.count).toBe(3);
846
+ expect(deletedCount).toBe(2);
847
+ // Summary string
848
+ const summary = `${added.count} added, ${modified.count} modified, ${deletedCount} deleted`;
849
+ expect(summary).toBe('5 added, 3 modified, 2 deleted');
850
+ });
851
+ });
852
+ });