mcp-memory-keeper 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1051 -0
- package/bin/mcp-memory-keeper +52 -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 +938 -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/cross-session-sharing.test.js +302 -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 +407 -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/git-integration.test.js +237 -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/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 +283 -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 +307 -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/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 +188 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -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 +4310 -0
- package/dist/index.phase1.backup.js +410 -0
- package/dist/index.phase2.backup.js +704 -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 +1873 -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/server.js +384 -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 +731 -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/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/package.json +84 -0
|
@@ -0,0 +1,544 @@
|
|
|
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 fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const database_1 = require("../../utils/database");
|
|
41
|
+
const database_helper_1 = require("../../test-helpers/database-helper");
|
|
42
|
+
/**
|
|
43
|
+
* Comprehensive test suite for validating watcher migrations at startup
|
|
44
|
+
*
|
|
45
|
+
* This test ensures that the watcher migration system (migrations 004 and 005)
|
|
46
|
+
* are properly applied and all required database components exist.
|
|
47
|
+
*
|
|
48
|
+
* SUCCESS CRITERIA:
|
|
49
|
+
* - All required tables exist (context_changes, context_watchers, deleted_items)
|
|
50
|
+
* - All required triggers are installed and functional
|
|
51
|
+
* - All required indexes are created for performance
|
|
52
|
+
* - Schema matches the expected structure from migration files
|
|
53
|
+
* - Triggers actually fire and record changes correctly
|
|
54
|
+
*/
|
|
55
|
+
(0, globals_1.describe)('Watcher Migration Validation Tests', () => {
|
|
56
|
+
let dbManager;
|
|
57
|
+
let db;
|
|
58
|
+
let tempDir;
|
|
59
|
+
let tempDbPath;
|
|
60
|
+
(0, globals_1.beforeEach)(() => {
|
|
61
|
+
// Create fresh test database for each test
|
|
62
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-watcher-test-'));
|
|
63
|
+
tempDbPath = path.join(tempDir, 'test.db');
|
|
64
|
+
// Initialize with DatabaseManager which should run all migrations
|
|
65
|
+
dbManager = new database_1.DatabaseManager({ filename: tempDbPath });
|
|
66
|
+
db = dbManager.getDatabase();
|
|
67
|
+
// Register for cleanup
|
|
68
|
+
database_helper_1.TestDatabaseHelper.registerForCleanup(dbManager);
|
|
69
|
+
});
|
|
70
|
+
(0, globals_1.afterEach)(() => {
|
|
71
|
+
try {
|
|
72
|
+
dbManager.close();
|
|
73
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn('Cleanup error:', error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
(0, globals_1.describe)('Required Tables Existence', () => {
|
|
80
|
+
(0, globals_1.it)('should have context_changes table with correct schema', () => {
|
|
81
|
+
// Check table exists
|
|
82
|
+
const tables = db
|
|
83
|
+
.prepare(`
|
|
84
|
+
SELECT name FROM sqlite_master
|
|
85
|
+
WHERE type='table' AND name='context_changes'
|
|
86
|
+
`)
|
|
87
|
+
.all();
|
|
88
|
+
(0, globals_1.expect)(tables).toHaveLength(1);
|
|
89
|
+
(0, globals_1.expect)(tables[0]).toEqual({ name: 'context_changes' });
|
|
90
|
+
// Check table schema matches migration 004
|
|
91
|
+
const columns = db.prepare('PRAGMA table_info(context_changes)').all();
|
|
92
|
+
const columnNames = columns.map(col => col.name);
|
|
93
|
+
// Verify all required columns from migration 004
|
|
94
|
+
const expectedColumns = [
|
|
95
|
+
'sequence_id',
|
|
96
|
+
'session_id',
|
|
97
|
+
'item_id',
|
|
98
|
+
'key',
|
|
99
|
+
'operation',
|
|
100
|
+
'old_value',
|
|
101
|
+
'new_value',
|
|
102
|
+
'old_metadata',
|
|
103
|
+
'new_metadata',
|
|
104
|
+
'category',
|
|
105
|
+
'priority',
|
|
106
|
+
'channel',
|
|
107
|
+
'size_delta',
|
|
108
|
+
'created_at',
|
|
109
|
+
'created_by',
|
|
110
|
+
];
|
|
111
|
+
expectedColumns.forEach(col => {
|
|
112
|
+
(0, globals_1.expect)(columnNames).toContain(col);
|
|
113
|
+
});
|
|
114
|
+
// Check sequence_id is primary key with autoincrement
|
|
115
|
+
const sequenceCol = columns.find(col => col.name === 'sequence_id');
|
|
116
|
+
(0, globals_1.expect)(sequenceCol.pk).toBe(1); // Primary key
|
|
117
|
+
(0, globals_1.expect)(sequenceCol.type).toBe('INTEGER');
|
|
118
|
+
// Check operation constraint
|
|
119
|
+
const operationCol = columns.find(col => col.name === 'operation');
|
|
120
|
+
(0, globals_1.expect)(operationCol.type).toBe('TEXT');
|
|
121
|
+
});
|
|
122
|
+
(0, globals_1.it)('should have context_watchers table with correct schema', () => {
|
|
123
|
+
// Check table exists
|
|
124
|
+
const tables = db
|
|
125
|
+
.prepare(`
|
|
126
|
+
SELECT name FROM sqlite_master
|
|
127
|
+
WHERE type='table' AND name='context_watchers'
|
|
128
|
+
`)
|
|
129
|
+
.all();
|
|
130
|
+
(0, globals_1.expect)(tables).toHaveLength(1);
|
|
131
|
+
// Check table schema
|
|
132
|
+
const columns = db.prepare('PRAGMA table_info(context_watchers)').all();
|
|
133
|
+
const columnNames = columns.map(col => col.name);
|
|
134
|
+
// Verify columns from migration 004
|
|
135
|
+
const expectedColumns = [
|
|
136
|
+
'id',
|
|
137
|
+
'session_id',
|
|
138
|
+
'filter_keys',
|
|
139
|
+
'filter_categories',
|
|
140
|
+
'filter_channels',
|
|
141
|
+
'filter_priorities',
|
|
142
|
+
'last_sequence',
|
|
143
|
+
'created_at',
|
|
144
|
+
'last_poll_at',
|
|
145
|
+
'expires_at',
|
|
146
|
+
'metadata',
|
|
147
|
+
];
|
|
148
|
+
expectedColumns.forEach(col => {
|
|
149
|
+
(0, globals_1.expect)(columnNames).toContain(col);
|
|
150
|
+
});
|
|
151
|
+
// Check id is primary key
|
|
152
|
+
const idCol = columns.find(col => col.name === 'id');
|
|
153
|
+
(0, globals_1.expect)(idCol.pk).toBe(1);
|
|
154
|
+
// Check for is_active column from migration 005
|
|
155
|
+
(0, globals_1.expect)(columnNames).toContain('is_active');
|
|
156
|
+
const isActiveCol = columns.find(col => col.name === 'is_active');
|
|
157
|
+
(0, globals_1.expect)(isActiveCol.dflt_value).toBe('1'); // Default to active
|
|
158
|
+
});
|
|
159
|
+
(0, globals_1.it)('should have deleted_items table from migration 005', () => {
|
|
160
|
+
// Check table exists
|
|
161
|
+
const tables = db
|
|
162
|
+
.prepare(`
|
|
163
|
+
SELECT name FROM sqlite_master
|
|
164
|
+
WHERE type='table' AND name='deleted_items'
|
|
165
|
+
`)
|
|
166
|
+
.all();
|
|
167
|
+
(0, globals_1.expect)(tables).toHaveLength(1);
|
|
168
|
+
// Check table schema
|
|
169
|
+
const columns = db.prepare('PRAGMA table_info(deleted_items)').all();
|
|
170
|
+
const columnNames = columns.map(col => col.name);
|
|
171
|
+
const expectedColumns = [
|
|
172
|
+
'id',
|
|
173
|
+
'session_id',
|
|
174
|
+
'key',
|
|
175
|
+
'category',
|
|
176
|
+
'channel',
|
|
177
|
+
'sequence_number',
|
|
178
|
+
'deleted_at',
|
|
179
|
+
];
|
|
180
|
+
expectedColumns.forEach(col => {
|
|
181
|
+
(0, globals_1.expect)(columnNames).toContain(col);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
(0, globals_1.it)('should have sequence_number column added to context_items', () => {
|
|
185
|
+
// Check that context_items table has sequence_number column from migration 005
|
|
186
|
+
const columns = db.prepare('PRAGMA table_info(context_items)').all();
|
|
187
|
+
const columnNames = columns.map(col => col.name);
|
|
188
|
+
(0, globals_1.expect)(columnNames).toContain('sequence_number');
|
|
189
|
+
const seqCol = columns.find(col => col.name === 'sequence_number');
|
|
190
|
+
(0, globals_1.expect)(seqCol.dflt_value).toBe('0'); // Default to 0
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
(0, globals_1.describe)('Required Indexes', () => {
|
|
194
|
+
(0, globals_1.it)('should have all performance indexes from migration 004', () => {
|
|
195
|
+
const indexes = db
|
|
196
|
+
.prepare(`
|
|
197
|
+
SELECT name FROM sqlite_master
|
|
198
|
+
WHERE type='index' AND name IN (
|
|
199
|
+
'idx_changes_sequence',
|
|
200
|
+
'idx_changes_session_seq',
|
|
201
|
+
'idx_changes_created',
|
|
202
|
+
'idx_watchers_expires',
|
|
203
|
+
'idx_watchers_session'
|
|
204
|
+
)
|
|
205
|
+
`)
|
|
206
|
+
.all();
|
|
207
|
+
const indexNames = indexes.map(idx => idx.name);
|
|
208
|
+
(0, globals_1.expect)(indexNames).toContain('idx_changes_sequence');
|
|
209
|
+
(0, globals_1.expect)(indexNames).toContain('idx_changes_session_seq');
|
|
210
|
+
(0, globals_1.expect)(indexNames).toContain('idx_changes_created');
|
|
211
|
+
(0, globals_1.expect)(indexNames).toContain('idx_watchers_expires');
|
|
212
|
+
(0, globals_1.expect)(indexNames).toContain('idx_watchers_session');
|
|
213
|
+
});
|
|
214
|
+
(0, globals_1.it)('should have indexes from migration 005', () => {
|
|
215
|
+
const indexes = db
|
|
216
|
+
.prepare(`
|
|
217
|
+
SELECT name FROM sqlite_master
|
|
218
|
+
WHERE type='index' AND name IN (
|
|
219
|
+
'idx_watchers_active',
|
|
220
|
+
'idx_deleted_items_session',
|
|
221
|
+
'idx_deleted_items_key'
|
|
222
|
+
)
|
|
223
|
+
`)
|
|
224
|
+
.all();
|
|
225
|
+
const indexNames = indexes.map(idx => idx.name);
|
|
226
|
+
(0, globals_1.expect)(indexNames).toContain('idx_watchers_active');
|
|
227
|
+
(0, globals_1.expect)(indexNames).toContain('idx_deleted_items_session');
|
|
228
|
+
(0, globals_1.expect)(indexNames).toContain('idx_deleted_items_key');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
(0, globals_1.describe)('Required Triggers', () => {
|
|
232
|
+
(0, globals_1.it)('should have change tracking triggers from migration 004', () => {
|
|
233
|
+
const triggers = db
|
|
234
|
+
.prepare(`
|
|
235
|
+
SELECT name FROM sqlite_master
|
|
236
|
+
WHERE type='trigger' AND name IN (
|
|
237
|
+
'track_context_insert',
|
|
238
|
+
'track_context_update',
|
|
239
|
+
'track_context_delete'
|
|
240
|
+
)
|
|
241
|
+
`)
|
|
242
|
+
.all();
|
|
243
|
+
const triggerNames = triggers.map(t => t.name);
|
|
244
|
+
(0, globals_1.expect)(triggerNames).toContain('track_context_insert');
|
|
245
|
+
(0, globals_1.expect)(triggerNames).toContain('track_context_update');
|
|
246
|
+
(0, globals_1.expect)(triggerNames).toContain('track_context_delete');
|
|
247
|
+
(0, globals_1.expect)(triggers).toHaveLength(3);
|
|
248
|
+
});
|
|
249
|
+
(0, globals_1.it)('should have sequence increment triggers from migration 005', () => {
|
|
250
|
+
const triggers = db
|
|
251
|
+
.prepare(`
|
|
252
|
+
SELECT name FROM sqlite_master
|
|
253
|
+
WHERE type='trigger' AND name IN (
|
|
254
|
+
'increment_sequence_insert',
|
|
255
|
+
'increment_sequence_update'
|
|
256
|
+
)
|
|
257
|
+
`)
|
|
258
|
+
.all();
|
|
259
|
+
const triggerNames = triggers.map(t => t.name);
|
|
260
|
+
(0, globals_1.expect)(triggerNames).toContain('increment_sequence_insert');
|
|
261
|
+
(0, globals_1.expect)(triggerNames).toContain('increment_sequence_update');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
(0, globals_1.describe)('Functional Trigger Testing', () => {
|
|
265
|
+
(0, globals_1.beforeEach)(() => {
|
|
266
|
+
// Create a test session for trigger testing
|
|
267
|
+
db.prepare(`
|
|
268
|
+
INSERT INTO sessions (id, name, description)
|
|
269
|
+
VALUES ('test-session', 'Test Session', 'Test Description')
|
|
270
|
+
`).run();
|
|
271
|
+
});
|
|
272
|
+
(0, globals_1.it)('should track INSERT operations with track_context_insert trigger', () => {
|
|
273
|
+
// Insert a context item
|
|
274
|
+
db.prepare(`
|
|
275
|
+
INSERT INTO context_items (
|
|
276
|
+
id, session_id, key, value, category, priority, channel, size
|
|
277
|
+
) VALUES (
|
|
278
|
+
'test-item-1', 'test-session', 'test-key', 'test-value',
|
|
279
|
+
'task', 'high', 'default', 10
|
|
280
|
+
)
|
|
281
|
+
`).run();
|
|
282
|
+
// Check that change was tracked
|
|
283
|
+
const changes = db
|
|
284
|
+
.prepare(`
|
|
285
|
+
SELECT * FROM context_changes
|
|
286
|
+
WHERE item_id = 'test-item-1' AND operation = 'CREATE'
|
|
287
|
+
`)
|
|
288
|
+
.all();
|
|
289
|
+
(0, globals_1.expect)(changes).toHaveLength(1);
|
|
290
|
+
const change = changes[0];
|
|
291
|
+
(0, globals_1.expect)(change.session_id).toBe('test-session');
|
|
292
|
+
(0, globals_1.expect)(change.key).toBe('test-key');
|
|
293
|
+
(0, globals_1.expect)(change.operation).toBe('CREATE');
|
|
294
|
+
(0, globals_1.expect)(change.new_value).toBe('test-value');
|
|
295
|
+
(0, globals_1.expect)(change.category).toBe('task');
|
|
296
|
+
(0, globals_1.expect)(change.priority).toBe('high');
|
|
297
|
+
(0, globals_1.expect)(change.channel).toBe('default');
|
|
298
|
+
(0, globals_1.expect)(change.size_delta).toBe(10);
|
|
299
|
+
(0, globals_1.expect)(change.created_by).toBe('context_save');
|
|
300
|
+
});
|
|
301
|
+
(0, globals_1.it)('should track UPDATE operations with track_context_update trigger', () => {
|
|
302
|
+
// Insert initial item
|
|
303
|
+
db.prepare(`
|
|
304
|
+
INSERT INTO context_items (
|
|
305
|
+
id, session_id, key, value, category, priority, size
|
|
306
|
+
) VALUES (
|
|
307
|
+
'test-item-2', 'test-session', 'update-key', 'old-value', 'note', 'low', 5
|
|
308
|
+
)
|
|
309
|
+
`).run();
|
|
310
|
+
// Update the item
|
|
311
|
+
db.prepare(`
|
|
312
|
+
UPDATE context_items
|
|
313
|
+
SET value = 'new-value', priority = 'high', size = 15
|
|
314
|
+
WHERE id = 'test-item-2'
|
|
315
|
+
`).run();
|
|
316
|
+
// Check that update was tracked
|
|
317
|
+
const changes = db
|
|
318
|
+
.prepare(`
|
|
319
|
+
SELECT * FROM context_changes
|
|
320
|
+
WHERE item_id = 'test-item-2' AND operation = 'UPDATE'
|
|
321
|
+
`)
|
|
322
|
+
.all();
|
|
323
|
+
(0, globals_1.expect)(changes).toHaveLength(1);
|
|
324
|
+
const change = changes[0];
|
|
325
|
+
(0, globals_1.expect)(change.old_value).toBe('old-value');
|
|
326
|
+
(0, globals_1.expect)(change.new_value).toBe('new-value');
|
|
327
|
+
(0, globals_1.expect)(change.size_delta).toBe(10); // 15 - 5
|
|
328
|
+
(0, globals_1.expect)(change.created_by).toBe('context_save');
|
|
329
|
+
});
|
|
330
|
+
(0, globals_1.it)('should track DELETE operations with track_context_delete trigger', () => {
|
|
331
|
+
// Insert item to delete
|
|
332
|
+
db.prepare(`
|
|
333
|
+
INSERT INTO context_items (
|
|
334
|
+
id, session_id, key, value, category, size
|
|
335
|
+
) VALUES (
|
|
336
|
+
'test-item-3', 'test-session', 'delete-key', 'delete-value', 'error', 8
|
|
337
|
+
)
|
|
338
|
+
`).run();
|
|
339
|
+
// Delete the item
|
|
340
|
+
db.prepare(`DELETE FROM context_items WHERE id = 'test-item-3'`).run();
|
|
341
|
+
// Check that deletion was tracked
|
|
342
|
+
const changes = db
|
|
343
|
+
.prepare(`
|
|
344
|
+
SELECT * FROM context_changes
|
|
345
|
+
WHERE item_id = 'test-item-3' AND operation = 'DELETE'
|
|
346
|
+
`)
|
|
347
|
+
.all();
|
|
348
|
+
(0, globals_1.expect)(changes).toHaveLength(1);
|
|
349
|
+
const change = changes[0];
|
|
350
|
+
(0, globals_1.expect)(change.old_value).toBe('delete-value');
|
|
351
|
+
(0, globals_1.expect)(change.new_value).toBeNull();
|
|
352
|
+
(0, globals_1.expect)(change.category).toBe('error');
|
|
353
|
+
(0, globals_1.expect)(change.size_delta).toBe(-8); // Negative for deletion
|
|
354
|
+
(0, globals_1.expect)(change.created_by).toBe('context_delete');
|
|
355
|
+
});
|
|
356
|
+
(0, globals_1.it)('should auto-increment sequence numbers on INSERT', () => {
|
|
357
|
+
// Insert multiple items
|
|
358
|
+
db.prepare(`
|
|
359
|
+
INSERT INTO context_items (
|
|
360
|
+
id, session_id, key, value, sequence_number
|
|
361
|
+
) VALUES
|
|
362
|
+
('seq-1', 'test-session', 'key1', 'value1', 0),
|
|
363
|
+
('seq-2', 'test-session', 'key2', 'value2', 0),
|
|
364
|
+
('seq-3', 'test-session', 'key3', 'value3', 0)
|
|
365
|
+
`).run();
|
|
366
|
+
// Check sequence numbers were assigned
|
|
367
|
+
const items = db
|
|
368
|
+
.prepare(`
|
|
369
|
+
SELECT id, sequence_number FROM context_items
|
|
370
|
+
WHERE session_id = 'test-session'
|
|
371
|
+
ORDER BY sequence_number
|
|
372
|
+
`)
|
|
373
|
+
.all();
|
|
374
|
+
(0, globals_1.expect)(items).toHaveLength(3);
|
|
375
|
+
(0, globals_1.expect)(items[0].sequence_number).toBeGreaterThan(0);
|
|
376
|
+
(0, globals_1.expect)(items[1].sequence_number).toBeGreaterThan(items[0].sequence_number);
|
|
377
|
+
(0, globals_1.expect)(items[2].sequence_number).toBeGreaterThan(items[1].sequence_number);
|
|
378
|
+
});
|
|
379
|
+
(0, globals_1.it)('should update sequence numbers on significant updates', () => {
|
|
380
|
+
// Insert item
|
|
381
|
+
db.prepare(`
|
|
382
|
+
INSERT INTO context_items (
|
|
383
|
+
id, session_id, key, value, sequence_number
|
|
384
|
+
) VALUES ('seq-update', 'test-session', 'update-key', 'old-value', 0)
|
|
385
|
+
`).run();
|
|
386
|
+
const originalSeq = db
|
|
387
|
+
.prepare(`
|
|
388
|
+
SELECT sequence_number FROM context_items WHERE id = 'seq-update'
|
|
389
|
+
`)
|
|
390
|
+
.get();
|
|
391
|
+
// Update the value (should trigger sequence increment)
|
|
392
|
+
db.prepare(`
|
|
393
|
+
UPDATE context_items SET value = 'new-value' WHERE id = 'seq-update'
|
|
394
|
+
`).run();
|
|
395
|
+
const newSeq = db
|
|
396
|
+
.prepare(`
|
|
397
|
+
SELECT sequence_number FROM context_items WHERE id = 'seq-update'
|
|
398
|
+
`)
|
|
399
|
+
.get();
|
|
400
|
+
(0, globals_1.expect)(newSeq.sequence_number).toBeGreaterThan(originalSeq.sequence_number);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
(0, globals_1.describe)('Edge Cases and Error Handling', () => {
|
|
404
|
+
(0, globals_1.it)('should handle missing foreign key relationships gracefully', () => {
|
|
405
|
+
// Try to insert change record with non-existent session
|
|
406
|
+
// This should fail due to foreign key constraint
|
|
407
|
+
(0, globals_1.expect)(() => {
|
|
408
|
+
db.prepare(`
|
|
409
|
+
INSERT INTO context_changes (
|
|
410
|
+
session_id, item_id, key, operation, new_value
|
|
411
|
+
) VALUES ('non-existent-session', 'item1', 'key1', 'CREATE', 'value1')
|
|
412
|
+
`).run();
|
|
413
|
+
}).toThrow();
|
|
414
|
+
});
|
|
415
|
+
(0, globals_1.it)('should enforce operation CHECK constraint', () => {
|
|
416
|
+
// Create session first
|
|
417
|
+
db.prepare(`
|
|
418
|
+
INSERT INTO sessions (id, name) VALUES ('constraint-test', 'Test')
|
|
419
|
+
`).run();
|
|
420
|
+
// Try invalid operation
|
|
421
|
+
(0, globals_1.expect)(() => {
|
|
422
|
+
db.prepare(`
|
|
423
|
+
INSERT INTO context_changes (
|
|
424
|
+
session_id, item_id, key, operation, new_value
|
|
425
|
+
) VALUES ('constraint-test', 'item1', 'key1', 'INVALID_OP', 'value1')
|
|
426
|
+
`).run();
|
|
427
|
+
}).toThrow();
|
|
428
|
+
// Valid operations should work
|
|
429
|
+
(0, globals_1.expect)(() => {
|
|
430
|
+
db.prepare(`
|
|
431
|
+
INSERT INTO context_changes (
|
|
432
|
+
session_id, item_id, key, operation, new_value
|
|
433
|
+
) VALUES ('constraint-test', 'item1', 'key1', 'CREATE', 'value1')
|
|
434
|
+
`).run();
|
|
435
|
+
}).not.toThrow();
|
|
436
|
+
});
|
|
437
|
+
(0, globals_1.it)('should handle NULL values correctly in trigger conditions', () => {
|
|
438
|
+
// Create session and item with NULL metadata
|
|
439
|
+
db.prepare(`
|
|
440
|
+
INSERT INTO sessions (id, name) VALUES ('null-test', 'Null Test')
|
|
441
|
+
`).run();
|
|
442
|
+
db.prepare(`
|
|
443
|
+
INSERT INTO context_items (
|
|
444
|
+
id, session_id, key, value, metadata, category
|
|
445
|
+
) VALUES ('null-item', 'null-test', 'null-key', 'value1', NULL, 'note')
|
|
446
|
+
`).run();
|
|
447
|
+
// Update to non-NULL metadata (should trigger update tracking)
|
|
448
|
+
db.prepare(`
|
|
449
|
+
UPDATE context_items
|
|
450
|
+
SET metadata = '{"test": true}'
|
|
451
|
+
WHERE id = 'null-item'
|
|
452
|
+
`).run();
|
|
453
|
+
// Check that change was tracked
|
|
454
|
+
const changes = db
|
|
455
|
+
.prepare(`
|
|
456
|
+
SELECT * FROM context_changes
|
|
457
|
+
WHERE item_id = 'null-item' AND operation = 'UPDATE'
|
|
458
|
+
`)
|
|
459
|
+
.all();
|
|
460
|
+
(0, globals_1.expect)(changes).toHaveLength(1);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
(0, globals_1.describe)('Migration Completeness', () => {
|
|
464
|
+
(0, globals_1.it)('should have applied all expected migrations', () => {
|
|
465
|
+
// Check migration records exist (if migration table exists)
|
|
466
|
+
const tables = db
|
|
467
|
+
.prepare(`
|
|
468
|
+
SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'
|
|
469
|
+
`)
|
|
470
|
+
.all();
|
|
471
|
+
if (tables.length > 0) {
|
|
472
|
+
const migrations = db
|
|
473
|
+
.prepare(`
|
|
474
|
+
SELECT version FROM migrations
|
|
475
|
+
WHERE version IN ('0.4.0', '0.5.0')
|
|
476
|
+
ORDER BY version
|
|
477
|
+
`)
|
|
478
|
+
.all();
|
|
479
|
+
// Should have both watcher migrations
|
|
480
|
+
(0, globals_1.expect)(migrations.map(m => m.version)).toEqual(['0.4.0', '0.5.0']);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
(0, globals_1.it)('should handle database with partial watcher schema gracefully', () => {
|
|
484
|
+
// This test verifies that migrations are idempotent
|
|
485
|
+
// and can handle databases in various states
|
|
486
|
+
// Check that all expected components exist
|
|
487
|
+
const expectedTables = ['context_changes', 'context_watchers', 'deleted_items'];
|
|
488
|
+
const existingTables = db
|
|
489
|
+
.prepare(`
|
|
490
|
+
SELECT name FROM sqlite_master
|
|
491
|
+
WHERE type='table' AND name IN (${expectedTables.map(() => '?').join(',')})
|
|
492
|
+
`)
|
|
493
|
+
.all(...expectedTables);
|
|
494
|
+
(0, globals_1.expect)(existingTables).toHaveLength(expectedTables.length);
|
|
495
|
+
// Check that all expected triggers exist
|
|
496
|
+
const expectedTriggers = [
|
|
497
|
+
'track_context_insert',
|
|
498
|
+
'track_context_update',
|
|
499
|
+
'track_context_delete',
|
|
500
|
+
'increment_sequence_insert',
|
|
501
|
+
'increment_sequence_update',
|
|
502
|
+
];
|
|
503
|
+
const existingTriggers = db
|
|
504
|
+
.prepare(`
|
|
505
|
+
SELECT name FROM sqlite_master
|
|
506
|
+
WHERE type='trigger' AND name IN (${expectedTriggers.map(() => '?').join(',')})
|
|
507
|
+
`)
|
|
508
|
+
.all(...expectedTriggers);
|
|
509
|
+
(0, globals_1.expect)(existingTriggers).toHaveLength(expectedTriggers.length);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
(0, globals_1.describe)('Performance Validation', () => {
|
|
513
|
+
(0, globals_1.it)('should have indexes that improve query performance', () => {
|
|
514
|
+
// Create test data to verify index usage
|
|
515
|
+
db.prepare(`
|
|
516
|
+
INSERT INTO sessions (id, name) VALUES ('perf-test', 'Performance Test')
|
|
517
|
+
`).run();
|
|
518
|
+
// Insert some test data
|
|
519
|
+
for (let i = 0; i < 100; i++) {
|
|
520
|
+
db.prepare(`
|
|
521
|
+
INSERT INTO context_changes (
|
|
522
|
+
session_id, item_id, key, operation, new_value, created_at
|
|
523
|
+
) VALUES (
|
|
524
|
+
'perf-test', 'item-${i}', 'key-${i}', 'CREATE', 'value-${i}',
|
|
525
|
+
datetime('now', '-${i} seconds')
|
|
526
|
+
)
|
|
527
|
+
`).run();
|
|
528
|
+
}
|
|
529
|
+
// Query that should use index
|
|
530
|
+
const explain = db
|
|
531
|
+
.prepare(`
|
|
532
|
+
EXPLAIN QUERY PLAN
|
|
533
|
+
SELECT * FROM context_changes
|
|
534
|
+
WHERE session_id = 'perf-test'
|
|
535
|
+
ORDER BY sequence_id DESC
|
|
536
|
+
LIMIT 10
|
|
537
|
+
`)
|
|
538
|
+
.all();
|
|
539
|
+
// Should mention index usage (not exact string match due to SQLite variations)
|
|
540
|
+
const hasIndexUsage = explain.some(row => row.detail.includes('idx_changes_session_seq') || row.detail.includes('USING INDEX'));
|
|
541
|
+
(0, globals_1.expect)(hasIndexUsage).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
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 fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const database_1 = require("../../utils/database");
|
|
41
|
+
const validation_1 = require("../../utils/validation");
|
|
42
|
+
(0, globals_1.describe)('Security - Input Validation Tests', () => {
|
|
43
|
+
let tempDir;
|
|
44
|
+
let dbManager;
|
|
45
|
+
(0, globals_1.beforeEach)(() => {
|
|
46
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-'));
|
|
47
|
+
const tempDbPath = path.join(tempDir, 'test.db');
|
|
48
|
+
dbManager = new database_1.DatabaseManager({
|
|
49
|
+
filename: tempDbPath,
|
|
50
|
+
walMode: false,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
(0, globals_1.afterEach)(() => {
|
|
54
|
+
dbManager.close();
|
|
55
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
(0, globals_1.describe)('Basic SQL Injection Prevention', () => {
|
|
58
|
+
(0, globals_1.it)('should prevent SQL injection in key parameter', () => {
|
|
59
|
+
const db = dbManager.getDatabase();
|
|
60
|
+
const sessionId = 'test-session';
|
|
61
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
|
|
62
|
+
const maliciousKey = "key'; DROP TABLE sessions; --";
|
|
63
|
+
db.prepare(`
|
|
64
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
66
|
+
`).run('item-1', sessionId, maliciousKey, 'value', 'test', 'normal');
|
|
67
|
+
const tables = db
|
|
68
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'")
|
|
69
|
+
.all();
|
|
70
|
+
(0, globals_1.expect)(tables.length).toBe(1);
|
|
71
|
+
const item = db.prepare('SELECT key FROM context_items WHERE id = ?').get('item-1');
|
|
72
|
+
(0, globals_1.expect)(item.key).toBe(maliciousKey);
|
|
73
|
+
});
|
|
74
|
+
(0, globals_1.it)('should sanitize search queries', () => {
|
|
75
|
+
const maliciousQuery = "test' OR '1'='1";
|
|
76
|
+
const validatedQuery = (0, validation_1.validateSearchQuery)(maliciousQuery);
|
|
77
|
+
(0, globals_1.expect)(validatedQuery).toBe('test OR 1=1');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(0, globals_1.describe)('Basic Validation', () => {
|
|
81
|
+
(0, globals_1.it)('should validate keys', () => {
|
|
82
|
+
(0, globals_1.expect)(() => (0, validation_1.validateKey)('')).toThrow();
|
|
83
|
+
(0, globals_1.expect)(() => (0, validation_1.validateKey)('valid-key')).not.toThrow();
|
|
84
|
+
});
|
|
85
|
+
(0, globals_1.it)('should validate values', () => {
|
|
86
|
+
(0, globals_1.expect)(() => (0, validation_1.validateValue)('valid value')).not.toThrow();
|
|
87
|
+
// Empty values might be allowed, so just test that function exists
|
|
88
|
+
(0, globals_1.expect)(typeof validation_1.validateValue).toBe('function');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
(0, globals_1.describe)('Resource Management', () => {
|
|
92
|
+
(0, globals_1.it)('should handle small bulk operations', () => {
|
|
93
|
+
const db = dbManager.getDatabase();
|
|
94
|
+
const sessionId = 'test-session';
|
|
95
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
|
|
96
|
+
const insertStmt = db.prepare(`
|
|
97
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority)
|
|
98
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
99
|
+
`);
|
|
100
|
+
const insertMany = db.transaction(() => {
|
|
101
|
+
for (let i = 0; i < 5; i++) {
|
|
102
|
+
insertStmt.run(`bulk-${i}`, sessionId, `key-${i}`, `value-${i}`, 'test', 'normal');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
insertMany();
|
|
107
|
+
const elapsed = Date.now() - start;
|
|
108
|
+
(0, globals_1.expect)(elapsed).toBeLessThan(1000);
|
|
109
|
+
const count = db
|
|
110
|
+
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
|
|
111
|
+
.get(sessionId);
|
|
112
|
+
(0, globals_1.expect)(count.count).toBe(5);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|