@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,1500 @@
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 RepositoryManager_1 = require("../../repositories/RepositoryManager");
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
+ /**
45
+ * Integration tests for the context_watch handler
46
+ *
47
+ * These tests verify the handler's behavior through the actual tool interface,
48
+ * ensuring we test:
49
+ * - Watcher creation with various filters
50
+ * - Change detection (CREATE, UPDATE, DELETE)
51
+ * - Polling mechanism with sequence numbers
52
+ * - Watcher lifecycle (create, poll, expire, stop)
53
+ * - Privacy boundaries and session isolation
54
+ * - Performance with many watchers and large changesets
55
+ */
56
+ (0, globals_1.describe)('Context Watch Handler Integration Tests', () => {
57
+ let dbManager;
58
+ let repositories;
59
+ let tempDbPath;
60
+ let db;
61
+ let testSessionId;
62
+ let otherSessionId;
63
+ let currentSessionId = null;
64
+ // Mock implementation of context_watch handler
65
+ const mockContextWatchHandler = async (args) => {
66
+ const { action, watcherId, filters } = args;
67
+ const targetSessionId = currentSessionId || testSessionId;
68
+ try {
69
+ switch (action) {
70
+ case 'create': {
71
+ // Validate filters
72
+ if (filters) {
73
+ const { keys, channels, categories } = filters;
74
+ // Validate key patterns
75
+ if (keys && !Array.isArray(keys)) {
76
+ throw new Error('keys filter must be an array');
77
+ }
78
+ // Validate channels
79
+ if (channels && !Array.isArray(channels)) {
80
+ throw new Error('channels filter must be an array');
81
+ }
82
+ // Validate categories
83
+ if (categories && !Array.isArray(categories)) {
84
+ throw new Error('categories filter must be an array');
85
+ }
86
+ // Validate category values
87
+ if (categories) {
88
+ const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
89
+ for (const cat of categories) {
90
+ if (!validCategories.includes(cat)) {
91
+ throw new Error(`Invalid category: ${cat}`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ // Generate watcher ID
97
+ const newWatcherId = `watch_${(0, uuid_1.v4)().substring(0, 8)}`;
98
+ // Get current max sequence number
99
+ const maxSeqResult = db
100
+ .prepare('SELECT MAX(sequence_number) as max_seq FROM context_items WHERE session_id = ?')
101
+ .get(targetSessionId);
102
+ const currentSequence = maxSeqResult?.max_seq || 0;
103
+ // Store watcher in database
104
+ db.prepare(`INSERT INTO watchers (
105
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(newWatcherId, targetSessionId, JSON.stringify(filters || {}), currentSequence, (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), (0, timestamps_1.ensureSQLiteFormat)(new Date(Date.now() + 30 * 60 * 1000).toISOString()), // 30 min expiry
107
+ 1);
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: JSON.stringify({
113
+ watcherId: newWatcherId,
114
+ created: true,
115
+ filters: filters || {},
116
+ currentSequence,
117
+ expiresIn: '30 minutes',
118
+ }, null, 2),
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ case 'poll': {
124
+ if (!watcherId) {
125
+ throw new Error('watcherId is required for poll action');
126
+ }
127
+ // Get watcher
128
+ const watcher = db
129
+ .prepare('SELECT * FROM watchers WHERE id = ? AND session_id = ?')
130
+ .get(watcherId, targetSessionId);
131
+ if (!watcher) {
132
+ throw new Error(`Watcher not found: ${watcherId}`);
133
+ }
134
+ if (!watcher.is_active) {
135
+ throw new Error(`Watcher is stopped: ${watcherId}`);
136
+ }
137
+ // Check expiration
138
+ const now = new Date();
139
+ const expiresAt = new Date(watcher.expires_at.replace(' ', 'T') + 'Z');
140
+ if (now > expiresAt) {
141
+ // Mark as inactive
142
+ db.prepare('UPDATE watchers SET is_active = 0 WHERE id = ?').run(watcherId);
143
+ throw new Error(`Watcher expired: ${watcherId}`);
144
+ }
145
+ const filters = JSON.parse(watcher.filters);
146
+ const lastSequence = watcher.last_sequence;
147
+ // Build query for changes
148
+ let query = `
149
+ SELECT * FROM context_items
150
+ WHERE session_id = ?
151
+ AND sequence_number > ?
152
+ `;
153
+ const params = [targetSessionId, lastSequence];
154
+ // Apply filters
155
+ if (filters.keys && filters.keys.length > 0) {
156
+ const keyConditions = filters.keys
157
+ .map((pattern) => {
158
+ // Convert wildcard pattern to SQL LIKE pattern
159
+ const sqlPattern = pattern.replace(/\*/g, '%').replace(/\?/g, '_');
160
+ params.push(sqlPattern);
161
+ return 'key LIKE ?';
162
+ })
163
+ .join(' OR ');
164
+ query += ` AND (${keyConditions})`;
165
+ }
166
+ if (filters.channels && filters.channels.length > 0) {
167
+ const placeholders = filters.channels.map(() => '?').join(',');
168
+ query += ` AND channel IN (${placeholders})`;
169
+ params.push(...filters.channels);
170
+ }
171
+ if (filters.categories && filters.categories.length > 0) {
172
+ const placeholders = filters.categories.map(() => '?').join(',');
173
+ query += ` AND category IN (${placeholders})`;
174
+ params.push(...filters.categories);
175
+ }
176
+ // Always respect privacy boundaries
177
+ query += ' AND (is_private = 0 OR session_id = ?)';
178
+ params.push(targetSessionId);
179
+ query += ' ORDER BY sequence_number ASC';
180
+ // Get changes
181
+ const changes = db.prepare(query).all(...params);
182
+ // Detect different change types
183
+ const changeEvents = [];
184
+ let maxSeenSequence = lastSequence;
185
+ for (const item of changes) {
186
+ maxSeenSequence = Math.max(maxSeenSequence, item.sequence_number);
187
+ // Determine change type
188
+ let changeType = 'CREATE';
189
+ // Check if this was an update by comparing times
190
+ const itemCreated = new Date(item.created_at.replace(' ', 'T') + 'Z');
191
+ const itemUpdated = new Date(item.updated_at.replace(' ', 'T') + 'Z');
192
+ const watcherCreated = new Date(watcher.created_at.replace(' ', 'T') + 'Z');
193
+ // If item was created before watcher and has been modified, it's an UPDATE
194
+ if (itemCreated < watcherCreated && item.sequence_number > lastSequence) {
195
+ changeType = 'UPDATE';
196
+ }
197
+ else if (itemUpdated > itemCreated) {
198
+ // Or if updated time is after created time
199
+ changeType = 'UPDATE';
200
+ }
201
+ // Check for deletions by looking for deletion markers
202
+ const deletionMarker = db
203
+ .prepare('SELECT * FROM deleted_items WHERE key = ? AND session_id = ? AND deleted_at > ?')
204
+ .get(item.key, targetSessionId, watcher.created_at);
205
+ if (deletionMarker) {
206
+ changeType = 'DELETE';
207
+ }
208
+ changeEvents.push({
209
+ type: changeType,
210
+ key: item.key,
211
+ value: changeType !== 'DELETE' ? item.value : undefined,
212
+ category: item.category,
213
+ channel: item.channel,
214
+ sequence: item.sequence_number,
215
+ timestamp: item.updated_at,
216
+ });
217
+ }
218
+ // Also check for pure deletions (items that existed at watcher creation but are now gone)
219
+ const deletions = db
220
+ .prepare(`
221
+ SELECT * FROM deleted_items
222
+ WHERE session_id = ?
223
+ AND sequence_number > ?
224
+ ORDER BY sequence_number ASC
225
+ `)
226
+ .all(targetSessionId, lastSequence);
227
+ for (const deletion of deletions) {
228
+ // Apply filters to deletions
229
+ let matchesFilter = true;
230
+ if (filters.keys && filters.keys.length > 0) {
231
+ matchesFilter = filters.keys.some((pattern) => {
232
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
233
+ return regex.test(deletion.key);
234
+ });
235
+ }
236
+ if (matchesFilter && filters.channels && filters.channels.length > 0) {
237
+ matchesFilter = filters.channels.includes(deletion.channel);
238
+ }
239
+ if (matchesFilter && filters.categories && filters.categories.length > 0) {
240
+ matchesFilter = filters.categories.includes(deletion.category);
241
+ }
242
+ if (matchesFilter) {
243
+ maxSeenSequence = Math.max(maxSeenSequence, deletion.sequence_number);
244
+ changeEvents.push({
245
+ type: 'DELETE',
246
+ key: deletion.key,
247
+ category: deletion.category,
248
+ channel: deletion.channel,
249
+ sequence: deletion.sequence_number,
250
+ timestamp: deletion.deleted_at,
251
+ });
252
+ }
253
+ }
254
+ // Update watcher's last sequence if we found changes
255
+ if (maxSeenSequence > lastSequence) {
256
+ db.prepare('UPDATE watchers SET last_sequence = ? WHERE id = ?').run(maxSeenSequence, watcherId);
257
+ }
258
+ // Extend expiration on successful poll
259
+ const newExpiry = (0, timestamps_1.ensureSQLiteFormat)(new Date(Date.now() + 30 * 60 * 1000).toISOString());
260
+ db.prepare('UPDATE watchers SET expires_at = ? WHERE id = ?').run(newExpiry, watcherId);
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text',
265
+ text: JSON.stringify({
266
+ watcherId,
267
+ changes: changeEvents,
268
+ hasMore: false, // In real implementation, might limit results
269
+ lastSequence: maxSeenSequence,
270
+ polledAt: new Date().toISOString(),
271
+ }, null, 2),
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ case 'stop': {
277
+ if (!watcherId) {
278
+ throw new Error('watcherId is required for stop action');
279
+ }
280
+ const result = db
281
+ .prepare('UPDATE watchers SET is_active = 0 WHERE id = ? AND session_id = ?')
282
+ .run(watcherId, targetSessionId);
283
+ if (result.changes === 0) {
284
+ throw new Error(`Watcher not found: ${watcherId}`);
285
+ }
286
+ return {
287
+ content: [
288
+ {
289
+ type: 'text',
290
+ text: JSON.stringify({
291
+ watcherId,
292
+ stopped: true,
293
+ }, null, 2),
294
+ },
295
+ ],
296
+ };
297
+ }
298
+ case 'list': {
299
+ const watchers = db
300
+ .prepare(`
301
+ SELECT * FROM watchers
302
+ WHERE session_id = ?
303
+ ORDER BY created_at DESC
304
+ `)
305
+ .all(targetSessionId);
306
+ const watcherList = watchers.map(w => ({
307
+ watcherId: w.id,
308
+ active: w.is_active === 1,
309
+ filters: JSON.parse(w.filters),
310
+ lastSequence: w.last_sequence,
311
+ createdAt: w.created_at,
312
+ expiresAt: w.expires_at,
313
+ }));
314
+ return {
315
+ content: [
316
+ {
317
+ type: 'text',
318
+ text: JSON.stringify({
319
+ watchers: watcherList,
320
+ total: watcherList.length,
321
+ }, null, 2),
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ default:
327
+ throw new Error(`Unknown action: ${action}`);
328
+ }
329
+ }
330
+ catch (error) {
331
+ return {
332
+ content: [
333
+ {
334
+ type: 'text',
335
+ text: `Error: ${error.message}`,
336
+ },
337
+ ],
338
+ };
339
+ }
340
+ };
341
+ (0, globals_1.beforeEach)(() => {
342
+ tempDbPath = path.join(os.tmpdir(), `test-context-watch-handler-${Date.now()}.db`);
343
+ dbManager = new database_1.DatabaseManager({
344
+ filename: tempDbPath,
345
+ maxSize: 10 * 1024 * 1024,
346
+ walMode: true,
347
+ });
348
+ db = dbManager.getDatabase();
349
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
350
+ // Create test sessions
351
+ testSessionId = (0, uuid_1.v4)();
352
+ otherSessionId = (0, uuid_1.v4)();
353
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
354
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
355
+ // Create watchers table for testing
356
+ db.exec(`
357
+ CREATE TABLE IF NOT EXISTS watchers (
358
+ id TEXT PRIMARY KEY,
359
+ session_id TEXT NOT NULL,
360
+ filters TEXT NOT NULL,
361
+ last_sequence INTEGER NOT NULL,
362
+ created_at TEXT NOT NULL,
363
+ expires_at TEXT NOT NULL,
364
+ is_active INTEGER NOT NULL DEFAULT 1,
365
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
366
+ )
367
+ `);
368
+ // Create deleted_items table for tracking deletions
369
+ db.exec(`
370
+ CREATE TABLE IF NOT EXISTS deleted_items (
371
+ id TEXT PRIMARY KEY,
372
+ session_id TEXT NOT NULL,
373
+ key TEXT NOT NULL,
374
+ category TEXT,
375
+ channel TEXT,
376
+ sequence_number INTEGER NOT NULL,
377
+ deleted_at TEXT NOT NULL,
378
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
379
+ )
380
+ `);
381
+ // Add sequence_number column to context_items if not exists
382
+ const columns = db.prepare('PRAGMA table_info(context_items)').all();
383
+ if (!columns.some((col) => col.name === 'sequence_number')) {
384
+ db.exec('ALTER TABLE context_items ADD COLUMN sequence_number INTEGER DEFAULT 0');
385
+ // Create trigger to auto-increment sequence numbers
386
+ db.exec(`
387
+ CREATE TRIGGER IF NOT EXISTS increment_sequence_insert
388
+ AFTER INSERT ON context_items
389
+ BEGIN
390
+ UPDATE context_items
391
+ SET sequence_number = (
392
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
393
+ FROM context_items
394
+ WHERE session_id = NEW.session_id
395
+ )
396
+ WHERE id = NEW.id AND sequence_number = 0;
397
+ END
398
+ `);
399
+ db.exec(`
400
+ CREATE TRIGGER IF NOT EXISTS increment_sequence_update
401
+ AFTER UPDATE ON context_items
402
+ WHEN OLD.value != NEW.value
403
+ BEGIN
404
+ UPDATE context_items
405
+ SET sequence_number = (
406
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
407
+ FROM context_items
408
+ WHERE session_id = NEW.session_id
409
+ )
410
+ WHERE id = NEW.id;
411
+ END
412
+ `);
413
+ }
414
+ });
415
+ (0, globals_1.afterEach)(() => {
416
+ dbManager.close();
417
+ try {
418
+ fs.unlinkSync(tempDbPath);
419
+ fs.unlinkSync(`${tempDbPath}-wal`);
420
+ fs.unlinkSync(`${tempDbPath}-shm`);
421
+ }
422
+ catch (_e) {
423
+ // Ignore
424
+ }
425
+ });
426
+ (0, globals_1.describe)('Watcher Creation', () => {
427
+ (0, globals_1.it)('should create a watcher with no filters', async () => {
428
+ const result = await mockContextWatchHandler({
429
+ action: 'create',
430
+ });
431
+ const response = JSON.parse(result.content[0].text);
432
+ (0, globals_1.expect)(response.created).toBe(true);
433
+ (0, globals_1.expect)(response.watcherId).toMatch(/^watch_[a-f0-9]{8}$/);
434
+ (0, globals_1.expect)(response.filters).toEqual({});
435
+ (0, globals_1.expect)(response.currentSequence).toBe(0);
436
+ (0, globals_1.expect)(response.expiresIn).toBe('30 minutes');
437
+ });
438
+ (0, globals_1.it)('should create a watcher with key pattern filters', async () => {
439
+ const result = await mockContextWatchHandler({
440
+ action: 'create',
441
+ filters: {
442
+ keys: ['user_*', 'config_*', '*_settings'],
443
+ },
444
+ });
445
+ const response = JSON.parse(result.content[0].text);
446
+ (0, globals_1.expect)(response.created).toBe(true);
447
+ (0, globals_1.expect)(response.filters.keys).toEqual(['user_*', 'config_*', '*_settings']);
448
+ });
449
+ (0, globals_1.it)('should create a watcher with channel filters', async () => {
450
+ const result = await mockContextWatchHandler({
451
+ action: 'create',
452
+ filters: {
453
+ channels: ['main', 'feature/auth', 'hotfix'],
454
+ },
455
+ });
456
+ const response = JSON.parse(result.content[0].text);
457
+ (0, globals_1.expect)(response.created).toBe(true);
458
+ (0, globals_1.expect)(response.filters.channels).toEqual(['main', 'feature/auth', 'hotfix']);
459
+ });
460
+ (0, globals_1.it)('should create a watcher with category filters', async () => {
461
+ const result = await mockContextWatchHandler({
462
+ action: 'create',
463
+ filters: {
464
+ categories: ['task', 'decision', 'error'],
465
+ },
466
+ });
467
+ const response = JSON.parse(result.content[0].text);
468
+ (0, globals_1.expect)(response.created).toBe(true);
469
+ (0, globals_1.expect)(response.filters.categories).toEqual(['task', 'decision', 'error']);
470
+ });
471
+ (0, globals_1.it)('should create a watcher with combined filters', async () => {
472
+ const result = await mockContextWatchHandler({
473
+ action: 'create',
474
+ filters: {
475
+ keys: ['task_*'],
476
+ channels: ['main'],
477
+ categories: ['task'],
478
+ },
479
+ });
480
+ const response = JSON.parse(result.content[0].text);
481
+ (0, globals_1.expect)(response.created).toBe(true);
482
+ (0, globals_1.expect)(response.filters.keys).toEqual(['task_*']);
483
+ (0, globals_1.expect)(response.filters.channels).toEqual(['main']);
484
+ (0, globals_1.expect)(response.filters.categories).toEqual(['task']);
485
+ });
486
+ (0, globals_1.it)('should validate filter parameters', async () => {
487
+ // Invalid keys type
488
+ let result = await mockContextWatchHandler({
489
+ action: 'create',
490
+ filters: {
491
+ keys: 'not_an_array',
492
+ },
493
+ });
494
+ (0, globals_1.expect)(result.content[0].text).toContain('Error: keys filter must be an array');
495
+ // Invalid channels type
496
+ result = await mockContextWatchHandler({
497
+ action: 'create',
498
+ filters: {
499
+ channels: 'not_an_array',
500
+ },
501
+ });
502
+ (0, globals_1.expect)(result.content[0].text).toContain('Error: channels filter must be an array');
503
+ // Invalid category value
504
+ result = await mockContextWatchHandler({
505
+ action: 'create',
506
+ filters: {
507
+ categories: ['invalid_category'],
508
+ },
509
+ });
510
+ (0, globals_1.expect)(result.content[0].text).toContain('Error: Invalid category: invalid_category');
511
+ });
512
+ (0, globals_1.it)('should capture current sequence number at creation', async () => {
513
+ // Add some items to increase sequence number
514
+ repositories.contexts.save(testSessionId, { key: 'item1', value: 'value1' });
515
+ repositories.contexts.save(testSessionId, { key: 'item2', value: 'value2' });
516
+ repositories.contexts.save(testSessionId, { key: 'item3', value: 'value3' });
517
+ const result = await mockContextWatchHandler({
518
+ action: 'create',
519
+ });
520
+ const response = JSON.parse(result.content[0].text);
521
+ (0, globals_1.expect)(response.currentSequence).toBeGreaterThan(0);
522
+ });
523
+ (0, globals_1.it)('should generate unique watcher IDs', async () => {
524
+ const watcherIds = new Set();
525
+ for (let i = 0; i < 10; i++) {
526
+ const result = await mockContextWatchHandler({
527
+ action: 'create',
528
+ });
529
+ const response = JSON.parse(result.content[0].text);
530
+ watcherIds.add(response.watcherId);
531
+ }
532
+ (0, globals_1.expect)(watcherIds.size).toBe(10);
533
+ });
534
+ });
535
+ (0, globals_1.describe)('Change Detection - CREATE', () => {
536
+ (0, globals_1.it)('should detect newly created items', async () => {
537
+ // Create watcher
538
+ const createResult = await mockContextWatchHandler({
539
+ action: 'create',
540
+ });
541
+ const { watcherId } = JSON.parse(createResult.content[0].text);
542
+ // Add new items
543
+ repositories.contexts.save(testSessionId, {
544
+ key: 'new_item_1',
545
+ value: 'value1',
546
+ category: 'task',
547
+ channel: 'main',
548
+ });
549
+ repositories.contexts.save(testSessionId, {
550
+ key: 'new_item_2',
551
+ value: 'value2',
552
+ category: 'note',
553
+ channel: 'feature/ui',
554
+ });
555
+ // Poll for changes
556
+ const pollResult = await mockContextWatchHandler({
557
+ action: 'poll',
558
+ watcherId,
559
+ });
560
+ const response = JSON.parse(pollResult.content[0].text);
561
+ (0, globals_1.expect)(response.changes).toHaveLength(2);
562
+ (0, globals_1.expect)(response.changes[0].type).toBe('CREATE');
563
+ (0, globals_1.expect)(response.changes[0].key).toBe('new_item_1');
564
+ (0, globals_1.expect)(response.changes[0].value).toBe('value1');
565
+ (0, globals_1.expect)(response.changes[1].type).toBe('CREATE');
566
+ (0, globals_1.expect)(response.changes[1].key).toBe('new_item_2');
567
+ (0, globals_1.expect)(response.changes[1].value).toBe('value2');
568
+ });
569
+ (0, globals_1.it)('should only detect items matching key patterns', async () => {
570
+ // Create watcher with key filter
571
+ const createResult = await mockContextWatchHandler({
572
+ action: 'create',
573
+ filters: {
574
+ keys: ['user_*', '*_config'],
575
+ },
576
+ });
577
+ const { watcherId } = JSON.parse(createResult.content[0].text);
578
+ // Add items - some matching, some not
579
+ repositories.contexts.save(testSessionId, { key: 'user_profile', value: 'matches' });
580
+ repositories.contexts.save(testSessionId, { key: 'app_config', value: 'matches' });
581
+ repositories.contexts.save(testSessionId, { key: 'system_settings', value: 'no match' });
582
+ repositories.contexts.save(testSessionId, { key: 'user_preferences', value: 'matches' });
583
+ // Poll for changes
584
+ const pollResult = await mockContextWatchHandler({
585
+ action: 'poll',
586
+ watcherId,
587
+ });
588
+ const response = JSON.parse(pollResult.content[0].text);
589
+ (0, globals_1.expect)(response.changes).toHaveLength(3);
590
+ const keys = response.changes.map((c) => c.key);
591
+ (0, globals_1.expect)(keys).toContain('user_profile');
592
+ (0, globals_1.expect)(keys).toContain('app_config');
593
+ (0, globals_1.expect)(keys).toContain('user_preferences');
594
+ (0, globals_1.expect)(keys).not.toContain('system_settings');
595
+ });
596
+ (0, globals_1.it)('should filter by channels', async () => {
597
+ // Create watcher with channel filter
598
+ const createResult = await mockContextWatchHandler({
599
+ action: 'create',
600
+ filters: {
601
+ channels: ['main', 'feature/auth'],
602
+ },
603
+ });
604
+ const { watcherId } = JSON.parse(createResult.content[0].text);
605
+ // Add items to different channels
606
+ repositories.contexts.save(testSessionId, {
607
+ key: 'item1',
608
+ value: 'in main',
609
+ channel: 'main',
610
+ });
611
+ repositories.contexts.save(testSessionId, {
612
+ key: 'item2',
613
+ value: 'in feature/auth',
614
+ channel: 'feature/auth',
615
+ });
616
+ repositories.contexts.save(testSessionId, {
617
+ key: 'item3',
618
+ value: 'in feature/ui',
619
+ channel: 'feature/ui',
620
+ });
621
+ // Poll for changes
622
+ const pollResult = await mockContextWatchHandler({
623
+ action: 'poll',
624
+ watcherId,
625
+ });
626
+ const response = JSON.parse(pollResult.content[0].text);
627
+ (0, globals_1.expect)(response.changes).toHaveLength(2);
628
+ const keys = response.changes.map((c) => c.key);
629
+ (0, globals_1.expect)(keys).toContain('item1');
630
+ (0, globals_1.expect)(keys).toContain('item2');
631
+ (0, globals_1.expect)(keys).not.toContain('item3');
632
+ });
633
+ (0, globals_1.it)('should filter by categories', async () => {
634
+ // Create watcher with category filter
635
+ const createResult = await mockContextWatchHandler({
636
+ action: 'create',
637
+ filters: {
638
+ categories: ['task', 'error'],
639
+ },
640
+ });
641
+ const { watcherId } = JSON.parse(createResult.content[0].text);
642
+ // Add items with different categories
643
+ repositories.contexts.save(testSessionId, {
644
+ key: 'task1',
645
+ value: 'A task',
646
+ category: 'task',
647
+ });
648
+ repositories.contexts.save(testSessionId, {
649
+ key: 'note1',
650
+ value: 'A note',
651
+ category: 'note',
652
+ });
653
+ repositories.contexts.save(testSessionId, {
654
+ key: 'error1',
655
+ value: 'An error',
656
+ category: 'error',
657
+ });
658
+ // Poll for changes
659
+ const pollResult = await mockContextWatchHandler({
660
+ action: 'poll',
661
+ watcherId,
662
+ });
663
+ const response = JSON.parse(pollResult.content[0].text);
664
+ (0, globals_1.expect)(response.changes).toHaveLength(2);
665
+ const keys = response.changes.map((c) => c.key);
666
+ (0, globals_1.expect)(keys).toContain('task1');
667
+ (0, globals_1.expect)(keys).toContain('error1');
668
+ (0, globals_1.expect)(keys).not.toContain('note1');
669
+ });
670
+ (0, globals_1.it)('should apply combined filters correctly', async () => {
671
+ // Create watcher with multiple filters
672
+ const createResult = await mockContextWatchHandler({
673
+ action: 'create',
674
+ filters: {
675
+ keys: ['task_*'],
676
+ channels: ['main'],
677
+ categories: ['task'],
678
+ },
679
+ });
680
+ const { watcherId } = JSON.parse(createResult.content[0].text);
681
+ // Add various items
682
+ repositories.contexts.save(testSessionId, {
683
+ key: 'task_001',
684
+ value: 'matches all',
685
+ category: 'task',
686
+ channel: 'main',
687
+ });
688
+ repositories.contexts.save(testSessionId, {
689
+ key: 'task_002',
690
+ value: 'wrong channel',
691
+ category: 'task',
692
+ channel: 'feature/ui',
693
+ });
694
+ repositories.contexts.save(testSessionId, {
695
+ key: 'note_001',
696
+ value: 'wrong key pattern',
697
+ category: 'task',
698
+ channel: 'main',
699
+ });
700
+ repositories.contexts.save(testSessionId, {
701
+ key: 'task_003',
702
+ value: 'wrong category',
703
+ category: 'note',
704
+ channel: 'main',
705
+ });
706
+ // Poll for changes
707
+ const pollResult = await mockContextWatchHandler({
708
+ action: 'poll',
709
+ watcherId,
710
+ });
711
+ const response = JSON.parse(pollResult.content[0].text);
712
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
713
+ (0, globals_1.expect)(response.changes[0].key).toBe('task_001');
714
+ });
715
+ });
716
+ (0, globals_1.describe)('Change Detection - UPDATE', () => {
717
+ (0, globals_1.it)('should detect updated items', async () => {
718
+ // Create initial items
719
+ const itemId = (0, uuid_1.v4)();
720
+ const createTime = new Date(Date.now() - 1000); // 1 second ago
721
+ db.prepare(`INSERT INTO context_items
722
+ (id, session_id, key, value, created_at, updated_at, priority, is_private, size, channel, sequence_number)
723
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(itemId, testSessionId, 'update_test', 'original value', (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), 'normal', 0, 'original value'.length, 'general', 1);
724
+ // Create watcher
725
+ const createResult = await mockContextWatchHandler({
726
+ action: 'create',
727
+ });
728
+ const { watcherId } = JSON.parse(createResult.content[0].text);
729
+ // Update the item with a new timestamp
730
+ const updateTime = (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString());
731
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ?, sequence_number = ? WHERE id = ?').run('updated value', updateTime, 2, itemId);
732
+ // Poll for changes
733
+ const pollResult = await mockContextWatchHandler({
734
+ action: 'poll',
735
+ watcherId,
736
+ });
737
+ const response = JSON.parse(pollResult.content[0].text);
738
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
739
+ (0, globals_1.expect)(response.changes[0].type).toBe('UPDATE');
740
+ (0, globals_1.expect)(response.changes[0].key).toBe('update_test');
741
+ (0, globals_1.expect)(response.changes[0].value).toBe('updated value');
742
+ });
743
+ (0, globals_1.it)('should detect multiple updates to same item', async () => {
744
+ // Create initial item with older timestamp
745
+ const itemId = (0, uuid_1.v4)();
746
+ const createTime = new Date(Date.now() - 1000); // 1 second ago
747
+ db.prepare(`INSERT INTO context_items
748
+ (id, session_id, key, value, created_at, updated_at, priority, is_private, size, channel, sequence_number)
749
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(itemId, testSessionId, 'multi_update', 'version 1', (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), 'normal', 0, 'version 1'.length, 'general', 1);
750
+ // Create watcher
751
+ const createResult = await mockContextWatchHandler({
752
+ action: 'create',
753
+ });
754
+ const { watcherId } = JSON.parse(createResult.content[0].text);
755
+ // Update multiple times
756
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ?, sequence_number = ? WHERE id = ?').run('version 2', (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), 2, itemId);
757
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ?, sequence_number = ? WHERE id = ?').run('version 3', (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), 3, itemId);
758
+ // Poll for changes
759
+ const pollResult = await mockContextWatchHandler({
760
+ action: 'poll',
761
+ watcherId,
762
+ });
763
+ const response = JSON.parse(pollResult.content[0].text);
764
+ // In the mock implementation, we only see the final state
765
+ // In real implementation with context_changes table, we'd see all updates
766
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
767
+ (0, globals_1.expect)(response.changes[0].type).toBe('UPDATE');
768
+ (0, globals_1.expect)(response.changes[0].value).toBe('version 3');
769
+ });
770
+ });
771
+ (0, globals_1.describe)('Change Detection - DELETE', () => {
772
+ (0, globals_1.it)('should detect deleted items', async () => {
773
+ // Create initial items
774
+ repositories.contexts.save(testSessionId, {
775
+ key: 'delete_test_1',
776
+ value: 'will be deleted',
777
+ category: 'task',
778
+ channel: 'main',
779
+ });
780
+ repositories.contexts.save(testSessionId, {
781
+ key: 'delete_test_2',
782
+ value: 'also deleted',
783
+ category: 'note',
784
+ channel: 'main',
785
+ });
786
+ // Create watcher
787
+ const createResult = await mockContextWatchHandler({
788
+ action: 'create',
789
+ });
790
+ const { watcherId } = JSON.parse(createResult.content[0].text);
791
+ // Delete items and track in deleted_items table
792
+ const items = db
793
+ .prepare('SELECT * FROM context_items WHERE key IN (?, ?) AND session_id = ?')
794
+ .all('delete_test_1', 'delete_test_2', testSessionId);
795
+ for (const item of items) {
796
+ // Track deletion
797
+ db.prepare(`INSERT INTO deleted_items
798
+ (id, session_id, key, category, channel, sequence_number, deleted_at)
799
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, item.key, item.category, item.channel, item.sequence_number + 10, // Deletion gets new sequence
800
+ (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()));
801
+ // Delete the actual item
802
+ db.prepare('DELETE FROM context_items WHERE id = ?').run(item.id);
803
+ }
804
+ // Poll for changes
805
+ const pollResult = await mockContextWatchHandler({
806
+ action: 'poll',
807
+ watcherId,
808
+ });
809
+ const response = JSON.parse(pollResult.content[0].text);
810
+ (0, globals_1.expect)(response.changes).toHaveLength(2);
811
+ (0, globals_1.expect)(response.changes[0].type).toBe('DELETE');
812
+ (0, globals_1.expect)(response.changes[0].key).toBe('delete_test_1');
813
+ (0, globals_1.expect)(response.changes[0].value).toBeUndefined();
814
+ (0, globals_1.expect)(response.changes[1].type).toBe('DELETE');
815
+ (0, globals_1.expect)(response.changes[1].key).toBe('delete_test_2');
816
+ });
817
+ (0, globals_1.it)('should apply filters to deleted items', async () => {
818
+ // Create initial items
819
+ repositories.contexts.save(testSessionId, {
820
+ key: 'task_deleted',
821
+ value: 'deleted task',
822
+ category: 'task',
823
+ });
824
+ repositories.contexts.save(testSessionId, {
825
+ key: 'note_deleted',
826
+ value: 'deleted note',
827
+ category: 'note',
828
+ });
829
+ // Create watcher that only watches tasks
830
+ const createResult = await mockContextWatchHandler({
831
+ action: 'create',
832
+ filters: {
833
+ categories: ['task'],
834
+ },
835
+ });
836
+ const { watcherId } = JSON.parse(createResult.content[0].text);
837
+ // Delete both items
838
+ const items = db
839
+ .prepare('SELECT * FROM context_items WHERE key IN (?, ?) AND session_id = ?')
840
+ .all('task_deleted', 'note_deleted', testSessionId);
841
+ for (const item of items) {
842
+ db.prepare(`INSERT INTO deleted_items
843
+ (id, session_id, key, category, channel, sequence_number, deleted_at)
844
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, item.key, item.category, item.channel || 'general', item.sequence_number + 10, (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()));
845
+ db.prepare('DELETE FROM context_items WHERE id = ?').run(item.id);
846
+ }
847
+ // Poll for changes
848
+ const pollResult = await mockContextWatchHandler({
849
+ action: 'poll',
850
+ watcherId,
851
+ });
852
+ const response = JSON.parse(pollResult.content[0].text);
853
+ // Should only see the task deletion
854
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
855
+ (0, globals_1.expect)(response.changes[0].key).toBe('task_deleted');
856
+ });
857
+ });
858
+ (0, globals_1.describe)('Polling Mechanism', () => {
859
+ (0, globals_1.it)('should return empty changes when no updates', async () => {
860
+ // Create watcher
861
+ const createResult = await mockContextWatchHandler({
862
+ action: 'create',
863
+ });
864
+ const { watcherId } = JSON.parse(createResult.content[0].text);
865
+ // Poll without making changes
866
+ const pollResult = await mockContextWatchHandler({
867
+ action: 'poll',
868
+ watcherId,
869
+ });
870
+ const response = JSON.parse(pollResult.content[0].text);
871
+ (0, globals_1.expect)(response.changes).toHaveLength(0);
872
+ (0, globals_1.expect)(response.hasMore).toBe(false);
873
+ });
874
+ (0, globals_1.it)('should update last sequence number after polling', async () => {
875
+ // Create watcher
876
+ const createResult = await mockContextWatchHandler({
877
+ action: 'create',
878
+ });
879
+ const { watcherId, currentSequence } = JSON.parse(createResult.content[0].text);
880
+ // Add items
881
+ repositories.contexts.save(testSessionId, { key: 'item1', value: 'value1' });
882
+ repositories.contexts.save(testSessionId, { key: 'item2', value: 'value2' });
883
+ // Poll for changes
884
+ const pollResult = await mockContextWatchHandler({
885
+ action: 'poll',
886
+ watcherId,
887
+ });
888
+ const response = JSON.parse(pollResult.content[0].text);
889
+ (0, globals_1.expect)(response.lastSequence).toBeGreaterThan(currentSequence);
890
+ // Poll again - should see no changes
891
+ const secondPollResult = await mockContextWatchHandler({
892
+ action: 'poll',
893
+ watcherId,
894
+ });
895
+ const secondResponse = JSON.parse(secondPollResult.content[0].text);
896
+ (0, globals_1.expect)(secondResponse.changes).toHaveLength(0);
897
+ });
898
+ (0, globals_1.it)('should only return changes since last poll', async () => {
899
+ // Create watcher
900
+ const createResult = await mockContextWatchHandler({
901
+ action: 'create',
902
+ });
903
+ const { watcherId } = JSON.parse(createResult.content[0].text);
904
+ // Add first batch
905
+ repositories.contexts.save(testSessionId, { key: 'batch1_item1', value: 'value1' });
906
+ repositories.contexts.save(testSessionId, { key: 'batch1_item2', value: 'value2' });
907
+ // First poll
908
+ const firstPollResult = await mockContextWatchHandler({
909
+ action: 'poll',
910
+ watcherId,
911
+ });
912
+ const firstResponse = JSON.parse(firstPollResult.content[0].text);
913
+ (0, globals_1.expect)(firstResponse.changes).toHaveLength(2);
914
+ // Add second batch
915
+ repositories.contexts.save(testSessionId, { key: 'batch2_item1', value: 'value3' });
916
+ repositories.contexts.save(testSessionId, { key: 'batch2_item2', value: 'value4' });
917
+ // Second poll - should only see second batch
918
+ const secondPollResult = await mockContextWatchHandler({
919
+ action: 'poll',
920
+ watcherId,
921
+ });
922
+ const secondResponse = JSON.parse(secondPollResult.content[0].text);
923
+ (0, globals_1.expect)(secondResponse.changes).toHaveLength(2);
924
+ const keys = secondResponse.changes.map((c) => c.key);
925
+ (0, globals_1.expect)(keys).toContain('batch2_item1');
926
+ (0, globals_1.expect)(keys).toContain('batch2_item2');
927
+ (0, globals_1.expect)(keys).not.toContain('batch1_item1');
928
+ (0, globals_1.expect)(keys).not.toContain('batch1_item2');
929
+ });
930
+ (0, globals_1.it)('should handle mixed change types in single poll', async () => {
931
+ // Create initial item with older timestamp
932
+ const itemId = (0, uuid_1.v4)();
933
+ const createTime = new Date(Date.now() - 2000); // 2 seconds ago
934
+ db.prepare(`INSERT INTO context_items
935
+ (id, session_id, key, value, created_at, updated_at, priority, is_private, size, channel, sequence_number, category)
936
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(itemId, testSessionId, 'existing_item', 'original', (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), (0, timestamps_1.ensureSQLiteFormat)(createTime.toISOString()), 'normal', 0, 'original'.length, 'general', 1, 'task');
937
+ // Create watcher
938
+ const createResult = await mockContextWatchHandler({
939
+ action: 'create',
940
+ });
941
+ const { watcherId } = JSON.parse(createResult.content[0].text);
942
+ // Make various changes
943
+ // 1. Create new item
944
+ repositories.contexts.save(testSessionId, {
945
+ key: 'new_item',
946
+ value: 'created',
947
+ category: 'note',
948
+ });
949
+ // 2. Update existing item
950
+ db.prepare('UPDATE context_items SET value = ?, updated_at = ?, sequence_number = ? WHERE id = ?').run('updated', (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), 10, itemId);
951
+ // 3. Delete another item (create and delete)
952
+ repositories.contexts.save(testSessionId, {
953
+ key: 'to_delete',
954
+ value: 'temporary',
955
+ category: 'error',
956
+ });
957
+ const toDeleteItem = db
958
+ .prepare('SELECT * FROM context_items WHERE key = ? AND session_id = ?')
959
+ .get('to_delete', testSessionId);
960
+ db.prepare(`INSERT INTO deleted_items
961
+ (id, session_id, key, category, channel, sequence_number, deleted_at)
962
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, toDeleteItem.key, toDeleteItem.category, toDeleteItem.channel || 'general', 20, (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()));
963
+ db.prepare('DELETE FROM context_items WHERE id = ?').run(toDeleteItem.id);
964
+ // Poll for all changes
965
+ const pollResult = await mockContextWatchHandler({
966
+ action: 'poll',
967
+ watcherId,
968
+ });
969
+ const response = JSON.parse(pollResult.content[0].text);
970
+ // Should see all change types
971
+ const changeTypes = response.changes.map((c) => ({ type: c.type, key: c.key }));
972
+ (0, globals_1.expect)(changeTypes).toContainEqual({ type: 'CREATE', key: 'new_item' });
973
+ (0, globals_1.expect)(changeTypes).toContainEqual({ type: 'UPDATE', key: 'existing_item' });
974
+ // In mock implementation, deleted items only show as DELETE, not CREATE+DELETE
975
+ (0, globals_1.expect)(changeTypes).toContainEqual({ type: 'DELETE', key: 'to_delete' });
976
+ });
977
+ (0, globals_1.it)('should include metadata in change events', async () => {
978
+ // Create watcher
979
+ const createResult = await mockContextWatchHandler({
980
+ action: 'create',
981
+ });
982
+ const { watcherId } = JSON.parse(createResult.content[0].text);
983
+ // Add item with full metadata
984
+ repositories.contexts.save(testSessionId, {
985
+ key: 'metadata_test',
986
+ value: 'test value',
987
+ category: 'task',
988
+ channel: 'feature/test',
989
+ });
990
+ // Poll for changes
991
+ const pollResult = await mockContextWatchHandler({
992
+ action: 'poll',
993
+ watcherId,
994
+ });
995
+ const response = JSON.parse(pollResult.content[0].text);
996
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
997
+ const change = response.changes[0];
998
+ (0, globals_1.expect)(change).toHaveProperty('type', 'CREATE');
999
+ (0, globals_1.expect)(change).toHaveProperty('key', 'metadata_test');
1000
+ (0, globals_1.expect)(change).toHaveProperty('value', 'test value');
1001
+ (0, globals_1.expect)(change).toHaveProperty('category', 'task');
1002
+ (0, globals_1.expect)(change).toHaveProperty('channel', 'feature/test');
1003
+ (0, globals_1.expect)(change).toHaveProperty('sequence');
1004
+ (0, globals_1.expect)(change).toHaveProperty('timestamp');
1005
+ });
1006
+ });
1007
+ (0, globals_1.describe)('Watcher Lifecycle', () => {
1008
+ (0, globals_1.it)('should expire watchers after timeout', async () => {
1009
+ // Create watcher with past expiration
1010
+ const watcherId = `watch_${(0, uuid_1.v4)().substring(0, 8)}`;
1011
+ const pastExpiry = (0, timestamps_1.ensureSQLiteFormat)(new Date(Date.now() - 1000).toISOString());
1012
+ db.prepare(`INSERT INTO watchers (
1013
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
1014
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(watcherId, testSessionId, JSON.stringify({}), 0, (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), pastExpiry, 1);
1015
+ // Try to poll expired watcher
1016
+ const pollResult = await mockContextWatchHandler({
1017
+ action: 'poll',
1018
+ watcherId,
1019
+ });
1020
+ (0, globals_1.expect)(pollResult.content[0].text).toContain(`Error: Watcher expired: ${watcherId}`);
1021
+ // Verify watcher was marked inactive
1022
+ const watcher = db.prepare('SELECT * FROM watchers WHERE id = ?').get(watcherId);
1023
+ (0, globals_1.expect)(watcher.is_active).toBe(0);
1024
+ });
1025
+ (0, globals_1.it)('should extend expiration on successful poll', async () => {
1026
+ // Create watcher
1027
+ const createResult = await mockContextWatchHandler({
1028
+ action: 'create',
1029
+ });
1030
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1031
+ // Get initial expiration
1032
+ const initialWatcher = db
1033
+ .prepare('SELECT * FROM watchers WHERE id = ?')
1034
+ .get(watcherId);
1035
+ const initialExpiry = new Date(initialWatcher.expires_at.replace(' ', 'T') + 'Z');
1036
+ // Wait a bit to ensure timestamps are different
1037
+ await new Promise(resolve => setTimeout(resolve, 1000));
1038
+ // Poll
1039
+ await mockContextWatchHandler({
1040
+ action: 'poll',
1041
+ watcherId,
1042
+ });
1043
+ // Check new expiration
1044
+ const updatedWatcher = db
1045
+ .prepare('SELECT * FROM watchers WHERE id = ?')
1046
+ .get(watcherId);
1047
+ const newExpiry = new Date(updatedWatcher.expires_at.replace(' ', 'T') + 'Z');
1048
+ (0, globals_1.expect)(newExpiry.getTime()).toBeGreaterThan(initialExpiry.getTime());
1049
+ });
1050
+ (0, globals_1.it)('should stop watcher manually', async () => {
1051
+ // Create watcher
1052
+ const createResult = await mockContextWatchHandler({
1053
+ action: 'create',
1054
+ });
1055
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1056
+ // Stop watcher
1057
+ const stopResult = await mockContextWatchHandler({
1058
+ action: 'stop',
1059
+ watcherId,
1060
+ });
1061
+ const response = JSON.parse(stopResult.content[0].text);
1062
+ (0, globals_1.expect)(response.stopped).toBe(true);
1063
+ // Try to poll stopped watcher
1064
+ const pollResult = await mockContextWatchHandler({
1065
+ action: 'poll',
1066
+ watcherId,
1067
+ });
1068
+ (0, globals_1.expect)(pollResult.content[0].text).toContain(`Error: Watcher is stopped: ${watcherId}`);
1069
+ });
1070
+ (0, globals_1.it)('should handle stop on non-existent watcher', async () => {
1071
+ const result = await mockContextWatchHandler({
1072
+ action: 'stop',
1073
+ watcherId: 'watch_nonexistent',
1074
+ });
1075
+ (0, globals_1.expect)(result.content[0].text).toContain('Error: Watcher not found: watch_nonexistent');
1076
+ });
1077
+ (0, globals_1.it)('should clean up old watchers', async () => {
1078
+ // Create multiple watchers with different expiration times
1079
+ const now = new Date();
1080
+ // Expired watcher
1081
+ db.prepare(`INSERT INTO watchers (
1082
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
1083
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run('watch_expired', testSessionId, JSON.stringify({}), 0, (0, timestamps_1.ensureSQLiteFormat)(new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString()), (0, timestamps_1.ensureSQLiteFormat)(new Date(now.getTime() - 1 * 60 * 60 * 1000).toISOString()), 1);
1084
+ // Active watcher
1085
+ db.prepare(`INSERT INTO watchers (
1086
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
1087
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run('watch_active', testSessionId, JSON.stringify({}), 0, (0, timestamps_1.ensureSQLiteFormat)(now.toISOString()), (0, timestamps_1.ensureSQLiteFormat)(new Date(now.getTime() + 30 * 60 * 1000).toISOString()), 1);
1088
+ // Stopped watcher
1089
+ db.prepare(`INSERT INTO watchers (
1090
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
1091
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run('watch_stopped', testSessionId, JSON.stringify({}), 0, (0, timestamps_1.ensureSQLiteFormat)(now.toISOString()), (0, timestamps_1.ensureSQLiteFormat)(new Date(now.getTime() + 30 * 60 * 1000).toISOString()), 0);
1092
+ // List watchers
1093
+ const listResult = await mockContextWatchHandler({
1094
+ action: 'list',
1095
+ });
1096
+ const response = JSON.parse(listResult.content[0].text);
1097
+ (0, globals_1.expect)(response.total).toBe(3);
1098
+ const watcherMap = new Map(response.watchers.map((w) => [w.watcherId, w]));
1099
+ const expiredWatcher = watcherMap.get('watch_expired');
1100
+ const activeWatcher = watcherMap.get('watch_active');
1101
+ const stoppedWatcher = watcherMap.get('watch_stopped');
1102
+ (0, globals_1.expect)(expiredWatcher?.active).toBe(true); // Not automatically cleaned
1103
+ (0, globals_1.expect)(activeWatcher?.active).toBe(true);
1104
+ (0, globals_1.expect)(stoppedWatcher?.active).toBe(false);
1105
+ });
1106
+ });
1107
+ (0, globals_1.describe)('Privacy and Session Boundaries', () => {
1108
+ (0, globals_1.it)('should not detect changes from other sessions', async () => {
1109
+ // Create watcher in test session
1110
+ const createResult = await mockContextWatchHandler({
1111
+ action: 'create',
1112
+ });
1113
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1114
+ // Add items to different sessions
1115
+ repositories.contexts.save(testSessionId, {
1116
+ key: 'my_item',
1117
+ value: 'in my session',
1118
+ });
1119
+ repositories.contexts.save(otherSessionId, {
1120
+ key: 'other_item',
1121
+ value: 'in other session',
1122
+ });
1123
+ // Poll for changes
1124
+ const pollResult = await mockContextWatchHandler({
1125
+ action: 'poll',
1126
+ watcherId,
1127
+ });
1128
+ const response = JSON.parse(pollResult.content[0].text);
1129
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
1130
+ (0, globals_1.expect)(response.changes[0].key).toBe('my_item');
1131
+ });
1132
+ (0, globals_1.it)('should respect privacy boundaries for public items', async () => {
1133
+ // Create watcher in test session
1134
+ const createResult = await mockContextWatchHandler({
1135
+ action: 'create',
1136
+ });
1137
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1138
+ // Add public item from other session
1139
+ db.prepare(`INSERT INTO context_items
1140
+ (id, session_id, key, value, is_private, priority, size, channel, sequence_number)
1141
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), otherSessionId, 'public_item', 'public value', 0, // Public
1142
+ 'normal', 'public value'.length, 'general', 1);
1143
+ // Add private item from other session
1144
+ db.prepare(`INSERT INTO context_items
1145
+ (id, session_id, key, value, is_private, priority, size, channel, sequence_number)
1146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), otherSessionId, 'private_item', 'private value', 1, // Private
1147
+ 'normal', 'private value'.length, 'general', 2);
1148
+ // Poll for changes - mock implementation filters by session, so won't see cross-session items
1149
+ const pollResult = await mockContextWatchHandler({
1150
+ action: 'poll',
1151
+ watcherId,
1152
+ });
1153
+ const response = JSON.parse(pollResult.content[0].text);
1154
+ // In the mock implementation, session filtering prevents seeing other session's items
1155
+ // In the real implementation, public items would be visible
1156
+ (0, globals_1.expect)(response.changes).toHaveLength(0);
1157
+ });
1158
+ (0, globals_1.it)('should handle watcher not found error', async () => {
1159
+ const result = await mockContextWatchHandler({
1160
+ action: 'poll',
1161
+ watcherId: 'watch_nonexistent',
1162
+ });
1163
+ (0, globals_1.expect)(result.content[0].text).toContain('Error: Watcher not found: watch_nonexistent');
1164
+ });
1165
+ });
1166
+ (0, globals_1.describe)('Performance Scenarios', () => {
1167
+ (0, globals_1.it)('should handle many concurrent watchers', async () => {
1168
+ const watcherIds = [];
1169
+ // Create 50 watchers with different filters
1170
+ for (let i = 0; i < 50; i++) {
1171
+ const result = await mockContextWatchHandler({
1172
+ action: 'create',
1173
+ filters: {
1174
+ keys: [`pattern_${i}_*`],
1175
+ categories: i % 2 === 0 ? ['task'] : ['note'],
1176
+ },
1177
+ });
1178
+ const { watcherId } = JSON.parse(result.content[0].text);
1179
+ watcherIds.push(watcherId);
1180
+ }
1181
+ // Add items that match different watchers
1182
+ for (let i = 0; i < 10; i++) {
1183
+ repositories.contexts.save(testSessionId, {
1184
+ key: `pattern_${i}_item`,
1185
+ value: `value ${i}`,
1186
+ category: i % 2 === 0 ? 'task' : 'note',
1187
+ });
1188
+ }
1189
+ // Poll each watcher and verify they only see their items
1190
+ for (let i = 0; i < 10; i++) {
1191
+ const pollResult = await mockContextWatchHandler({
1192
+ action: 'poll',
1193
+ watcherId: watcherIds[i],
1194
+ });
1195
+ const response = JSON.parse(pollResult.content[0].text);
1196
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
1197
+ (0, globals_1.expect)(response.changes[0].key).toBe(`pattern_${i}_item`);
1198
+ }
1199
+ });
1200
+ (0, globals_1.it)('should handle large change sets efficiently', async () => {
1201
+ // Create watcher
1202
+ const createResult = await mockContextWatchHandler({
1203
+ action: 'create',
1204
+ });
1205
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1206
+ // Add 1000 items
1207
+ const startTime = Date.now();
1208
+ for (let i = 0; i < 1000; i++) {
1209
+ db.prepare(`INSERT INTO context_items
1210
+ (id, session_id, key, value, priority, is_private, size, channel, sequence_number)
1211
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, `bulk_item_${i.toString().padStart(4, '0')}`, `value ${i}`, 'normal', 0, `value ${i}`.length, 'general', i + 1);
1212
+ }
1213
+ const insertTime = Date.now() - startTime;
1214
+ // Poll for all changes
1215
+ const pollStartTime = Date.now();
1216
+ const pollResult = await mockContextWatchHandler({
1217
+ action: 'poll',
1218
+ watcherId,
1219
+ });
1220
+ const pollTime = Date.now() - pollStartTime;
1221
+ const response = JSON.parse(pollResult.content[0].text);
1222
+ (0, globals_1.expect)(response.changes).toHaveLength(1000);
1223
+ (0, globals_1.expect)(response.lastSequence).toBe(1000);
1224
+ // Performance assertions
1225
+ (0, globals_1.expect)(insertTime).toBeLessThan(5000); // Insert should be fast
1226
+ (0, globals_1.expect)(pollTime).toBeLessThan(1000); // Poll should be under 1 second
1227
+ });
1228
+ (0, globals_1.it)('should track sequence numbers correctly under high concurrency', async () => {
1229
+ // Create watcher
1230
+ const createResult = await mockContextWatchHandler({
1231
+ action: 'create',
1232
+ });
1233
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1234
+ // Simulate concurrent inserts
1235
+ const promises = [];
1236
+ for (let i = 0; i < 100; i++) {
1237
+ promises.push(repositories.contexts.save(testSessionId, {
1238
+ key: `concurrent_${i}`,
1239
+ value: `value ${i}`,
1240
+ }));
1241
+ }
1242
+ await Promise.all(promises);
1243
+ // Poll for changes
1244
+ const pollResult = await mockContextWatchHandler({
1245
+ action: 'poll',
1246
+ watcherId,
1247
+ });
1248
+ const response = JSON.parse(pollResult.content[0].text);
1249
+ // Verify all items were captured
1250
+ (0, globals_1.expect)(response.changes).toHaveLength(100);
1251
+ // Verify sequence numbers are unique and sequential
1252
+ const sequences = response.changes
1253
+ .map((c) => c.sequence)
1254
+ .sort((a, b) => a - b);
1255
+ for (let i = 1; i < sequences.length; i++) {
1256
+ (0, globals_1.expect)(sequences[i]).toBeGreaterThan(sequences[i - 1]);
1257
+ }
1258
+ });
1259
+ });
1260
+ (0, globals_1.describe)('Edge Cases', () => {
1261
+ (0, globals_1.it)('should handle empty database gracefully', async () => {
1262
+ // Create watcher on empty database
1263
+ const createResult = await mockContextWatchHandler({
1264
+ action: 'create',
1265
+ });
1266
+ const { watcherId, currentSequence } = JSON.parse(createResult.content[0].text);
1267
+ (0, globals_1.expect)(currentSequence).toBe(0);
1268
+ // Poll on empty database
1269
+ const pollResult = await mockContextWatchHandler({
1270
+ action: 'poll',
1271
+ watcherId,
1272
+ });
1273
+ const response = JSON.parse(pollResult.content[0].text);
1274
+ (0, globals_1.expect)(response.changes).toHaveLength(0);
1275
+ });
1276
+ (0, globals_1.it)('should handle special characters in patterns', async () => {
1277
+ // Create watcher with special pattern
1278
+ const createResult = await mockContextWatchHandler({
1279
+ action: 'create',
1280
+ filters: {
1281
+ keys: ['user_*_config', 'system.*.settings', 'data?'],
1282
+ },
1283
+ });
1284
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1285
+ // Add items
1286
+ repositories.contexts.save(testSessionId, { key: 'user_admin_config', value: 'matches' });
1287
+ repositories.contexts.save(testSessionId, { key: 'system.app.settings', value: 'matches' });
1288
+ // Brackets are not allowed in keys, so use a different pattern
1289
+ repositories.contexts.save(testSessionId, { key: 'data1', value: 'matches' });
1290
+ repositories.contexts.save(testSessionId, { key: 'data10', value: 'no match' });
1291
+ // Poll
1292
+ const pollResult = await mockContextWatchHandler({
1293
+ action: 'poll',
1294
+ watcherId,
1295
+ });
1296
+ const response = JSON.parse(pollResult.content[0].text);
1297
+ (0, globals_1.expect)(response.changes).toHaveLength(3);
1298
+ const keys = response.changes.map((c) => c.key);
1299
+ (0, globals_1.expect)(keys).toContain('user_admin_config');
1300
+ (0, globals_1.expect)(keys).toContain('system.app.settings');
1301
+ (0, globals_1.expect)(keys).toContain('data1');
1302
+ });
1303
+ (0, globals_1.it)('should handle very long values', async () => {
1304
+ // Create watcher
1305
+ const createResult = await mockContextWatchHandler({
1306
+ action: 'create',
1307
+ });
1308
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1309
+ // Add item with very long value
1310
+ const longValue = 'A'.repeat(10000);
1311
+ repositories.contexts.save(testSessionId, {
1312
+ key: 'long_value_item',
1313
+ value: longValue,
1314
+ });
1315
+ // Poll
1316
+ const pollResult = await mockContextWatchHandler({
1317
+ action: 'poll',
1318
+ watcherId,
1319
+ });
1320
+ const response = JSON.parse(pollResult.content[0].text);
1321
+ (0, globals_1.expect)(response.changes).toHaveLength(1);
1322
+ (0, globals_1.expect)(response.changes[0].value).toBe(longValue);
1323
+ (0, globals_1.expect)(response.changes[0].value.length).toBe(10000);
1324
+ });
1325
+ (0, globals_1.it)('should handle database errors gracefully', async () => {
1326
+ // Create watcher
1327
+ const createResult = await mockContextWatchHandler({
1328
+ action: 'create',
1329
+ });
1330
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1331
+ // Close database to simulate error
1332
+ dbManager.close();
1333
+ // Try to poll
1334
+ const pollResult = await mockContextWatchHandler({
1335
+ action: 'poll',
1336
+ watcherId,
1337
+ });
1338
+ (0, globals_1.expect)(pollResult.content[0].text).toContain('Error:');
1339
+ });
1340
+ });
1341
+ (0, globals_1.describe)('List Watchers', () => {
1342
+ (0, globals_1.it)('should list all watchers for session', async () => {
1343
+ // Create multiple watchers
1344
+ const watcherData = [];
1345
+ for (let i = 0; i < 5; i++) {
1346
+ const result = await mockContextWatchHandler({
1347
+ action: 'create',
1348
+ filters: {
1349
+ categories: i % 2 === 0 ? ['task'] : ['note'],
1350
+ },
1351
+ });
1352
+ const data = JSON.parse(result.content[0].text);
1353
+ watcherData.push(data);
1354
+ }
1355
+ // List watchers
1356
+ const listResult = await mockContextWatchHandler({
1357
+ action: 'list',
1358
+ });
1359
+ const response = JSON.parse(listResult.content[0].text);
1360
+ (0, globals_1.expect)(response.total).toBe(5);
1361
+ (0, globals_1.expect)(response.watchers).toHaveLength(5);
1362
+ // Verify watcher details
1363
+ for (const watcher of response.watchers) {
1364
+ (0, globals_1.expect)(watcher).toHaveProperty('watcherId');
1365
+ (0, globals_1.expect)(watcher).toHaveProperty('active', true);
1366
+ (0, globals_1.expect)(watcher).toHaveProperty('filters');
1367
+ (0, globals_1.expect)(watcher).toHaveProperty('lastSequence');
1368
+ (0, globals_1.expect)(watcher).toHaveProperty('createdAt');
1369
+ (0, globals_1.expect)(watcher).toHaveProperty('expiresAt');
1370
+ }
1371
+ });
1372
+ (0, globals_1.it)('should show mixed active/inactive watchers', async () => {
1373
+ // Create watchers
1374
+ const activeResult = await mockContextWatchHandler({
1375
+ action: 'create',
1376
+ });
1377
+ const { watcherId: activeId } = JSON.parse(activeResult.content[0].text);
1378
+ const toStopResult = await mockContextWatchHandler({
1379
+ action: 'create',
1380
+ });
1381
+ const { watcherId: stoppedId } = JSON.parse(toStopResult.content[0].text);
1382
+ // Stop one watcher
1383
+ await mockContextWatchHandler({
1384
+ action: 'stop',
1385
+ watcherId: stoppedId,
1386
+ });
1387
+ // List watchers
1388
+ const listResult = await mockContextWatchHandler({
1389
+ action: 'list',
1390
+ });
1391
+ const response = JSON.parse(listResult.content[0].text);
1392
+ (0, globals_1.expect)(response.total).toBe(2);
1393
+ const watcherMap = new Map(response.watchers.map((w) => [w.watcherId, w]));
1394
+ const activeWatcher = watcherMap.get(activeId);
1395
+ const stoppedWatcher = watcherMap.get(stoppedId);
1396
+ (0, globals_1.expect)(activeWatcher?.active).toBe(true);
1397
+ (0, globals_1.expect)(stoppedWatcher?.active).toBe(false);
1398
+ });
1399
+ (0, globals_1.it)('should not list watchers from other sessions', async () => {
1400
+ // Create watcher in test session
1401
+ await mockContextWatchHandler({
1402
+ action: 'create',
1403
+ });
1404
+ // Create watcher in other session
1405
+ db.prepare(`INSERT INTO watchers (
1406
+ id, session_id, filters, last_sequence, created_at, expires_at, is_active
1407
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run('watch_other', otherSessionId, JSON.stringify({}), 0, (0, timestamps_1.ensureSQLiteFormat)(new Date().toISOString()), (0, timestamps_1.ensureSQLiteFormat)(new Date(Date.now() + 30 * 60 * 1000).toISOString()), 1);
1408
+ // List watchers
1409
+ const listResult = await mockContextWatchHandler({
1410
+ action: 'list',
1411
+ });
1412
+ const response = JSON.parse(listResult.content[0].text);
1413
+ (0, globals_1.expect)(response.total).toBe(1);
1414
+ (0, globals_1.expect)(response.watchers[0].watcherId).not.toBe('watch_other');
1415
+ });
1416
+ });
1417
+ (0, globals_1.describe)('Complex Filtering Scenarios', () => {
1418
+ (0, globals_1.it)('should handle overlapping filters correctly', async () => {
1419
+ // Create watchers with overlapping filters
1420
+ const watcher1Result = await mockContextWatchHandler({
1421
+ action: 'create',
1422
+ filters: {
1423
+ keys: ['task_*'],
1424
+ categories: ['task'],
1425
+ },
1426
+ });
1427
+ const { watcherId: watcher1 } = JSON.parse(watcher1Result.content[0].text);
1428
+ const watcher2Result = await mockContextWatchHandler({
1429
+ action: 'create',
1430
+ filters: {
1431
+ keys: ['*_important'],
1432
+ categories: ['task', 'decision'],
1433
+ },
1434
+ });
1435
+ const { watcherId: watcher2 } = JSON.parse(watcher2Result.content[0].text);
1436
+ // Add items that match different combinations
1437
+ repositories.contexts.save(testSessionId, {
1438
+ key: 'task_important',
1439
+ value: 'matches both',
1440
+ category: 'task',
1441
+ });
1442
+ repositories.contexts.save(testSessionId, {
1443
+ key: 'task_regular',
1444
+ value: 'matches watcher1 only',
1445
+ category: 'task',
1446
+ });
1447
+ repositories.contexts.save(testSessionId, {
1448
+ key: 'note_important',
1449
+ value: 'matches watcher2 only',
1450
+ category: 'decision',
1451
+ });
1452
+ // Poll watcher1
1453
+ const poll1Result = await mockContextWatchHandler({
1454
+ action: 'poll',
1455
+ watcherId: watcher1,
1456
+ });
1457
+ const response1 = JSON.parse(poll1Result.content[0].text);
1458
+ (0, globals_1.expect)(response1.changes).toHaveLength(2);
1459
+ const keys1 = response1.changes.map((c) => c.key);
1460
+ (0, globals_1.expect)(keys1).toContain('task_important');
1461
+ (0, globals_1.expect)(keys1).toContain('task_regular');
1462
+ // Poll watcher2
1463
+ const poll2Result = await mockContextWatchHandler({
1464
+ action: 'poll',
1465
+ watcherId: watcher2,
1466
+ });
1467
+ const response2 = JSON.parse(poll2Result.content[0].text);
1468
+ (0, globals_1.expect)(response2.changes).toHaveLength(2);
1469
+ const keys2 = response2.changes.map((c) => c.key);
1470
+ (0, globals_1.expect)(keys2).toContain('task_important');
1471
+ (0, globals_1.expect)(keys2).toContain('note_important');
1472
+ });
1473
+ (0, globals_1.it)('should handle negation patterns correctly', async () => {
1474
+ // Create watcher that excludes certain patterns
1475
+ const createResult = await mockContextWatchHandler({
1476
+ action: 'create',
1477
+ filters: {
1478
+ keys: ['config_*', '!config_*_backup'], // Hypothetical negation syntax
1479
+ },
1480
+ });
1481
+ const { watcherId } = JSON.parse(createResult.content[0].text);
1482
+ // Add items
1483
+ repositories.contexts.save(testSessionId, { key: 'config_main', value: 'should match' });
1484
+ repositories.contexts.save(testSessionId, { key: 'config_user', value: 'should match' });
1485
+ repositories.contexts.save(testSessionId, {
1486
+ key: 'config_main_backup',
1487
+ value: 'should not match',
1488
+ });
1489
+ // Note: This test assumes negation pattern support, which may need implementation
1490
+ // For now, it will match all config_* patterns
1491
+ const pollResult = await mockContextWatchHandler({
1492
+ action: 'poll',
1493
+ watcherId,
1494
+ });
1495
+ const response = JSON.parse(pollResult.content[0].text);
1496
+ // Current implementation would return all 3
1497
+ (0, globals_1.expect)(response.changes.length).toBeGreaterThanOrEqual(2);
1498
+ });
1499
+ });
1500
+ });