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.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,1151 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const globals_1 = require("@jest/globals");
37
+ const database_1 = require("../../utils/database");
38
+ const ContextRepository_1 = require("../../repositories/ContextRepository");
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const uuid_1 = require("uuid");
43
+ const validation_1 = require("../../utils/validation");
44
+ (0, globals_1.describe)('Context Relationships Handler Integration Tests', () => {
45
+ let dbManager;
46
+ let tempDbPath;
47
+ let db;
48
+ let _contextRepo;
49
+ let testSessionId;
50
+ let secondSessionId;
51
+ (0, globals_1.beforeEach)(() => {
52
+ tempDbPath = path.join(os.tmpdir(), `test-context-relationships-${Date.now()}.db`);
53
+ dbManager = new database_1.DatabaseManager({
54
+ filename: tempDbPath,
55
+ maxSize: 10 * 1024 * 1024,
56
+ walMode: true,
57
+ });
58
+ db = dbManager.getDatabase();
59
+ _contextRepo = new ContextRepository_1.ContextRepository(dbManager);
60
+ // Create test sessions
61
+ testSessionId = (0, uuid_1.v4)();
62
+ secondSessionId = (0, uuid_1.v4)();
63
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
64
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(secondSessionId, 'Second Session');
65
+ // Create relationships table if it doesn't exist
66
+ db.prepare(`
67
+ CREATE TABLE IF NOT EXISTS context_relationships (
68
+ id TEXT PRIMARY KEY,
69
+ session_id TEXT NOT NULL,
70
+ from_key TEXT NOT NULL,
71
+ to_key TEXT NOT NULL,
72
+ relationship_type TEXT NOT NULL,
73
+ metadata TEXT,
74
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
75
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
76
+ UNIQUE(session_id, from_key, to_key, relationship_type)
77
+ )
78
+ `).run();
79
+ // Create indexes for performance
80
+ db.prepare('CREATE INDEX IF NOT EXISTS idx_relationships_from ON context_relationships(session_id, from_key)').run();
81
+ db.prepare('CREATE INDEX IF NOT EXISTS idx_relationships_to ON context_relationships(session_id, to_key)').run();
82
+ db.prepare('CREATE INDEX IF NOT EXISTS idx_relationships_type ON context_relationships(relationship_type)').run();
83
+ });
84
+ (0, globals_1.afterEach)(() => {
85
+ dbManager.close();
86
+ try {
87
+ fs.unlinkSync(tempDbPath);
88
+ fs.unlinkSync(`${tempDbPath}-wal`);
89
+ fs.unlinkSync(`${tempDbPath}-shm`);
90
+ }
91
+ catch (_e) {
92
+ // Ignore
93
+ }
94
+ });
95
+ function createTestItems() {
96
+ const items = [
97
+ // Project structure
98
+ { key: 'project.auth', value: 'Authentication module' },
99
+ { key: 'project.database', value: 'Database layer' },
100
+ { key: 'project.api', value: 'API endpoints' },
101
+ { key: 'project.frontend', value: 'Frontend application' },
102
+ // Components
103
+ { key: 'component.login', value: 'Login component' },
104
+ { key: 'component.dashboard', value: 'Dashboard component' },
105
+ { key: 'component.user_profile', value: 'User profile component' },
106
+ // Tasks
107
+ { key: 'task.implement_auth', value: 'Implement authentication', category: 'task' },
108
+ { key: 'task.setup_db', value: 'Setup database', category: 'task' },
109
+ { key: 'task.create_api', value: 'Create API endpoints', category: 'task' },
110
+ // Decisions
111
+ { key: 'decision.use_oauth', value: 'Use OAuth2 for authentication', category: 'decision' },
112
+ { key: 'decision.postgres', value: 'Use PostgreSQL for database', category: 'decision' },
113
+ // Notes
114
+ { key: 'note.security', value: 'Security considerations for auth', category: 'note' },
115
+ { key: 'note.performance', value: 'Performance optimization notes', category: 'note' },
116
+ ];
117
+ const stmt = db.prepare(`
118
+ INSERT INTO context_items (id, session_id, key, value, category)
119
+ VALUES (?, ?, ?, ?, ?)
120
+ `);
121
+ items.forEach(item => {
122
+ stmt.run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category || null);
123
+ });
124
+ }
125
+ (0, globals_1.describe)('Create Relationships', () => {
126
+ (0, globals_1.beforeEach)(() => {
127
+ createTestItems();
128
+ });
129
+ (0, globals_1.it)('should create a simple relationship between two items', () => {
130
+ const fromKey = 'project.auth';
131
+ const toKey = 'component.login';
132
+ const relationshipType = 'contains';
133
+ // Verify both items exist
134
+ const fromItem = db
135
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
136
+ .get(testSessionId, fromKey);
137
+ const toItem = db
138
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
139
+ .get(testSessionId, toKey);
140
+ (0, globals_1.expect)(fromItem).toBeTruthy();
141
+ (0, globals_1.expect)(toItem).toBeTruthy();
142
+ // Create relationship
143
+ const relationshipId = (0, uuid_1.v4)();
144
+ const result = db
145
+ .prepare(`
146
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
147
+ VALUES (?, ?, ?, ?, ?)
148
+ `)
149
+ .run(relationshipId, testSessionId, fromKey, toKey, relationshipType);
150
+ (0, globals_1.expect)(result.changes).toBe(1);
151
+ // Handler response
152
+ const handlerResponse = {
153
+ content: [
154
+ {
155
+ type: 'text',
156
+ text: JSON.stringify({
157
+ operation: 'context_link',
158
+ relationshipId: relationshipId,
159
+ fromKey: fromKey,
160
+ toKey: toKey,
161
+ relationshipType: relationshipType,
162
+ created: true,
163
+ }, null, 2),
164
+ },
165
+ ],
166
+ };
167
+ const parsed = JSON.parse(handlerResponse.content[0].text);
168
+ (0, globals_1.expect)(parsed.created).toBe(true);
169
+ (0, globals_1.expect)(parsed.relationshipId).toBe(relationshipId);
170
+ });
171
+ (0, globals_1.it)('should create relationship with metadata', () => {
172
+ const fromKey = 'task.implement_auth';
173
+ const toKey = 'decision.use_oauth';
174
+ const relationshipType = 'depends_on';
175
+ const metadata = {
176
+ reason: 'OAuth decision affects authentication implementation',
177
+ priority: 'high',
178
+ createdBy: 'system',
179
+ };
180
+ const relationshipId = (0, uuid_1.v4)();
181
+ const result = db
182
+ .prepare(`
183
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type, metadata)
184
+ VALUES (?, ?, ?, ?, ?, ?)
185
+ `)
186
+ .run(relationshipId, testSessionId, fromKey, toKey, relationshipType, JSON.stringify(metadata));
187
+ (0, globals_1.expect)(result.changes).toBe(1);
188
+ // Verify metadata was stored
189
+ const relationship = db
190
+ .prepare('SELECT * FROM context_relationships WHERE id = ?')
191
+ .get(relationshipId);
192
+ (0, globals_1.expect)(relationship).toBeTruthy();
193
+ const storedMetadata = JSON.parse(relationship.metadata);
194
+ (0, globals_1.expect)(storedMetadata.reason).toBe(metadata.reason);
195
+ (0, globals_1.expect)(storedMetadata.priority).toBe(metadata.priority);
196
+ });
197
+ (0, globals_1.it)('should validate relationship types', () => {
198
+ const validTypes = [
199
+ 'contains',
200
+ 'depends_on',
201
+ 'references',
202
+ 'implements',
203
+ 'extends',
204
+ 'related_to',
205
+ 'blocks',
206
+ 'blocked_by',
207
+ 'parent_of',
208
+ 'child_of',
209
+ ];
210
+ validTypes.forEach(type => {
211
+ (0, globals_1.expect)(() => {
212
+ if (!validTypes.includes(type)) {
213
+ throw new validation_1.ValidationError(`Invalid relationship type: ${type}`);
214
+ }
215
+ }).not.toThrow();
216
+ });
217
+ // Test invalid type
218
+ const invalidType = 'invalid_type';
219
+ (0, globals_1.expect)(() => {
220
+ if (!validTypes.includes(invalidType)) {
221
+ throw new validation_1.ValidationError(`Invalid relationship type: ${invalidType}`);
222
+ }
223
+ }).toThrow(validation_1.ValidationError);
224
+ });
225
+ (0, globals_1.it)('should prevent duplicate relationships', () => {
226
+ const fromKey = 'project.api';
227
+ const toKey = 'component.dashboard';
228
+ const relationshipType = 'contains';
229
+ // Create first relationship
230
+ db.prepare(`
231
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
232
+ VALUES (?, ?, ?, ?, ?)
233
+ `).run((0, uuid_1.v4)(), testSessionId, fromKey, toKey, relationshipType);
234
+ // Try to create duplicate
235
+ try {
236
+ db.prepare(`
237
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
238
+ VALUES (?, ?, ?, ?, ?)
239
+ `).run((0, uuid_1.v4)(), testSessionId, fromKey, toKey, relationshipType);
240
+ }
241
+ catch (error) {
242
+ (0, globals_1.expect)(error).toBeTruthy();
243
+ (0, globals_1.expect)(error.message).toContain('UNIQUE constraint failed');
244
+ }
245
+ });
246
+ (0, globals_1.it)('should allow same relationship type for different pairs', () => {
247
+ const relationshipType = 'contains';
248
+ // Create multiple relationships with same type
249
+ const relationships = [
250
+ { from: 'project.auth', to: 'component.login' },
251
+ { from: 'project.frontend', to: 'component.dashboard' },
252
+ { from: 'project.frontend', to: 'component.user_profile' },
253
+ ];
254
+ relationships.forEach(rel => {
255
+ const result = db
256
+ .prepare(`
257
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
258
+ VALUES (?, ?, ?, ?, ?)
259
+ `)
260
+ .run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, relationshipType);
261
+ (0, globals_1.expect)(result.changes).toBe(1);
262
+ });
263
+ // Verify all were created
264
+ const count = db
265
+ .prepare('SELECT COUNT(*) as count FROM context_relationships WHERE session_id = ? AND relationship_type = ?')
266
+ .get(testSessionId, relationshipType).count;
267
+ (0, globals_1.expect)(count).toBe(3);
268
+ });
269
+ (0, globals_1.it)('should validate that both items exist before creating relationship', () => {
270
+ const fromKey = 'project.auth';
271
+ const toKey = 'non.existent.item';
272
+ const _relationshipType = 'contains';
273
+ // Check if items exist
274
+ const fromExists = db
275
+ .prepare('SELECT 1 FROM context_items WHERE session_id = ? AND key = ?')
276
+ .get(testSessionId, fromKey);
277
+ const toExists = db
278
+ .prepare('SELECT 1 FROM context_items WHERE session_id = ? AND key = ?')
279
+ .get(testSessionId, toKey);
280
+ if (!fromExists || !toExists) {
281
+ const missingKeys = [];
282
+ if (!fromExists)
283
+ missingKeys.push(fromKey);
284
+ if (!toExists)
285
+ missingKeys.push(toKey);
286
+ const handlerResponse = {
287
+ content: [
288
+ {
289
+ type: 'text',
290
+ text: `Error: The following items do not exist: ${missingKeys.join(', ')}`,
291
+ },
292
+ ],
293
+ };
294
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('do not exist');
295
+ }
296
+ });
297
+ (0, globals_1.it)('should handle self-referential relationships', () => {
298
+ const key = 'project.auth';
299
+ const relationshipType = 'related_to';
300
+ // Allow self-reference for certain relationship types
301
+ const result = db
302
+ .prepare(`
303
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
304
+ VALUES (?, ?, ?, ?, ?)
305
+ `)
306
+ .run((0, uuid_1.v4)(), testSessionId, key, key, relationshipType);
307
+ (0, globals_1.expect)(result.changes).toBe(1);
308
+ });
309
+ });
310
+ (0, globals_1.describe)('Retrieve Related Items', () => {
311
+ (0, globals_1.beforeEach)(() => {
312
+ createTestItems();
313
+ // Create test relationships
314
+ const relationships = [
315
+ { from: 'project.auth', to: 'component.login', type: 'contains' },
316
+ { from: 'project.auth', to: 'task.implement_auth', type: 'has_task' },
317
+ { from: 'project.auth', to: 'decision.use_oauth', type: 'implements' },
318
+ { from: 'project.auth', to: 'note.security', type: 'documented_in' },
319
+ { from: 'task.implement_auth', to: 'decision.use_oauth', type: 'depends_on' },
320
+ { from: 'component.login', to: 'note.security', type: 'references' },
321
+ { from: 'project.database', to: 'task.setup_db', type: 'has_task' },
322
+ { from: 'project.database', to: 'decision.postgres', type: 'implements' },
323
+ { from: 'task.setup_db', to: 'decision.postgres', type: 'depends_on' },
324
+ ];
325
+ const stmt = db.prepare(`
326
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
327
+ VALUES (?, ?, ?, ?, ?)
328
+ `);
329
+ relationships.forEach(rel => {
330
+ stmt.run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
331
+ });
332
+ });
333
+ (0, globals_1.it)('should retrieve all directly related items', () => {
334
+ const key = 'project.auth';
335
+ // Get outgoing relationships
336
+ const outgoing = db
337
+ .prepare(`
338
+ SELECT r.*, ci.value, ci.category, ci.priority
339
+ FROM context_relationships r
340
+ JOIN context_items ci ON ci.key = r.to_key AND ci.session_id = r.session_id
341
+ WHERE r.session_id = ? AND r.from_key = ?
342
+ `)
343
+ .all(testSessionId, key);
344
+ // Get incoming relationships
345
+ const incoming = db
346
+ .prepare(`
347
+ SELECT r.*, ci.value, ci.category, ci.priority
348
+ FROM context_relationships r
349
+ JOIN context_items ci ON ci.key = r.from_key AND ci.session_id = r.session_id
350
+ WHERE r.session_id = ? AND r.to_key = ?
351
+ `)
352
+ .all(testSessionId, key);
353
+ (0, globals_1.expect)(outgoing.length).toBe(4); // component.login, task.implement_auth, decision.use_oauth, note.security
354
+ (0, globals_1.expect)(incoming.length).toBe(0); // No items point to project.auth
355
+ // Handler response
356
+ const handlerResponse = {
357
+ content: [
358
+ {
359
+ type: 'text',
360
+ text: JSON.stringify({
361
+ operation: 'context_get_related',
362
+ key: key,
363
+ related: {
364
+ outgoing: outgoing.map(r => ({
365
+ key: r.to_key,
366
+ value: r.value,
367
+ relationshipType: r.relationship_type,
368
+ direction: 'outgoing',
369
+ })),
370
+ incoming: incoming.map(r => ({
371
+ key: r.from_key,
372
+ value: r.value,
373
+ relationshipType: r.relationship_type,
374
+ direction: 'incoming',
375
+ })),
376
+ },
377
+ totalRelated: outgoing.length + incoming.length,
378
+ }, null, 2),
379
+ },
380
+ ],
381
+ };
382
+ const parsed = JSON.parse(handlerResponse.content[0].text);
383
+ (0, globals_1.expect)(parsed.totalRelated).toBe(4);
384
+ (0, globals_1.expect)(parsed.related.outgoing).toHaveLength(4);
385
+ });
386
+ (0, globals_1.it)('should filter by relationship type', () => {
387
+ const key = 'project.auth';
388
+ const relationshipType = 'contains';
389
+ const related = db
390
+ .prepare(`
391
+ SELECT r.*, ci.value, ci.category
392
+ FROM context_relationships r
393
+ JOIN context_items ci ON ci.key = r.to_key AND ci.session_id = r.session_id
394
+ WHERE r.session_id = ? AND r.from_key = ? AND r.relationship_type = ?
395
+ `)
396
+ .all(testSessionId, key, relationshipType);
397
+ (0, globals_1.expect)(related.length).toBe(1);
398
+ (0, globals_1.expect)(related[0].to_key).toBe('component.login');
399
+ });
400
+ (0, globals_1.it)('should retrieve relationships with depth traversal', () => {
401
+ const key = 'project.auth';
402
+ const maxDepth = 2;
403
+ // Simulate depth traversal
404
+ const visited = new Set();
405
+ const relationships = [];
406
+ function traverse(currentKey, depth, path) {
407
+ if (depth > maxDepth || visited.has(currentKey))
408
+ return;
409
+ visited.add(currentKey);
410
+ // Get outgoing relationships
411
+ const outgoing = db
412
+ .prepare(`
413
+ SELECT r.*, ci.value, ci.category
414
+ FROM context_relationships r
415
+ JOIN context_items ci ON ci.key = r.to_key AND ci.session_id = r.session_id
416
+ WHERE r.session_id = ? AND r.from_key = ?
417
+ `)
418
+ .all(testSessionId, currentKey);
419
+ outgoing.forEach(rel => {
420
+ relationships.push({
421
+ path: [...path, currentKey],
422
+ from: currentKey,
423
+ to: rel.to_key,
424
+ type: rel.relationship_type,
425
+ value: rel.value,
426
+ depth: depth,
427
+ });
428
+ // Traverse deeper
429
+ traverse(rel.to_key, depth + 1, [...path, currentKey]);
430
+ });
431
+ }
432
+ traverse(key, 1, []);
433
+ // Should find direct and indirect relationships
434
+ (0, globals_1.expect)(relationships.length).toBeGreaterThan(4); // More than just direct relationships
435
+ // Check we found the indirect relationship: project.auth -> task.implement_auth -> decision.use_oauth
436
+ const indirectRelation = relationships.find(r => r.from === 'task.implement_auth' && r.to === 'decision.use_oauth');
437
+ (0, globals_1.expect)(indirectRelation).toBeTruthy();
438
+ (0, globals_1.expect)(indirectRelation.depth).toBe(2);
439
+ });
440
+ (0, globals_1.it)('should handle bidirectional relationships', () => {
441
+ const key = 'decision.use_oauth';
442
+ // Get all relationships (both directions)
443
+ const allRelationships = db
444
+ .prepare(`
445
+ SELECT
446
+ CASE
447
+ WHEN r.from_key = ? THEN r.to_key
448
+ ELSE r.from_key
449
+ END as related_key,
450
+ CASE
451
+ WHEN r.from_key = ? THEN 'outgoing'
452
+ ELSE 'incoming'
453
+ END as direction,
454
+ r.relationship_type,
455
+ ci.value,
456
+ ci.category
457
+ FROM context_relationships r
458
+ JOIN context_items ci ON ci.key = CASE
459
+ WHEN r.from_key = ? THEN r.to_key
460
+ ELSE r.from_key
461
+ END AND ci.session_id = r.session_id
462
+ WHERE r.session_id = ? AND (r.from_key = ? OR r.to_key = ?)
463
+ `)
464
+ .all(key, key, key, testSessionId, key, key);
465
+ (0, globals_1.expect)(allRelationships.length).toBe(2);
466
+ const incoming = allRelationships.filter(r => r.direction === 'incoming');
467
+ const outgoing = allRelationships.filter(r => r.direction === 'outgoing');
468
+ (0, globals_1.expect)(incoming.length).toBe(2); // project.auth and task.implement_auth
469
+ (0, globals_1.expect)(outgoing.length).toBe(0);
470
+ });
471
+ (0, globals_1.it)('should include relationship metadata in results', () => {
472
+ // Add a relationship with metadata
473
+ const fromKey = 'project.api';
474
+ const toKey = 'task.create_api';
475
+ const metadata = {
476
+ priority: 'high',
477
+ estimatedHours: 40,
478
+ assignee: 'dev-team',
479
+ };
480
+ db.prepare(`
481
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type, metadata)
482
+ VALUES (?, ?, ?, ?, ?, ?)
483
+ `).run((0, uuid_1.v4)(), testSessionId, fromKey, toKey, 'has_task', JSON.stringify(metadata));
484
+ // Retrieve with metadata
485
+ const related = db
486
+ .prepare(`
487
+ SELECT r.*, ci.value
488
+ FROM context_relationships r
489
+ JOIN context_items ci ON ci.key = r.to_key AND ci.session_id = r.session_id
490
+ WHERE r.session_id = ? AND r.from_key = ?
491
+ `)
492
+ .all(testSessionId, fromKey);
493
+ (0, globals_1.expect)(related.length).toBe(1);
494
+ const parsedMetadata = JSON.parse(related[0].metadata);
495
+ (0, globals_1.expect)(parsedMetadata.priority).toBe('high');
496
+ (0, globals_1.expect)(parsedMetadata.estimatedHours).toBe(40);
497
+ // Handler response with metadata
498
+ const handlerResponse = {
499
+ content: [
500
+ {
501
+ type: 'text',
502
+ text: JSON.stringify({
503
+ operation: 'context_get_related',
504
+ key: fromKey,
505
+ related: {
506
+ outgoing: related.map(r => ({
507
+ key: r.to_key,
508
+ value: r.value,
509
+ relationshipType: r.relationship_type,
510
+ metadata: JSON.parse(r.metadata),
511
+ })),
512
+ incoming: [],
513
+ },
514
+ }, null, 2),
515
+ },
516
+ ],
517
+ };
518
+ const parsed = JSON.parse(handlerResponse.content[0].text);
519
+ (0, globals_1.expect)(parsed.related.outgoing[0].metadata.estimatedHours).toBe(40);
520
+ });
521
+ });
522
+ (0, globals_1.describe)('Relationship Queries and Analysis', () => {
523
+ (0, globals_1.beforeEach)(() => {
524
+ createTestItems();
525
+ // Create a complex relationship graph
526
+ const relationships = [
527
+ // Project hierarchy
528
+ { from: 'project.auth', to: 'component.login', type: 'contains' },
529
+ { from: 'project.frontend', to: 'component.dashboard', type: 'contains' },
530
+ { from: 'project.frontend', to: 'component.user_profile', type: 'contains' },
531
+ // Dependencies
532
+ { from: 'component.dashboard', to: 'project.api', type: 'depends_on' },
533
+ { from: 'component.user_profile', to: 'project.auth', type: 'depends_on' },
534
+ { from: 'project.api', to: 'project.database', type: 'depends_on' },
535
+ // Task relationships
536
+ { from: 'task.implement_auth', to: 'task.setup_db', type: 'blocked_by' },
537
+ { from: 'task.create_api', to: 'task.setup_db', type: 'blocked_by' },
538
+ ];
539
+ const stmt = db.prepare(`
540
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
541
+ VALUES (?, ?, ?, ?, ?)
542
+ `);
543
+ relationships.forEach(rel => {
544
+ stmt.run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
545
+ });
546
+ });
547
+ (0, globals_1.it)('should find all paths between two items', () => {
548
+ const startKey = 'component.user_profile';
549
+ const endKey = 'project.database';
550
+ // Simple path finding (BFS)
551
+ const paths = [];
552
+ const queue = [{ key: startKey, path: [startKey] }];
553
+ const visited = new Set();
554
+ while (queue.length > 0) {
555
+ const current = queue.shift();
556
+ if (current.key === endKey) {
557
+ paths.push(current.path);
558
+ continue;
559
+ }
560
+ if (visited.has(current.key))
561
+ continue;
562
+ visited.add(current.key);
563
+ // Get all connections
564
+ const connections = db
565
+ .prepare(`
566
+ SELECT to_key as next_key FROM context_relationships
567
+ WHERE session_id = ? AND from_key = ?
568
+ UNION
569
+ SELECT from_key as next_key FROM context_relationships
570
+ WHERE session_id = ? AND to_key = ?
571
+ `)
572
+ .all(testSessionId, current.key, testSessionId, current.key);
573
+ connections.forEach(conn => {
574
+ if (!current.path.includes(conn.next_key)) {
575
+ queue.push({
576
+ key: conn.next_key,
577
+ path: [...current.path, conn.next_key],
578
+ });
579
+ }
580
+ });
581
+ }
582
+ (0, globals_1.expect)(paths.length).toBeGreaterThan(0);
583
+ // Should find path: component.user_profile -> project.auth -> (other connections) -> project.database
584
+ });
585
+ (0, globals_1.it)('should identify relationship cycles', () => {
586
+ // Add relationships to create a cycle: project.api -> project.database -> project.api
587
+ db.prepare(`
588
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
589
+ VALUES (?, ?, ?, ?, ?)
590
+ `).run((0, uuid_1.v4)(), testSessionId, 'project.database', 'project.api', 'depends_on');
591
+ // Detect cycles using DFS
592
+ const visited = new Set();
593
+ const recursionStack = new Set();
594
+ const cycles = [];
595
+ function detectCycle(key, path) {
596
+ visited.add(key);
597
+ recursionStack.add(key);
598
+ const neighbors = db
599
+ .prepare('SELECT to_key FROM context_relationships WHERE session_id = ? AND from_key = ?')
600
+ .all(testSessionId, key);
601
+ for (const neighbor of neighbors) {
602
+ if (recursionStack.has(neighbor.to_key)) {
603
+ // Found cycle
604
+ const cycleStart = path.indexOf(neighbor.to_key);
605
+ cycles.push([...path.slice(cycleStart), neighbor.to_key]);
606
+ }
607
+ else if (!visited.has(neighbor.to_key)) {
608
+ detectCycle(neighbor.to_key, [...path, neighbor.to_key]);
609
+ }
610
+ }
611
+ recursionStack.delete(key);
612
+ }
613
+ // Check all nodes
614
+ const allKeys = db
615
+ .prepare('SELECT DISTINCT key FROM context_items WHERE session_id = ?')
616
+ .all(testSessionId);
617
+ allKeys.forEach(item => {
618
+ if (!visited.has(item.key)) {
619
+ detectCycle(item.key, [item.key]);
620
+ }
621
+ });
622
+ (0, globals_1.expect)(cycles.length).toBeGreaterThan(0);
623
+ });
624
+ (0, globals_1.it)('should calculate relationship statistics', () => {
625
+ // Get statistics
626
+ const stats = {
627
+ totalRelationships: db
628
+ .prepare('SELECT COUNT(*) as count FROM context_relationships WHERE session_id = ?')
629
+ .get(testSessionId).count,
630
+ byType: db
631
+ .prepare(`
632
+ SELECT relationship_type, COUNT(*) as count
633
+ FROM context_relationships
634
+ WHERE session_id = ?
635
+ GROUP BY relationship_type
636
+ `)
637
+ .all(testSessionId),
638
+ mostConnected: db
639
+ .prepare(`
640
+ SELECT key, COUNT(*) as connection_count
641
+ FROM (
642
+ SELECT from_key as key FROM context_relationships WHERE session_id = ?
643
+ UNION ALL
644
+ SELECT to_key as key FROM context_relationships WHERE session_id = ?
645
+ )
646
+ GROUP BY key
647
+ ORDER BY connection_count DESC
648
+ LIMIT 5
649
+ `)
650
+ .all(testSessionId, testSessionId),
651
+ orphanedItems: db
652
+ .prepare(`
653
+ SELECT key FROM context_items
654
+ WHERE session_id = ?
655
+ AND key NOT IN (
656
+ SELECT from_key FROM context_relationships WHERE session_id = ?
657
+ UNION
658
+ SELECT to_key FROM context_relationships WHERE session_id = ?
659
+ )
660
+ `)
661
+ .all(testSessionId, testSessionId, testSessionId),
662
+ };
663
+ (0, globals_1.expect)(stats.totalRelationships).toBeGreaterThan(0);
664
+ (0, globals_1.expect)(stats.byType.length).toBeGreaterThan(0);
665
+ (0, globals_1.expect)(stats.mostConnected.length).toBeGreaterThan(0);
666
+ // Handler response
667
+ const handlerResponse = {
668
+ content: [
669
+ {
670
+ type: 'text',
671
+ text: JSON.stringify({
672
+ operation: 'context_relationship_stats',
673
+ statistics: stats,
674
+ }, null, 2),
675
+ },
676
+ ],
677
+ };
678
+ const parsed = JSON.parse(handlerResponse.content[0].text);
679
+ (0, globals_1.expect)(parsed.statistics.totalRelationships).toBeGreaterThan(0);
680
+ });
681
+ (0, globals_1.it)('should find items by relationship pattern', () => {
682
+ // Find all items that are blocked by something
683
+ const blockedItems = db
684
+ .prepare(`
685
+ SELECT DISTINCT ci.key, ci.value, r.to_key as blocked_by
686
+ FROM context_relationships r
687
+ JOIN context_items ci ON ci.key = r.from_key AND ci.session_id = r.session_id
688
+ WHERE r.session_id = ? AND r.relationship_type = 'blocked_by'
689
+ `)
690
+ .all(testSessionId);
691
+ (0, globals_1.expect)(blockedItems.length).toBe(2); // task.implement_auth and task.create_api
692
+ // Find all container items (items that contain other items)
693
+ const containers = db
694
+ .prepare(`
695
+ SELECT DISTINCT ci.key, ci.value, COUNT(r.to_key) as contained_items
696
+ FROM context_relationships r
697
+ JOIN context_items ci ON ci.key = r.from_key AND ci.session_id = r.session_id
698
+ WHERE r.session_id = ? AND r.relationship_type = 'contains'
699
+ GROUP BY ci.key, ci.value
700
+ `)
701
+ .all(testSessionId);
702
+ (0, globals_1.expect)(containers.length).toBeGreaterThan(0);
703
+ (0, globals_1.expect)(containers.every((c) => c.contained_items > 0)).toBe(true);
704
+ });
705
+ });
706
+ (0, globals_1.describe)('Relationship Management', () => {
707
+ (0, globals_1.beforeEach)(() => {
708
+ createTestItems();
709
+ });
710
+ (0, globals_1.it)('should update relationship type', () => {
711
+ const fromKey = 'project.auth';
712
+ const toKey = 'component.login';
713
+ const oldType = 'contains';
714
+ const newType = 'parent_of';
715
+ // Create initial relationship
716
+ db.prepare(`
717
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
718
+ VALUES (?, ?, ?, ?, ?)
719
+ `).run((0, uuid_1.v4)(), testSessionId, fromKey, toKey, oldType);
720
+ // Update relationship type
721
+ const result = db
722
+ .prepare(`
723
+ UPDATE context_relationships
724
+ SET relationship_type = ?
725
+ WHERE session_id = ? AND from_key = ? AND to_key = ? AND relationship_type = ?
726
+ `)
727
+ .run(newType, testSessionId, fromKey, toKey, oldType);
728
+ (0, globals_1.expect)(result.changes).toBe(1);
729
+ // Verify update
730
+ const updated = db
731
+ .prepare('SELECT * FROM context_relationships WHERE session_id = ? AND from_key = ? AND to_key = ?')
732
+ .get(testSessionId, fromKey, toKey);
733
+ (0, globals_1.expect)(updated.relationship_type).toBe(newType);
734
+ });
735
+ (0, globals_1.it)('should delete specific relationships', () => {
736
+ // Create relationships
737
+ const relationships = [
738
+ { from: 'project.auth', to: 'component.login', type: 'contains' },
739
+ { from: 'project.auth', to: 'task.implement_auth', type: 'has_task' },
740
+ ];
741
+ relationships.forEach(rel => {
742
+ db.prepare(`
743
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
744
+ VALUES (?, ?, ?, ?, ?)
745
+ `).run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
746
+ });
747
+ // Delete one relationship
748
+ const result = db
749
+ .prepare(`
750
+ DELETE FROM context_relationships
751
+ WHERE session_id = ? AND from_key = ? AND to_key = ? AND relationship_type = ?
752
+ `)
753
+ .run(testSessionId, relationships[0].from, relationships[0].to, relationships[0].type);
754
+ (0, globals_1.expect)(result.changes).toBe(1);
755
+ // Verify only one remains
756
+ const remaining = db
757
+ .prepare('SELECT COUNT(*) as count FROM context_relationships WHERE session_id = ?')
758
+ .get(testSessionId);
759
+ (0, globals_1.expect)(remaining.count).toBe(1);
760
+ });
761
+ (0, globals_1.it)('should delete all relationships for an item when item is deleted', () => {
762
+ // Create relationships
763
+ const itemKey = 'project.auth';
764
+ const relationships = [
765
+ { from: itemKey, to: 'component.login', type: 'contains' },
766
+ { from: itemKey, to: 'task.implement_auth', type: 'has_task' },
767
+ { from: 'component.user_profile', to: itemKey, type: 'depends_on' },
768
+ ];
769
+ relationships.forEach(rel => {
770
+ db.prepare(`
771
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
772
+ VALUES (?, ?, ?, ?, ?)
773
+ `).run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
774
+ });
775
+ // Delete all relationships involving the item
776
+ const result = db
777
+ .prepare(`
778
+ DELETE FROM context_relationships
779
+ WHERE session_id = ? AND (from_key = ? OR to_key = ?)
780
+ `)
781
+ .run(testSessionId, itemKey, itemKey);
782
+ (0, globals_1.expect)(result.changes).toBe(3);
783
+ // Verify all relationships are gone
784
+ const remaining = db
785
+ .prepare('SELECT COUNT(*) as count FROM context_relationships WHERE session_id = ? AND (from_key = ? OR to_key = ?)')
786
+ .get(testSessionId, itemKey, itemKey);
787
+ (0, globals_1.expect)(remaining.count).toBe(0);
788
+ });
789
+ (0, globals_1.it)('should bulk create relationships', () => {
790
+ const relationships = [
791
+ { from: 'project.api', to: 'component.dashboard', type: 'serves' },
792
+ { from: 'project.api', to: 'project.database', type: 'depends_on' },
793
+ { from: 'project.api', to: 'note.performance', type: 'documented_in' },
794
+ ];
795
+ db.prepare('BEGIN TRANSACTION').run();
796
+ try {
797
+ const stmt = db.prepare(`
798
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
799
+ VALUES (?, ?, ?, ?, ?)
800
+ `);
801
+ relationships.forEach(rel => {
802
+ stmt.run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
803
+ });
804
+ db.prepare('COMMIT').run();
805
+ }
806
+ catch (error) {
807
+ db.prepare('ROLLBACK').run();
808
+ throw error;
809
+ }
810
+ // Verify all were created
811
+ const count = db
812
+ .prepare('SELECT COUNT(*) as count FROM context_relationships WHERE session_id = ? AND from_key = ?')
813
+ .get(testSessionId, 'project.api').count;
814
+ (0, globals_1.expect)(count).toBe(3);
815
+ });
816
+ });
817
+ (0, globals_1.describe)('Error Handling', () => {
818
+ (0, globals_1.beforeEach)(() => {
819
+ createTestItems();
820
+ });
821
+ (0, globals_1.it)('should handle invalid relationship data', () => {
822
+ const invalidCases = [
823
+ { from: '', to: 'component.login', type: 'contains', error: 'From key cannot be empty' },
824
+ { from: 'project.auth', to: '', type: 'contains', error: 'To key cannot be empty' },
825
+ {
826
+ from: 'project.auth',
827
+ to: 'component.login',
828
+ type: '',
829
+ error: 'Relationship type cannot be empty',
830
+ },
831
+ {
832
+ from: 'project.auth',
833
+ to: 'component.login',
834
+ type: 'invalid_type',
835
+ error: 'Invalid relationship type',
836
+ },
837
+ ];
838
+ invalidCases.forEach(testCase => {
839
+ try {
840
+ if (!testCase.from || !testCase.from.trim()) {
841
+ throw new validation_1.ValidationError('From key cannot be empty');
842
+ }
843
+ if (!testCase.to || !testCase.to.trim()) {
844
+ throw new validation_1.ValidationError('To key cannot be empty');
845
+ }
846
+ if (!testCase.type || !testCase.type.trim()) {
847
+ throw new validation_1.ValidationError('Relationship type cannot be empty');
848
+ }
849
+ const validTypes = [
850
+ 'contains',
851
+ 'depends_on',
852
+ 'references',
853
+ 'implements',
854
+ 'extends',
855
+ 'related_to',
856
+ 'blocks',
857
+ 'blocked_by',
858
+ 'parent_of',
859
+ 'child_of',
860
+ ];
861
+ if (!validTypes.includes(testCase.type)) {
862
+ throw new validation_1.ValidationError('Invalid relationship type');
863
+ }
864
+ }
865
+ catch (error) {
866
+ (0, globals_1.expect)(error).toBeInstanceOf(validation_1.ValidationError);
867
+ (0, globals_1.expect)(error.message).toBe(testCase.error);
868
+ }
869
+ });
870
+ });
871
+ (0, globals_1.it)('should handle non-existent items gracefully', () => {
872
+ const key = 'non.existent.item';
873
+ // Try to get related items
874
+ const related = db
875
+ .prepare(`
876
+ SELECT r.*, ci.value
877
+ FROM context_relationships r
878
+ JOIN context_items ci ON ci.key = r.to_key AND ci.session_id = r.session_id
879
+ WHERE r.session_id = ? AND r.from_key = ?
880
+ `)
881
+ .all(testSessionId, key);
882
+ (0, globals_1.expect)(related.length).toBe(0);
883
+ // Handler response
884
+ const handlerResponse = {
885
+ content: [
886
+ {
887
+ type: 'text',
888
+ text: JSON.stringify({
889
+ operation: 'context_get_related',
890
+ key: key,
891
+ related: {
892
+ outgoing: [],
893
+ incoming: [],
894
+ },
895
+ totalRelated: 0,
896
+ message: 'No relationships found for this item',
897
+ }, null, 2),
898
+ },
899
+ ],
900
+ };
901
+ const parsed = JSON.parse(handlerResponse.content[0].text);
902
+ (0, globals_1.expect)(parsed.totalRelated).toBe(0);
903
+ (0, globals_1.expect)(parsed.message).toBeTruthy();
904
+ });
905
+ (0, globals_1.it)('should handle circular dependency detection', () => {
906
+ // Create a circular dependency
907
+ const circularRels = [
908
+ { from: 'project.auth', to: 'project.api', type: 'depends_on' },
909
+ { from: 'project.api', to: 'project.database', type: 'depends_on' },
910
+ { from: 'project.database', to: 'project.auth', type: 'depends_on' }, // Creates circle
911
+ ];
912
+ circularRels.forEach(rel => {
913
+ db.prepare(`
914
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
915
+ VALUES (?, ?, ?, ?, ?)
916
+ `).run((0, uuid_1.v4)(), testSessionId, rel.from, rel.to, rel.type);
917
+ });
918
+ // Function to detect circular dependencies
919
+ function hasCircularDependency(startKey) {
920
+ const visited = new Set();
921
+ const stack = new Set();
922
+ function dfs(key) {
923
+ visited.add(key);
924
+ stack.add(key);
925
+ const dependencies = db
926
+ .prepare(`SELECT to_key FROM context_relationships
927
+ WHERE session_id = ? AND from_key = ? AND relationship_type = 'depends_on'`)
928
+ .all(testSessionId, key);
929
+ for (const dep of dependencies) {
930
+ if (stack.has(dep.to_key)) {
931
+ return true; // Circular dependency found
932
+ }
933
+ if (!visited.has(dep.to_key) && dfs(dep.to_key)) {
934
+ return true;
935
+ }
936
+ }
937
+ stack.delete(key);
938
+ return false;
939
+ }
940
+ return dfs(startKey);
941
+ }
942
+ (0, globals_1.expect)(hasCircularDependency('project.auth')).toBe(true);
943
+ });
944
+ });
945
+ (0, globals_1.describe)('Performance and Scalability', () => {
946
+ (0, globals_1.it)('should handle large relationship graphs efficiently', () => {
947
+ // Create a large number of items and relationships
948
+ const itemCount = 100;
949
+ const items = [];
950
+ // Create items
951
+ for (let i = 0; i < itemCount; i++) {
952
+ const key = `item.${i}`;
953
+ items.push(key);
954
+ db.prepare(`
955
+ INSERT INTO context_items (id, session_id, key, value)
956
+ VALUES (?, ?, ?, ?)
957
+ `).run((0, uuid_1.v4)(), testSessionId, key, `Value for ${key}`);
958
+ }
959
+ // Create relationships (each item connected to 2-5 others)
960
+ const startTime = Date.now();
961
+ db.prepare('BEGIN TRANSACTION').run();
962
+ const stmt = db.prepare(`
963
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
964
+ VALUES (?, ?, ?, ?, ?)
965
+ `);
966
+ for (let i = 0; i < itemCount; i++) {
967
+ const connectionCount = 2 + Math.floor(Math.random() * 4);
968
+ for (let j = 0; j < connectionCount; j++) {
969
+ const targetIndex = Math.floor(Math.random() * itemCount);
970
+ if (targetIndex !== i) {
971
+ try {
972
+ stmt.run((0, uuid_1.v4)(), testSessionId, items[i], items[targetIndex], 'related_to');
973
+ }
974
+ catch (_e) {
975
+ // Ignore duplicate relationships
976
+ }
977
+ }
978
+ }
979
+ }
980
+ db.prepare('COMMIT').run();
981
+ const endTime = Date.now();
982
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(5000); // Should complete within 5 seconds
983
+ // Test query performance
984
+ const queryStart = Date.now();
985
+ const mostConnected = db
986
+ .prepare(`
987
+ SELECT key, COUNT(*) as connections
988
+ FROM (
989
+ SELECT from_key as key FROM context_relationships WHERE session_id = ?
990
+ UNION ALL
991
+ SELECT to_key as key FROM context_relationships WHERE session_id = ?
992
+ )
993
+ GROUP BY key
994
+ ORDER BY connections DESC
995
+ LIMIT 10
996
+ `)
997
+ .all(testSessionId, testSessionId);
998
+ const queryEnd = Date.now();
999
+ (0, globals_1.expect)(queryEnd - queryStart).toBeLessThan(100); // Query should be fast
1000
+ (0, globals_1.expect)(mostConnected.length).toBeGreaterThan(0);
1001
+ });
1002
+ (0, globals_1.it)('should efficiently traverse deep relationship chains', () => {
1003
+ // Create a chain of relationships
1004
+ const chainLength = 20;
1005
+ for (let i = 0; i < chainLength; i++) {
1006
+ db.prepare(`
1007
+ INSERT INTO context_items (id, session_id, key, value)
1008
+ VALUES (?, ?, ?, ?)
1009
+ `).run((0, uuid_1.v4)(), testSessionId, `chain.${i}`, `Chain item ${i}`);
1010
+ if (i > 0) {
1011
+ db.prepare(`
1012
+ INSERT INTO context_relationships (id, session_id, from_key, to_key, relationship_type)
1013
+ VALUES (?, ?, ?, ?, ?)
1014
+ `).run((0, uuid_1.v4)(), testSessionId, `chain.${i - 1}`, `chain.${i}`, 'leads_to');
1015
+ }
1016
+ }
1017
+ // Traverse the entire chain
1018
+ const startTime = Date.now();
1019
+ let currentKey = 'chain.0';
1020
+ const path = [currentKey];
1021
+ while (true) {
1022
+ const next = db
1023
+ .prepare(`SELECT to_key FROM context_relationships
1024
+ WHERE session_id = ? AND from_key = ? AND relationship_type = 'leads_to'`)
1025
+ .get(testSessionId, currentKey);
1026
+ if (!next)
1027
+ break;
1028
+ currentKey = next.to_key;
1029
+ path.push(currentKey);
1030
+ }
1031
+ const endTime = Date.now();
1032
+ (0, globals_1.expect)(path.length).toBe(chainLength);
1033
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(100); // Should be fast even for long chains
1034
+ });
1035
+ });
1036
+ (0, globals_1.describe)('Handler Response Formats', () => {
1037
+ (0, globals_1.beforeEach)(() => {
1038
+ createTestItems();
1039
+ });
1040
+ (0, globals_1.it)('should format relationship creation response', () => {
1041
+ const relationshipData = {
1042
+ fromKey: 'project.auth',
1043
+ toKey: 'component.login',
1044
+ relationshipType: 'contains',
1045
+ metadata: { created: new Date().toISOString() },
1046
+ };
1047
+ const relationshipId = (0, uuid_1.v4)();
1048
+ const handlerResponse = {
1049
+ content: [
1050
+ {
1051
+ type: 'text',
1052
+ text: JSON.stringify({
1053
+ operation: 'context_link',
1054
+ relationshipId: relationshipId,
1055
+ fromKey: relationshipData.fromKey,
1056
+ toKey: relationshipData.toKey,
1057
+ relationshipType: relationshipData.relationshipType,
1058
+ metadata: relationshipData.metadata,
1059
+ created: true,
1060
+ timestamp: new Date().toISOString(),
1061
+ }, null, 2),
1062
+ },
1063
+ ],
1064
+ };
1065
+ const parsed = JSON.parse(handlerResponse.content[0].text);
1066
+ (0, globals_1.expect)(parsed.operation).toBe('context_link');
1067
+ (0, globals_1.expect)(parsed.created).toBe(true);
1068
+ (0, globals_1.expect)(parsed.relationshipId).toBe(relationshipId);
1069
+ });
1070
+ (0, globals_1.it)('should format related items response with graph visualization hints', () => {
1071
+ const key = 'project.auth';
1072
+ // Mock data structure for visualization
1073
+ const graphData = {
1074
+ nodes: [
1075
+ { id: 'project.auth', label: 'Authentication module', type: 'project' },
1076
+ { id: 'component.login', label: 'Login component', type: 'component' },
1077
+ { id: 'task.implement_auth', label: 'Implement authentication', type: 'task' },
1078
+ ],
1079
+ edges: [
1080
+ { from: 'project.auth', to: 'component.login', type: 'contains', label: 'contains' },
1081
+ { from: 'project.auth', to: 'task.implement_auth', type: 'has_task', label: 'has task' },
1082
+ ],
1083
+ };
1084
+ const handlerResponse = {
1085
+ content: [
1086
+ {
1087
+ type: 'text',
1088
+ text: JSON.stringify({
1089
+ operation: 'context_get_related',
1090
+ key: key,
1091
+ visualization: {
1092
+ format: 'graph',
1093
+ nodes: graphData.nodes,
1094
+ edges: graphData.edges,
1095
+ },
1096
+ summary: {
1097
+ totalNodes: graphData.nodes.length,
1098
+ totalEdges: graphData.edges.length,
1099
+ relationshipTypes: ['contains', 'has_task'],
1100
+ },
1101
+ }, null, 2),
1102
+ },
1103
+ ],
1104
+ };
1105
+ const parsed = JSON.parse(handlerResponse.content[0].text);
1106
+ (0, globals_1.expect)(parsed.visualization.format).toBe('graph');
1107
+ (0, globals_1.expect)(parsed.visualization.nodes).toHaveLength(3);
1108
+ (0, globals_1.expect)(parsed.visualization.edges).toHaveLength(2);
1109
+ });
1110
+ (0, globals_1.it)('should provide text summary for complex relationship queries', () => {
1111
+ const analysisResult = {
1112
+ mostConnectedItems: [
1113
+ { key: 'project.auth', connections: 4 },
1114
+ { key: 'project.database', connections: 3 },
1115
+ ],
1116
+ relationshipTypeCounts: [
1117
+ { type: 'contains', count: 3 },
1118
+ { type: 'depends_on', count: 4 },
1119
+ { type: 'has_task', count: 2 },
1120
+ ],
1121
+ orphanedItems: ['note.performance'],
1122
+ circularDependencies: [['project.auth', 'project.api', 'project.database', 'project.auth']],
1123
+ };
1124
+ const handlerResponse = {
1125
+ content: [
1126
+ {
1127
+ type: 'text',
1128
+ text: `Relationship Analysis Summary:
1129
+
1130
+ Most Connected Items:
1131
+ ${analysisResult.mostConnectedItems
1132
+ .map(item => `• ${item.key}: ${item.connections} connections`)
1133
+ .join('\n')}
1134
+
1135
+ Relationship Types:
1136
+ ${analysisResult.relationshipTypeCounts
1137
+ .map(type => `• ${type.type}: ${type.count} relationships`)
1138
+ .join('\n')}
1139
+
1140
+ Orphaned Items: ${analysisResult.orphanedItems.join(', ')}
1141
+
1142
+ Circular Dependencies Detected:
1143
+ ${analysisResult.circularDependencies.map(cycle => `• ${cycle.join(' → ')}`).join('\n')}`,
1144
+ },
1145
+ ],
1146
+ };
1147
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('Most Connected Items');
1148
+ (0, globals_1.expect)(handlerResponse.content[0].text).toContain('Circular Dependencies');
1149
+ });
1150
+ });
1151
+ });