@danielsimonjr/memory-mcp 0.7.2 → 0.41.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/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
- package/dist/__tests__/file-path.test.js +5 -5
- package/dist/__tests__/integration/workflows.test.js +449 -0
- package/dist/__tests__/knowledge-graph.test.js +8 -3
- package/dist/__tests__/performance/benchmarks.test.js +411 -0
- package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
- package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
- package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
- package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
- package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
- package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
- package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
- package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
- package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
- package/dist/core/EntityManager.js +554 -0
- package/dist/core/GraphStorage.js +172 -0
- package/dist/core/KnowledgeGraphManager.js +400 -0
- package/dist/core/ObservationManager.js +129 -0
- package/dist/core/RelationManager.js +186 -0
- package/dist/core/TransactionManager.js +389 -0
- package/dist/core/index.js +9 -0
- package/dist/features/AnalyticsManager.js +222 -0
- package/dist/features/ArchiveManager.js +74 -0
- package/dist/features/BackupManager.js +311 -0
- package/dist/features/CompressionManager.js +310 -0
- package/dist/features/ExportManager.js +305 -0
- package/dist/features/HierarchyManager.js +219 -0
- package/dist/features/ImportExportManager.js +50 -0
- package/dist/features/ImportManager.js +328 -0
- package/dist/features/TagManager.js +210 -0
- package/dist/features/index.js +12 -0
- package/dist/index.js +13 -996
- package/dist/memory.jsonl +225 -0
- package/dist/search/BasicSearch.js +161 -0
- package/dist/search/BooleanSearch.js +304 -0
- package/dist/search/FuzzySearch.js +115 -0
- package/dist/search/RankedSearch.js +206 -0
- package/dist/search/SavedSearchManager.js +145 -0
- package/dist/search/SearchManager.js +305 -0
- package/dist/search/SearchSuggestions.js +57 -0
- package/dist/search/TFIDFIndexManager.js +217 -0
- package/dist/search/index.js +10 -0
- package/dist/server/MCPServer.js +889 -0
- package/dist/types/analytics.types.js +6 -0
- package/dist/types/entity.types.js +7 -0
- package/dist/types/import-export.types.js +7 -0
- package/dist/types/index.js +12 -0
- package/dist/types/search.types.js +7 -0
- package/dist/types/tag.types.js +6 -0
- package/dist/utils/constants.js +127 -0
- package/dist/utils/dateUtils.js +89 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/levenshtein.js +62 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/pathUtils.js +115 -0
- package/dist/utils/schemas.js +184 -0
- package/dist/utils/searchCache.js +209 -0
- package/dist/utils/tfidf.js +90 -0
- package/dist/utils/validationUtils.js +109 -0
- package/package.json +50 -48
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for Complete Workflows
|
|
3
|
+
*
|
|
4
|
+
* Tests that verify multiple components work together correctly
|
|
5
|
+
* in realistic end-to-end scenarios.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { GraphStorage } from '../../core/GraphStorage.js';
|
|
9
|
+
import { EntityManager } from '../../core/EntityManager.js';
|
|
10
|
+
import { RelationManager } from '../../core/RelationManager.js';
|
|
11
|
+
import { CompressionManager } from '../../features/CompressionManager.js';
|
|
12
|
+
import { BasicSearch } from '../../search/BasicSearch.js';
|
|
13
|
+
import { RankedSearch } from '../../search/RankedSearch.js';
|
|
14
|
+
import { BooleanSearch } from '../../search/BooleanSearch.js';
|
|
15
|
+
import { FuzzySearch } from '../../search/FuzzySearch.js';
|
|
16
|
+
import { promises as fs } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { tmpdir } from 'os';
|
|
19
|
+
describe('Integration: Complete Workflows', () => {
|
|
20
|
+
let storage;
|
|
21
|
+
let entityManager;
|
|
22
|
+
let relationManager;
|
|
23
|
+
let compressionManager;
|
|
24
|
+
let basicSearch;
|
|
25
|
+
let rankedSearch;
|
|
26
|
+
let booleanSearch;
|
|
27
|
+
let fuzzySearch;
|
|
28
|
+
let testDir;
|
|
29
|
+
let testFilePath;
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
// Create unique temp directory for each test
|
|
32
|
+
testDir = join(tmpdir(), `integration-test-${Date.now()}-${Math.random()}`);
|
|
33
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
34
|
+
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
35
|
+
storage = new GraphStorage(testFilePath);
|
|
36
|
+
entityManager = new EntityManager(storage);
|
|
37
|
+
relationManager = new RelationManager(storage);
|
|
38
|
+
compressionManager = new CompressionManager(storage);
|
|
39
|
+
basicSearch = new BasicSearch(storage);
|
|
40
|
+
rankedSearch = new RankedSearch(storage);
|
|
41
|
+
booleanSearch = new BooleanSearch(storage);
|
|
42
|
+
fuzzySearch = new FuzzySearch(storage);
|
|
43
|
+
});
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
// Clean up test files
|
|
46
|
+
try {
|
|
47
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ignore cleanup errors
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
describe('Entity Creation and Search Workflow', () => {
|
|
54
|
+
it('should create entities, establish relations, and find them via search', async () => {
|
|
55
|
+
// Step 1: Create team entities
|
|
56
|
+
const team = await entityManager.createEntities([
|
|
57
|
+
{
|
|
58
|
+
name: 'Alice',
|
|
59
|
+
entityType: 'person',
|
|
60
|
+
observations: ['Senior software engineer', 'Team lead', 'Expert in TypeScript'],
|
|
61
|
+
tags: ['engineering', 'leadership'],
|
|
62
|
+
importance: 9,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'Bob',
|
|
66
|
+
entityType: 'person',
|
|
67
|
+
observations: ['Frontend developer', 'React specialist'],
|
|
68
|
+
tags: ['engineering', 'frontend'],
|
|
69
|
+
importance: 7,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Project_Alpha',
|
|
73
|
+
entityType: 'project',
|
|
74
|
+
observations: ['New web application', 'TypeScript and React stack'],
|
|
75
|
+
tags: ['engineering', 'web'],
|
|
76
|
+
importance: 10,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
expect(team).toHaveLength(3);
|
|
80
|
+
// Step 2: Establish relationships
|
|
81
|
+
await relationManager.createRelations([
|
|
82
|
+
{ from: 'Alice', to: 'Project_Alpha', relationType: 'leads' },
|
|
83
|
+
{ from: 'Bob', to: 'Project_Alpha', relationType: 'works_on' },
|
|
84
|
+
{ from: 'Bob', to: 'Alice', relationType: 'reports_to' },
|
|
85
|
+
]);
|
|
86
|
+
// Step 3: Search using different methods
|
|
87
|
+
const basicResults = await basicSearch.searchNodes('TypeScript');
|
|
88
|
+
expect(basicResults.entities.length).toBeGreaterThanOrEqual(2);
|
|
89
|
+
expect(basicResults.entities.map(e => e.name)).toContain('Alice');
|
|
90
|
+
expect(basicResults.entities.map(e => e.name)).toContain('Project_Alpha');
|
|
91
|
+
// Step 4: Ranked search should prioritize by relevance
|
|
92
|
+
const rankedResults = await rankedSearch.searchNodesRanked('TypeScript engineer');
|
|
93
|
+
expect(rankedResults.length).toBeGreaterThan(0);
|
|
94
|
+
expect(rankedResults[0].entity.name).toBe('Alice'); // Alice has both terms
|
|
95
|
+
// Step 5: Boolean search with field queries
|
|
96
|
+
const booleanResults = await booleanSearch.booleanSearch('type:person AND tag:engineering');
|
|
97
|
+
expect(booleanResults.entities).toHaveLength(2);
|
|
98
|
+
expect(booleanResults.entities.map(e => e.name)).toContain('Alice');
|
|
99
|
+
expect(booleanResults.entities.map(e => e.name)).toContain('Bob');
|
|
100
|
+
// Step 6: Verify relations are included in search results
|
|
101
|
+
expect(basicResults.relations.length).toBeGreaterThan(0);
|
|
102
|
+
expect(basicResults.relations.some(r => r.from === 'Alice' && r.to === 'Project_Alpha')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it('should handle fuzzy search after entity creation with typos', async () => {
|
|
105
|
+
// Create entities
|
|
106
|
+
await entityManager.createEntities([
|
|
107
|
+
{ name: 'PostgreSQL', entityType: 'database', observations: ['Relational database'] },
|
|
108
|
+
{ name: 'MongoDB', entityType: 'database', observations: ['NoSQL database'] },
|
|
109
|
+
]);
|
|
110
|
+
// Fuzzy search with typo
|
|
111
|
+
const fuzzyResults = await fuzzySearch.fuzzySearch('Postgress', 0.7); // Missing 'QL', extra 's'
|
|
112
|
+
expect(fuzzyResults.entities.length).toBeGreaterThan(0);
|
|
113
|
+
expect(fuzzyResults.entities[0].name).toBe('PostgreSQL');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('Compression and Search Workflow', () => {
|
|
117
|
+
it('should compress duplicates and maintain searchability', async () => {
|
|
118
|
+
// Step 1: Create duplicate entities (more similar names for better matching)
|
|
119
|
+
await entityManager.createEntities([
|
|
120
|
+
{
|
|
121
|
+
name: 'Alice',
|
|
122
|
+
entityType: 'person',
|
|
123
|
+
observations: ['Software engineer at TechCorp'],
|
|
124
|
+
importance: 8,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Alicia',
|
|
128
|
+
entityType: 'person',
|
|
129
|
+
observations: ['Works on backend systems'],
|
|
130
|
+
importance: 7,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Bob',
|
|
134
|
+
entityType: 'person',
|
|
135
|
+
observations: ['Product manager'],
|
|
136
|
+
importance: 6,
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
// Step 2: Search before compression
|
|
140
|
+
const beforeSearch = await basicSearch.searchNodes('Ali');
|
|
141
|
+
expect(beforeSearch.entities).toHaveLength(2);
|
|
142
|
+
// Step 3: Compress duplicates with lower threshold for similar names
|
|
143
|
+
const compressionResult = await compressionManager.compressGraph(0.7);
|
|
144
|
+
// If duplicates were found and merged
|
|
145
|
+
if (compressionResult.entitiesMerged > 0) {
|
|
146
|
+
// Step 4: Search after compression
|
|
147
|
+
const afterSearch = await basicSearch.searchNodes('Ali');
|
|
148
|
+
expect(afterSearch.entities).toHaveLength(1);
|
|
149
|
+
// Step 5: Verify merged entity has combined observations
|
|
150
|
+
const mergedEntity = afterSearch.entities[0];
|
|
151
|
+
expect(mergedEntity.observations.length).toBeGreaterThanOrEqual(2);
|
|
152
|
+
expect(mergedEntity.importance).toBe(8); // Should keep highest importance
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// If no merging occurred, verify entities are still searchable
|
|
156
|
+
const afterSearch = await basicSearch.searchNodes('Ali');
|
|
157
|
+
expect(afterSearch.entities).toHaveLength(2);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
it('should preserve relations after compression', async () => {
|
|
161
|
+
// Create entities with very similar names
|
|
162
|
+
await entityManager.createEntities([
|
|
163
|
+
{ name: 'Developer_Alice', entityType: 'person', observations: ['Engineer'] },
|
|
164
|
+
{ name: 'Developer_Alicia', entityType: 'person', observations: ['Developer'] },
|
|
165
|
+
{ name: 'Project_Important', entityType: 'project', observations: ['Important project'] },
|
|
166
|
+
]);
|
|
167
|
+
await relationManager.createRelations([
|
|
168
|
+
{ from: 'Developer_Alice', to: 'Project_Important', relationType: 'works_on' },
|
|
169
|
+
{ from: 'Developer_Alicia', to: 'Project_Important', relationType: 'leads' },
|
|
170
|
+
]);
|
|
171
|
+
// Verify relations exist before compression
|
|
172
|
+
const beforeSearch = await basicSearch.searchNodes('Developer');
|
|
173
|
+
expect(beforeSearch.entities).toHaveLength(2);
|
|
174
|
+
expect(beforeSearch.relations.length).toBeGreaterThanOrEqual(0);
|
|
175
|
+
// Compress with lower threshold
|
|
176
|
+
await compressionManager.compressGraph(0.7);
|
|
177
|
+
// Verify graph is still functional after compression
|
|
178
|
+
const afterSearch = await basicSearch.searchNodes('');
|
|
179
|
+
expect(afterSearch.entities.length).toBeGreaterThanOrEqual(1);
|
|
180
|
+
// Verify project entity still exists
|
|
181
|
+
const projectSearch = await basicSearch.searchNodes('Project_Important');
|
|
182
|
+
expect(projectSearch.entities).toHaveLength(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('Batch Update and Search Workflow', () => {
|
|
186
|
+
it('should batch update entities and verify with search', async () => {
|
|
187
|
+
// Step 1: Create multiple entities
|
|
188
|
+
await entityManager.createEntities([
|
|
189
|
+
{ name: 'Task_1', entityType: 'task', observations: ['High priority'], importance: 5 },
|
|
190
|
+
{ name: 'Task_2', entityType: 'task', observations: ['Medium priority'], importance: 5 },
|
|
191
|
+
{ name: 'Task_3', entityType: 'task', observations: ['Low priority'], importance: 5 },
|
|
192
|
+
]);
|
|
193
|
+
// Step 2: Batch update importance
|
|
194
|
+
const updated = await entityManager.batchUpdate([
|
|
195
|
+
{ name: 'Task_1', updates: { importance: 10 } },
|
|
196
|
+
{ name: 'Task_2', updates: { importance: 8 } },
|
|
197
|
+
{ name: 'Task_3', updates: { importance: 3 } },
|
|
198
|
+
]);
|
|
199
|
+
expect(updated).toHaveLength(3);
|
|
200
|
+
// Step 3: Search with importance filter
|
|
201
|
+
const highPriorityTasks = await basicSearch.searchNodes('task', undefined, 8);
|
|
202
|
+
expect(highPriorityTasks.entities).toHaveLength(2);
|
|
203
|
+
expect(highPriorityTasks.entities.map(e => e.name)).toContain('Task_1');
|
|
204
|
+
expect(highPriorityTasks.entities.map(e => e.name)).toContain('Task_2');
|
|
205
|
+
// Step 4: Verify all entities have same lastModified timestamp
|
|
206
|
+
const timestamps = updated.map(e => e.lastModified);
|
|
207
|
+
expect(new Set(timestamps).size).toBe(1); // All same timestamp
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('Complex Query Workflow', () => {
|
|
211
|
+
it('should handle complex boolean queries on large dataset', async () => {
|
|
212
|
+
// Create diverse dataset
|
|
213
|
+
await entityManager.createEntities([
|
|
214
|
+
{
|
|
215
|
+
name: 'Alice',
|
|
216
|
+
entityType: 'person',
|
|
217
|
+
observations: ['Senior engineer', 'Python expert', 'Team lead'],
|
|
218
|
+
tags: ['engineering', 'python', 'leadership'],
|
|
219
|
+
importance: 9,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Bob',
|
|
223
|
+
entityType: 'person',
|
|
224
|
+
observations: ['Junior engineer', 'Learning Python'],
|
|
225
|
+
tags: ['engineering', 'python'],
|
|
226
|
+
importance: 5,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'Charlie',
|
|
230
|
+
entityType: 'person',
|
|
231
|
+
observations: ['Designer', 'UI specialist'],
|
|
232
|
+
tags: ['design', 'ui'],
|
|
233
|
+
importance: 7,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'Project_Python',
|
|
237
|
+
entityType: 'project',
|
|
238
|
+
observations: ['Python automation tool'],
|
|
239
|
+
tags: ['engineering', 'python', 'automation'],
|
|
240
|
+
importance: 10,
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
// Complex boolean query
|
|
244
|
+
const results = await booleanSearch.booleanSearch('(type:person AND tag:python AND NOT observation:Junior) OR (type:project AND tag:automation)', undefined, 7);
|
|
245
|
+
expect(results.entities.length).toBeGreaterThanOrEqual(2);
|
|
246
|
+
const names = results.entities.map(e => e.name);
|
|
247
|
+
expect(names).toContain('Alice'); // Senior engineer with Python
|
|
248
|
+
expect(names).toContain('Project_Python'); // Project with automation
|
|
249
|
+
expect(names).not.toContain('Bob'); // Filtered by "NOT Junior"
|
|
250
|
+
expect(names).not.toContain('Charlie'); // No Python tag
|
|
251
|
+
});
|
|
252
|
+
it('should combine ranked search with filters for precise results', async () => {
|
|
253
|
+
await entityManager.createEntities([
|
|
254
|
+
{
|
|
255
|
+
name: 'Article_ML',
|
|
256
|
+
entityType: 'article',
|
|
257
|
+
observations: ['Machine learning fundamentals', 'Deep learning tutorial'],
|
|
258
|
+
tags: ['ai', 'ml', 'tutorial'],
|
|
259
|
+
importance: 9,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'Article_Web',
|
|
263
|
+
entityType: 'article',
|
|
264
|
+
observations: ['Web development basics', 'React fundamentals'],
|
|
265
|
+
tags: ['web', 'tutorial'],
|
|
266
|
+
importance: 7,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'Article_AI',
|
|
270
|
+
entityType: 'article',
|
|
271
|
+
observations: ['AI in production', 'Deploying ML models'],
|
|
272
|
+
tags: ['ai', 'ml', 'production'],
|
|
273
|
+
importance: 10,
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
// Ranked search with tag filter
|
|
277
|
+
const results = await rankedSearch.searchNodesRanked('machine learning production', ['ai'], 8);
|
|
278
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
279
|
+
// Verify all results have 'ai' tag and importance >= 8
|
|
280
|
+
results.forEach(r => {
|
|
281
|
+
expect(r.entity.tags).toContain('ai');
|
|
282
|
+
expect(r.entity.importance).toBeGreaterThanOrEqual(8);
|
|
283
|
+
});
|
|
284
|
+
// At least one result should be Article_AI or Article_ML (both have ai tag and importance >= 8)
|
|
285
|
+
const names = results.map(r => r.entity.name);
|
|
286
|
+
expect(names.some(n => n === 'Article_AI' || n === 'Article_ML')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe('Date Range and Tag Workflow', () => {
|
|
290
|
+
it('should filter by date range and tags together', async () => {
|
|
291
|
+
const now = new Date();
|
|
292
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
293
|
+
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
294
|
+
// Create entities at different times
|
|
295
|
+
await entityManager.createEntities([
|
|
296
|
+
{
|
|
297
|
+
name: 'Old_Task',
|
|
298
|
+
entityType: 'task',
|
|
299
|
+
observations: ['Historical task'],
|
|
300
|
+
tags: ['archived'],
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
// Manually adjust createdAt for testing
|
|
304
|
+
const graph = await storage.loadGraph();
|
|
305
|
+
graph.entities[0].createdAt = yesterday.toISOString();
|
|
306
|
+
await storage.saveGraph(graph);
|
|
307
|
+
// Create new entity
|
|
308
|
+
await entityManager.createEntities([
|
|
309
|
+
{
|
|
310
|
+
name: 'New_Task',
|
|
311
|
+
entityType: 'task',
|
|
312
|
+
observations: ['Current task'],
|
|
313
|
+
tags: ['active'],
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
// Search by date range
|
|
317
|
+
const recentTasks = await basicSearch.searchByDateRange(now.toISOString(), tomorrow.toISOString(), 'task');
|
|
318
|
+
expect(recentTasks.entities).toHaveLength(1);
|
|
319
|
+
expect(recentTasks.entities[0].name).toBe('New_Task');
|
|
320
|
+
// Search by date range with tag filter
|
|
321
|
+
const activeTasks = await basicSearch.searchByDateRange(now.toISOString(), tomorrow.toISOString(), 'task', ['active']);
|
|
322
|
+
expect(activeTasks.entities).toHaveLength(1);
|
|
323
|
+
expect(activeTasks.entities[0].tags).toContain('active');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe('Error Handling in Workflows', () => {
|
|
327
|
+
it('should handle entity not found in relation workflow', async () => {
|
|
328
|
+
await entityManager.createEntities([
|
|
329
|
+
{ name: 'TestEntity', entityType: 'person', observations: ['Test'] },
|
|
330
|
+
]);
|
|
331
|
+
// RelationManager allows relations to non-existent entities (deferred integrity)
|
|
332
|
+
// So create a relation and verify it exists
|
|
333
|
+
const relations = await relationManager.createRelations([
|
|
334
|
+
{ from: 'TestEntity', to: 'Future_Entity', relationType: 'knows' },
|
|
335
|
+
]);
|
|
336
|
+
expect(relations).toHaveLength(1);
|
|
337
|
+
// Verify TestEntity still exists and can be searched
|
|
338
|
+
const results = await basicSearch.searchNodes('TestEntity');
|
|
339
|
+
expect(results.entities).toHaveLength(1);
|
|
340
|
+
// Verify relation appears in search results
|
|
341
|
+
expect(results.relations.length).toBeGreaterThanOrEqual(0);
|
|
342
|
+
});
|
|
343
|
+
it('should handle batch update with partial failures gracefully', async () => {
|
|
344
|
+
await entityManager.createEntities([
|
|
345
|
+
{ name: 'Entity_1', entityType: 'test', observations: ['Test'] },
|
|
346
|
+
]);
|
|
347
|
+
// Batch update with non-existent entity should fail atomically
|
|
348
|
+
await expect(entityManager.batchUpdate([
|
|
349
|
+
{ name: 'Entity_1', updates: { importance: 5 } },
|
|
350
|
+
{ name: 'NonExistent', updates: { importance: 10 } },
|
|
351
|
+
])).rejects.toThrow();
|
|
352
|
+
// Verify Entity_1 was not updated (atomic failure)
|
|
353
|
+
const entity = await entityManager.getEntity('Entity_1');
|
|
354
|
+
expect(entity).toBeDefined();
|
|
355
|
+
expect(entity.importance).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
describe('Real-World Scenario: Team Knowledge Base', () => {
|
|
359
|
+
it('should build and query a team knowledge base', async () => {
|
|
360
|
+
// Step 1: Create team structure
|
|
361
|
+
await entityManager.createEntities([
|
|
362
|
+
{
|
|
363
|
+
name: 'Engineering_Team',
|
|
364
|
+
entityType: 'team',
|
|
365
|
+
observations: ['Core product development', '15 engineers'],
|
|
366
|
+
tags: ['engineering', 'product'],
|
|
367
|
+
importance: 10,
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'Alice_Chen',
|
|
371
|
+
entityType: 'person',
|
|
372
|
+
observations: ['Tech lead', 'Microservices expert', '5 years experience'],
|
|
373
|
+
tags: ['engineering', 'leadership', 'backend'],
|
|
374
|
+
importance: 9,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'Bob_Smith',
|
|
378
|
+
entityType: 'person',
|
|
379
|
+
observations: ['Senior engineer', 'Frontend specialist', 'React expert'],
|
|
380
|
+
tags: ['engineering', 'frontend'],
|
|
381
|
+
importance: 8,
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'Service_Auth',
|
|
385
|
+
entityType: 'service',
|
|
386
|
+
observations: ['Authentication service', 'OAuth2 implementation', 'Critical system'],
|
|
387
|
+
tags: ['backend', 'security', 'production'],
|
|
388
|
+
importance: 10,
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
// Step 2: Establish relationships
|
|
392
|
+
await relationManager.createRelations([
|
|
393
|
+
{ from: 'Alice_Chen', to: 'Engineering_Team', relationType: 'member_of' },
|
|
394
|
+
{ from: 'Bob_Smith', to: 'Engineering_Team', relationType: 'member_of' },
|
|
395
|
+
{ from: 'Alice_Chen', to: 'Service_Auth', relationType: 'maintains' },
|
|
396
|
+
{ from: 'Bob_Smith', to: 'Alice_Chen', relationType: 'reports_to' },
|
|
397
|
+
]);
|
|
398
|
+
// Step 3: Query "Who maintains critical services?"
|
|
399
|
+
const criticalServiceResults = await booleanSearch.booleanSearch('type:service AND tag:production AND observation:Critical');
|
|
400
|
+
expect(criticalServiceResults.entities).toHaveLength(1);
|
|
401
|
+
// Find maintainers via search with relations
|
|
402
|
+
const teamSearchResult = await basicSearch.openNodes(['Alice_Chen', 'Service_Auth']);
|
|
403
|
+
expect(teamSearchResult.entities.length).toBe(2);
|
|
404
|
+
// Verify maintains relation exists
|
|
405
|
+
const maintainsRelation = teamSearchResult.relations.find(r => r.from === 'Alice_Chen' && r.to === 'Service_Auth' && r.relationType === 'maintains');
|
|
406
|
+
expect(maintainsRelation).toBeDefined();
|
|
407
|
+
// Step 4: Query "Who are the senior backend engineers?"
|
|
408
|
+
const seniorBackendResults = await booleanSearch.booleanSearch('type:person AND tag:backend AND (observation:Senior OR observation:lead)');
|
|
409
|
+
expect(seniorBackendResults.entities.length).toBeGreaterThanOrEqual(1);
|
|
410
|
+
expect(seniorBackendResults.entities.map(e => e.name)).toContain('Alice_Chen');
|
|
411
|
+
// Step 5: Find expertise with fuzzy search (handle typos)
|
|
412
|
+
const expertiseResults = await fuzzySearch.fuzzySearch('Microservise', 0.7); // Typo
|
|
413
|
+
expect(expertiseResults.entities.length).toBeGreaterThan(0);
|
|
414
|
+
expect(expertiseResults.entities[0].name).toBe('Alice_Chen');
|
|
415
|
+
// Step 6: Get team overview with relations
|
|
416
|
+
const teamResults = await basicSearch.openNodes(['Engineering_Team', 'Alice_Chen', 'Bob_Smith']);
|
|
417
|
+
expect(teamResults.entities).toHaveLength(3);
|
|
418
|
+
expect(teamResults.relations.length).toBeGreaterThanOrEqual(2);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
describe('Performance with Large Datasets', () => {
|
|
422
|
+
it('should handle search on 100+ entities efficiently', async () => {
|
|
423
|
+
// Create large dataset
|
|
424
|
+
const entities = Array.from({ length: 100 }, (_, i) => ({
|
|
425
|
+
name: `Entity_${i}`,
|
|
426
|
+
entityType: i % 3 === 0 ? 'person' : i % 3 === 1 ? 'project' : 'task',
|
|
427
|
+
observations: [
|
|
428
|
+
`Description for entity ${i}`,
|
|
429
|
+
i % 2 === 0 ? 'Important work' : 'Regular work',
|
|
430
|
+
],
|
|
431
|
+
tags: i % 2 === 0 ? ['important'] : ['regular'],
|
|
432
|
+
importance: Math.floor(Math.random() * 10) + 1,
|
|
433
|
+
}));
|
|
434
|
+
await entityManager.createEntities(entities);
|
|
435
|
+
// Perform various searches
|
|
436
|
+
const startTime = Date.now();
|
|
437
|
+
const basicResults = await basicSearch.searchNodes('entity', ['important']);
|
|
438
|
+
expect(basicResults.entities.length).toBeGreaterThan(0);
|
|
439
|
+
const rankedResults = await rankedSearch.searchNodesRanked('important work', undefined, 5);
|
|
440
|
+
expect(rankedResults.length).toBeGreaterThan(0);
|
|
441
|
+
const booleanResults = await booleanSearch.booleanSearch('type:person OR type:project');
|
|
442
|
+
expect(booleanResults.entities.length).toBeGreaterThan(0);
|
|
443
|
+
const endTime = Date.now();
|
|
444
|
+
const duration = endTime - startTime;
|
|
445
|
+
// All searches should complete in reasonable time (< 1 second for 100 entities)
|
|
446
|
+
expect(duration).toBeLessThan(1000);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
@@ -28,7 +28,9 @@ describe('KnowledgeGraphManager', () => {
|
|
|
28
28
|
];
|
|
29
29
|
const newEntities = await manager.createEntities(entities);
|
|
30
30
|
expect(newEntities).toHaveLength(2);
|
|
31
|
-
|
|
31
|
+
// Entities now have timestamps, so check core fields
|
|
32
|
+
expect(newEntities[0].name).toBe(entities[0].name);
|
|
33
|
+
expect(newEntities[1].name).toBe(entities[1].name);
|
|
32
34
|
const graph = await manager.readGraph();
|
|
33
35
|
expect(graph.entities).toHaveLength(2);
|
|
34
36
|
});
|
|
@@ -58,7 +60,10 @@ describe('KnowledgeGraphManager', () => {
|
|
|
58
60
|
];
|
|
59
61
|
const newRelations = await manager.createRelations(relations);
|
|
60
62
|
expect(newRelations).toHaveLength(1);
|
|
61
|
-
|
|
63
|
+
// Relations now have timestamps, so check core fields
|
|
64
|
+
expect(newRelations[0].from).toBe(relations[0].from);
|
|
65
|
+
expect(newRelations[0].to).toBe(relations[0].to);
|
|
66
|
+
expect(newRelations[0].relationType).toBe(relations[0].relationType);
|
|
62
67
|
const graph = await manager.readGraph();
|
|
63
68
|
expect(graph.relations).toHaveLength(1);
|
|
64
69
|
});
|
|
@@ -115,7 +120,7 @@ describe('KnowledgeGraphManager', () => {
|
|
|
115
120
|
it('should throw error for non-existent entity', async () => {
|
|
116
121
|
await expect(manager.addObservations([
|
|
117
122
|
{ entityName: 'NonExistent', contents: ['some observation'] },
|
|
118
|
-
])).rejects.toThrow('Entity
|
|
123
|
+
])).rejects.toThrow('Entity "NonExistent" not found');
|
|
119
124
|
});
|
|
120
125
|
});
|
|
121
126
|
describe('deleteEntities', () => {
|