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,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
|
+
});
|