@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.
- package/CHANGELOG.md +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +1054 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +411 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +291 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +305 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4425 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +2017 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +780 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- 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
|
+
});
|