@danielsimonjr/memory-mcp 0.7.1 → 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 -997
- 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,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntityManager Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { EntityManager } from '../../../core/EntityManager.js';
|
|
6
|
+
import { GraphStorage } from '../../../core/GraphStorage.js';
|
|
7
|
+
import { EntityNotFoundError, ValidationError } from '../../../utils/errors.js';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
describe('EntityManager', () => {
|
|
12
|
+
let storage;
|
|
13
|
+
let manager;
|
|
14
|
+
let testDir;
|
|
15
|
+
let testFilePath;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
// Create unique temp directory for each test
|
|
18
|
+
testDir = join(tmpdir(), `entity-manager-test-${Date.now()}-${Math.random()}`);
|
|
19
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
20
|
+
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
21
|
+
storage = new GraphStorage(testFilePath);
|
|
22
|
+
manager = new EntityManager(storage);
|
|
23
|
+
});
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
// Clean up test files
|
|
26
|
+
try {
|
|
27
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Ignore cleanup errors
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
describe('createEntities', () => {
|
|
34
|
+
it('should create a single entity with timestamps', async () => {
|
|
35
|
+
const entities = await manager.createEntities([
|
|
36
|
+
{
|
|
37
|
+
name: 'Alice',
|
|
38
|
+
entityType: 'person',
|
|
39
|
+
observations: ['Software engineer'],
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
expect(entities).toHaveLength(1);
|
|
43
|
+
expect(entities[0].name).toBe('Alice');
|
|
44
|
+
expect(entities[0].entityType).toBe('person');
|
|
45
|
+
expect(entities[0].observations).toEqual(['Software engineer']);
|
|
46
|
+
expect(entities[0].createdAt).toBeDefined();
|
|
47
|
+
expect(entities[0].lastModified).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
it('should create multiple entities in batch', async () => {
|
|
50
|
+
const entities = await manager.createEntities([
|
|
51
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
52
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
53
|
+
{ name: 'Company', entityType: 'organization', observations: [] },
|
|
54
|
+
]);
|
|
55
|
+
expect(entities).toHaveLength(3);
|
|
56
|
+
expect(entities.map(e => e.name)).toEqual(['Alice', 'Bob', 'Company']);
|
|
57
|
+
});
|
|
58
|
+
it('should filter out duplicate entities', async () => {
|
|
59
|
+
await manager.createEntities([
|
|
60
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
61
|
+
]);
|
|
62
|
+
const result = await manager.createEntities([
|
|
63
|
+
{ name: 'Alice', entityType: 'person', observations: ['Duplicate'] },
|
|
64
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
65
|
+
]);
|
|
66
|
+
expect(result).toHaveLength(1);
|
|
67
|
+
expect(result[0].name).toBe('Bob');
|
|
68
|
+
});
|
|
69
|
+
it('should normalize tags to lowercase', async () => {
|
|
70
|
+
const entities = await manager.createEntities([
|
|
71
|
+
{
|
|
72
|
+
name: 'Alice',
|
|
73
|
+
entityType: 'person',
|
|
74
|
+
observations: [],
|
|
75
|
+
tags: ['Engineering', 'LEADERSHIP', 'Team'],
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
expect(entities[0].tags).toEqual(['engineering', 'leadership', 'team']);
|
|
79
|
+
});
|
|
80
|
+
it('should validate importance range', async () => {
|
|
81
|
+
await expect(manager.createEntities([
|
|
82
|
+
{
|
|
83
|
+
name: 'Alice',
|
|
84
|
+
entityType: 'person',
|
|
85
|
+
observations: [],
|
|
86
|
+
importance: 11,
|
|
87
|
+
},
|
|
88
|
+
])).rejects.toThrow();
|
|
89
|
+
});
|
|
90
|
+
it('should throw ValidationError for invalid entity data', async () => {
|
|
91
|
+
await expect(manager.createEntities([
|
|
92
|
+
{
|
|
93
|
+
name: '',
|
|
94
|
+
entityType: 'person',
|
|
95
|
+
observations: [],
|
|
96
|
+
},
|
|
97
|
+
])).rejects.toThrow(ValidationError);
|
|
98
|
+
});
|
|
99
|
+
it('should handle empty array (no-op)', async () => {
|
|
100
|
+
const result = await manager.createEntities([]);
|
|
101
|
+
expect(result).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
it('should preserve optional fields', async () => {
|
|
104
|
+
const entities = await manager.createEntities([
|
|
105
|
+
{
|
|
106
|
+
name: 'Alice',
|
|
107
|
+
entityType: 'person',
|
|
108
|
+
observations: ['Engineer'],
|
|
109
|
+
importance: 8,
|
|
110
|
+
tags: ['team'],
|
|
111
|
+
parentId: 'Company',
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
expect(entities[0].importance).toBe(8);
|
|
115
|
+
expect(entities[0].tags).toEqual(['team']);
|
|
116
|
+
expect(entities[0].parentId).toBe('Company');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('deleteEntities', () => {
|
|
120
|
+
beforeEach(async () => {
|
|
121
|
+
await manager.createEntities([
|
|
122
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
123
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
it('should delete a single entity', async () => {
|
|
127
|
+
await manager.deleteEntities(['Alice']);
|
|
128
|
+
const alice = await manager.getEntity('Alice');
|
|
129
|
+
expect(alice).toBeNull();
|
|
130
|
+
const bob = await manager.getEntity('Bob');
|
|
131
|
+
expect(bob).not.toBeNull();
|
|
132
|
+
});
|
|
133
|
+
it('should delete multiple entities', async () => {
|
|
134
|
+
await manager.deleteEntities(['Alice', 'Bob']);
|
|
135
|
+
const alice = await manager.getEntity('Alice');
|
|
136
|
+
const bob = await manager.getEntity('Bob');
|
|
137
|
+
expect(alice).toBeNull();
|
|
138
|
+
expect(bob).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
it('should silently ignore non-existent entities', async () => {
|
|
141
|
+
await expect(manager.deleteEntities(['NonExistent'])).resolves.not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
it('should throw ValidationError for invalid input', async () => {
|
|
144
|
+
await expect(manager.deleteEntities([])).rejects.toThrow(ValidationError);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('getEntity', () => {
|
|
148
|
+
beforeEach(async () => {
|
|
149
|
+
await manager.createEntities([
|
|
150
|
+
{
|
|
151
|
+
name: 'Alice',
|
|
152
|
+
entityType: 'person',
|
|
153
|
+
observations: ['Software engineer'],
|
|
154
|
+
importance: 8,
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
it('should retrieve an existing entity', async () => {
|
|
159
|
+
const alice = await manager.getEntity('Alice');
|
|
160
|
+
expect(alice).not.toBeNull();
|
|
161
|
+
expect(alice.name).toBe('Alice');
|
|
162
|
+
expect(alice.entityType).toBe('person');
|
|
163
|
+
expect(alice.observations).toEqual(['Software engineer']);
|
|
164
|
+
expect(alice.importance).toBe(8);
|
|
165
|
+
});
|
|
166
|
+
it('should return null for non-existent entity', async () => {
|
|
167
|
+
const result = await manager.getEntity('NonExistent');
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
it('should be case-sensitive', async () => {
|
|
171
|
+
const result = await manager.getEntity('alice');
|
|
172
|
+
expect(result).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('updateEntity', () => {
|
|
176
|
+
beforeEach(async () => {
|
|
177
|
+
await manager.createEntities([
|
|
178
|
+
{
|
|
179
|
+
name: 'Alice',
|
|
180
|
+
entityType: 'person',
|
|
181
|
+
observations: ['Engineer'],
|
|
182
|
+
importance: 5,
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
it('should update entity importance', async () => {
|
|
187
|
+
const updated = await manager.updateEntity('Alice', {
|
|
188
|
+
importance: 9,
|
|
189
|
+
});
|
|
190
|
+
expect(updated.importance).toBe(9);
|
|
191
|
+
expect(updated.name).toBe('Alice');
|
|
192
|
+
});
|
|
193
|
+
it('should update entity observations', async () => {
|
|
194
|
+
const updated = await manager.updateEntity('Alice', {
|
|
195
|
+
observations: ['Senior Engineer', 'Team Lead'],
|
|
196
|
+
});
|
|
197
|
+
expect(updated.observations).toEqual(['Senior Engineer', 'Team Lead']);
|
|
198
|
+
});
|
|
199
|
+
it('should update lastModified timestamp', async () => {
|
|
200
|
+
const original = await manager.getEntity('Alice');
|
|
201
|
+
const originalTimestamp = original.lastModified;
|
|
202
|
+
// Wait a bit to ensure timestamp difference
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
204
|
+
const updated = await manager.updateEntity('Alice', {
|
|
205
|
+
importance: 8,
|
|
206
|
+
});
|
|
207
|
+
expect(updated.lastModified).not.toBe(originalTimestamp);
|
|
208
|
+
});
|
|
209
|
+
it('should throw EntityNotFoundError for non-existent entity', async () => {
|
|
210
|
+
await expect(manager.updateEntity('NonExistent', { importance: 5 })).rejects.toThrow(EntityNotFoundError);
|
|
211
|
+
});
|
|
212
|
+
it('should throw ValidationError for invalid updates', async () => {
|
|
213
|
+
await expect(manager.updateEntity('Alice', { importance: 11 })).rejects.toThrow(ValidationError);
|
|
214
|
+
});
|
|
215
|
+
it('should update multiple fields at once', async () => {
|
|
216
|
+
const updated = await manager.updateEntity('Alice', {
|
|
217
|
+
entityType: 'senior_engineer',
|
|
218
|
+
importance: 9,
|
|
219
|
+
tags: ['leadership'],
|
|
220
|
+
observations: ['Lead Engineer'],
|
|
221
|
+
});
|
|
222
|
+
expect(updated.entityType).toBe('senior_engineer');
|
|
223
|
+
expect(updated.importance).toBe(9);
|
|
224
|
+
expect(updated.tags).toEqual(['leadership']);
|
|
225
|
+
expect(updated.observations).toEqual(['Lead Engineer']);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('batchUpdate', () => {
|
|
229
|
+
beforeEach(async () => {
|
|
230
|
+
await manager.createEntities([
|
|
231
|
+
{ name: 'Alice', entityType: 'person', observations: ['Engineer'], importance: 7 },
|
|
232
|
+
{ name: 'Bob', entityType: 'person', observations: ['Manager'], importance: 6 },
|
|
233
|
+
{ name: 'Charlie', entityType: 'person', observations: ['Designer'], importance: 5 },
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
it('should update multiple entities in a single operation', async () => {
|
|
237
|
+
const updated = await manager.batchUpdate([
|
|
238
|
+
{ name: 'Alice', updates: { importance: 9 } },
|
|
239
|
+
{ name: 'Bob', updates: { importance: 8 } },
|
|
240
|
+
]);
|
|
241
|
+
expect(updated).toHaveLength(2);
|
|
242
|
+
expect(updated[0].importance).toBe(9);
|
|
243
|
+
expect(updated[1].importance).toBe(8);
|
|
244
|
+
});
|
|
245
|
+
it('should update different fields for different entities', async () => {
|
|
246
|
+
const updated = await manager.batchUpdate([
|
|
247
|
+
{ name: 'Alice', updates: { tags: ['senior', 'tech-lead'] } },
|
|
248
|
+
{ name: 'Bob', updates: { entityType: 'senior_manager' } },
|
|
249
|
+
{ name: 'Charlie', updates: { importance: 8, tags: ['ui-expert'] } },
|
|
250
|
+
]);
|
|
251
|
+
expect(updated).toHaveLength(3);
|
|
252
|
+
expect(updated[0].tags).toEqual(['senior', 'tech-lead']);
|
|
253
|
+
expect(updated[1].entityType).toBe('senior_manager');
|
|
254
|
+
expect(updated[2].importance).toBe(8);
|
|
255
|
+
expect(updated[2].tags).toEqual(['ui-expert']);
|
|
256
|
+
});
|
|
257
|
+
it('should update lastModified timestamp for all entities', async () => {
|
|
258
|
+
const beforeUpdate = new Date().toISOString();
|
|
259
|
+
// Wait a bit to ensure timestamp difference
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
261
|
+
const updated = await manager.batchUpdate([
|
|
262
|
+
{ name: 'Alice', updates: { importance: 9 } },
|
|
263
|
+
{ name: 'Bob', updates: { importance: 8 } },
|
|
264
|
+
]);
|
|
265
|
+
expect(updated[0].lastModified >= beforeUpdate).toBe(true);
|
|
266
|
+
expect(updated[1].lastModified >= beforeUpdate).toBe(true);
|
|
267
|
+
expect(updated[0].lastModified).toBe(updated[1].lastModified); // Same timestamp
|
|
268
|
+
});
|
|
269
|
+
it('should only load and save graph once', async () => {
|
|
270
|
+
// This is a performance benefit - single load/save vs multiple
|
|
271
|
+
const updated = await manager.batchUpdate([
|
|
272
|
+
{ name: 'Alice', updates: { importance: 10 } },
|
|
273
|
+
{ name: 'Bob', updates: { importance: 9 } },
|
|
274
|
+
{ name: 'Charlie', updates: { importance: 8 } },
|
|
275
|
+
]);
|
|
276
|
+
expect(updated).toHaveLength(3);
|
|
277
|
+
// Verify all updates persisted
|
|
278
|
+
const alice = await manager.getEntity('Alice');
|
|
279
|
+
const bob = await manager.getEntity('Bob');
|
|
280
|
+
const charlie = await manager.getEntity('Charlie');
|
|
281
|
+
expect(alice.importance).toBe(10);
|
|
282
|
+
expect(bob.importance).toBe(9);
|
|
283
|
+
expect(charlie.importance).toBe(8);
|
|
284
|
+
});
|
|
285
|
+
it('should throw EntityNotFoundError if any entity not found', async () => {
|
|
286
|
+
await expect(manager.batchUpdate([
|
|
287
|
+
{ name: 'Alice', updates: { importance: 9 } },
|
|
288
|
+
{ name: 'NonExistent', updates: { importance: 8 } },
|
|
289
|
+
])).rejects.toThrow(EntityNotFoundError);
|
|
290
|
+
// Verify no updates were applied (atomic operation)
|
|
291
|
+
const alice = await manager.getEntity('Alice');
|
|
292
|
+
expect(alice.importance).toBe(7); // Original value
|
|
293
|
+
});
|
|
294
|
+
it('should throw ValidationError for invalid update data', async () => {
|
|
295
|
+
await expect(manager.batchUpdate([
|
|
296
|
+
{ name: 'Alice', updates: { importance: 9 } },
|
|
297
|
+
{ name: 'Bob', updates: { importance: 11 } }, // Invalid: > 10
|
|
298
|
+
])).rejects.toThrow(ValidationError);
|
|
299
|
+
});
|
|
300
|
+
it('should handle empty updates array', async () => {
|
|
301
|
+
const updated = await manager.batchUpdate([]);
|
|
302
|
+
expect(updated).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
it('should handle single entity update', async () => {
|
|
305
|
+
const updated = await manager.batchUpdate([
|
|
306
|
+
{ name: 'Alice', updates: { importance: 10 } },
|
|
307
|
+
]);
|
|
308
|
+
expect(updated).toHaveLength(1);
|
|
309
|
+
expect(updated[0].importance).toBe(10);
|
|
310
|
+
});
|
|
311
|
+
it('should preserve unchanged fields', async () => {
|
|
312
|
+
const beforeAlice = await manager.getEntity('Alice');
|
|
313
|
+
const updated = await manager.batchUpdate([
|
|
314
|
+
{ name: 'Alice', updates: { importance: 10 } },
|
|
315
|
+
]);
|
|
316
|
+
expect(updated[0].entityType).toBe(beforeAlice.entityType);
|
|
317
|
+
expect(updated[0].observations).toEqual(beforeAlice.observations);
|
|
318
|
+
expect(updated[0].importance).toBe(10); // Changed
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe('persistence', () => {
|
|
322
|
+
it('should persist entities across storage instances', async () => {
|
|
323
|
+
await manager.createEntities([
|
|
324
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
325
|
+
]);
|
|
326
|
+
// Create new storage and manager instances
|
|
327
|
+
const newStorage = new GraphStorage(testFilePath);
|
|
328
|
+
const newManager = new EntityManager(newStorage);
|
|
329
|
+
const alice = await newManager.getEntity('Alice');
|
|
330
|
+
expect(alice).not.toBeNull();
|
|
331
|
+
expect(alice.name).toBe('Alice');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphStorage Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { GraphStorage } from '../../../core/GraphStorage.js';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { tmpdir } from 'os';
|
|
9
|
+
describe('GraphStorage', () => {
|
|
10
|
+
let storage;
|
|
11
|
+
let testDir;
|
|
12
|
+
let testFilePath;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
testDir = join(tmpdir(), `graph-storage-test-${Date.now()}-${Math.random()}`);
|
|
15
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
16
|
+
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
17
|
+
storage = new GraphStorage(testFilePath);
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
try {
|
|
21
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Ignore cleanup errors
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
describe('loadGraph', () => {
|
|
28
|
+
it('should return empty graph when file does not exist', async () => {
|
|
29
|
+
const graph = await storage.loadGraph();
|
|
30
|
+
expect(graph.entities).toEqual([]);
|
|
31
|
+
expect(graph.relations).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
it('should load entities and relations from file', async () => {
|
|
34
|
+
// Write test data
|
|
35
|
+
const testData = [
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
type: 'entity',
|
|
38
|
+
name: 'Alice',
|
|
39
|
+
entityType: 'person',
|
|
40
|
+
observations: ['Engineer'],
|
|
41
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
42
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
43
|
+
}),
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
type: 'relation',
|
|
46
|
+
from: 'Alice',
|
|
47
|
+
to: 'Bob',
|
|
48
|
+
relationType: 'knows',
|
|
49
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
50
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
51
|
+
}),
|
|
52
|
+
].join('\n');
|
|
53
|
+
await fs.writeFile(testFilePath, testData);
|
|
54
|
+
const graph = await storage.loadGraph();
|
|
55
|
+
expect(graph.entities).toHaveLength(1);
|
|
56
|
+
expect(graph.entities[0].name).toBe('Alice');
|
|
57
|
+
expect(graph.relations).toHaveLength(1);
|
|
58
|
+
expect(graph.relations[0].from).toBe('Alice');
|
|
59
|
+
});
|
|
60
|
+
it('should add missing timestamps for backward compatibility', async () => {
|
|
61
|
+
const testData = JSON.stringify({
|
|
62
|
+
type: 'entity',
|
|
63
|
+
name: 'Alice',
|
|
64
|
+
entityType: 'person',
|
|
65
|
+
observations: [],
|
|
66
|
+
});
|
|
67
|
+
await fs.writeFile(testFilePath, testData);
|
|
68
|
+
const graph = await storage.loadGraph();
|
|
69
|
+
expect(graph.entities[0].createdAt).toBeDefined();
|
|
70
|
+
expect(graph.entities[0].lastModified).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
it('should use cache on second load', async () => {
|
|
73
|
+
// First load - populates cache
|
|
74
|
+
await storage.loadGraph();
|
|
75
|
+
// Modify file directly
|
|
76
|
+
await fs.writeFile(testFilePath, JSON.stringify({
|
|
77
|
+
type: 'entity',
|
|
78
|
+
name: 'Modified',
|
|
79
|
+
entityType: 'test',
|
|
80
|
+
observations: [],
|
|
81
|
+
}));
|
|
82
|
+
// Second load - should return cached data (not modified data)
|
|
83
|
+
const graph = await storage.loadGraph();
|
|
84
|
+
expect(graph.entities).toHaveLength(0); // Empty from first load
|
|
85
|
+
});
|
|
86
|
+
it('should return deep copy of cached data', async () => {
|
|
87
|
+
const graph1 = await storage.loadGraph();
|
|
88
|
+
graph1.entities.push({
|
|
89
|
+
name: 'Mutated',
|
|
90
|
+
entityType: 'test',
|
|
91
|
+
observations: [],
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
lastModified: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
const graph2 = await storage.loadGraph();
|
|
96
|
+
expect(graph2.entities).toHaveLength(0); // Not affected by mutation
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('saveGraph', () => {
|
|
100
|
+
it('should save entities and relations to JSONL format', async () => {
|
|
101
|
+
const graph = {
|
|
102
|
+
entities: [
|
|
103
|
+
{
|
|
104
|
+
name: 'Alice',
|
|
105
|
+
entityType: 'person',
|
|
106
|
+
observations: ['Engineer'],
|
|
107
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
108
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
relations: [
|
|
112
|
+
{
|
|
113
|
+
from: 'Alice',
|
|
114
|
+
to: 'Bob',
|
|
115
|
+
relationType: 'knows',
|
|
116
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
117
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
await storage.saveGraph(graph);
|
|
122
|
+
const content = await fs.readFile(testFilePath, 'utf-8');
|
|
123
|
+
const lines = content.split('\n');
|
|
124
|
+
expect(lines).toHaveLength(2);
|
|
125
|
+
const entity = JSON.parse(lines[0]);
|
|
126
|
+
expect(entity.type).toBe('entity');
|
|
127
|
+
expect(entity.name).toBe('Alice');
|
|
128
|
+
const relation = JSON.parse(lines[1]);
|
|
129
|
+
expect(relation.type).toBe('relation');
|
|
130
|
+
expect(relation.from).toBe('Alice');
|
|
131
|
+
});
|
|
132
|
+
it('should include optional entity fields', async () => {
|
|
133
|
+
const graph = {
|
|
134
|
+
entities: [
|
|
135
|
+
{
|
|
136
|
+
name: 'Alice',
|
|
137
|
+
entityType: 'person',
|
|
138
|
+
observations: [],
|
|
139
|
+
tags: ['team'],
|
|
140
|
+
importance: 8,
|
|
141
|
+
parentId: 'Company',
|
|
142
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
143
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
relations: [],
|
|
147
|
+
};
|
|
148
|
+
await storage.saveGraph(graph);
|
|
149
|
+
const content = await fs.readFile(testFilePath, 'utf-8');
|
|
150
|
+
const entity = JSON.parse(content);
|
|
151
|
+
expect(entity.tags).toEqual(['team']);
|
|
152
|
+
expect(entity.importance).toBe(8);
|
|
153
|
+
expect(entity.parentId).toBe('Company');
|
|
154
|
+
});
|
|
155
|
+
it('should invalidate cache after save', async () => {
|
|
156
|
+
// Load to populate cache
|
|
157
|
+
await storage.loadGraph();
|
|
158
|
+
// Save new data
|
|
159
|
+
const graph = {
|
|
160
|
+
entities: [{
|
|
161
|
+
name: 'Alice',
|
|
162
|
+
entityType: 'person',
|
|
163
|
+
observations: [],
|
|
164
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
165
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
166
|
+
}],
|
|
167
|
+
relations: [],
|
|
168
|
+
};
|
|
169
|
+
await storage.saveGraph(graph);
|
|
170
|
+
// Load again - should read from disk (cache invalidated)
|
|
171
|
+
const loaded = await storage.loadGraph();
|
|
172
|
+
expect(loaded.entities).toHaveLength(1);
|
|
173
|
+
expect(loaded.entities[0].name).toBe('Alice');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe('clearCache', () => {
|
|
177
|
+
it('should clear the in-memory cache', async () => {
|
|
178
|
+
// Load to populate cache
|
|
179
|
+
await storage.loadGraph();
|
|
180
|
+
// Modify file
|
|
181
|
+
const graph = {
|
|
182
|
+
entities: [{
|
|
183
|
+
name: 'NewEntity',
|
|
184
|
+
entityType: 'test',
|
|
185
|
+
observations: [],
|
|
186
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
187
|
+
lastModified: '2024-01-01T00:00:00.000Z',
|
|
188
|
+
}],
|
|
189
|
+
relations: [],
|
|
190
|
+
};
|
|
191
|
+
await storage.saveGraph(graph);
|
|
192
|
+
// Clear cache manually
|
|
193
|
+
storage.clearCache();
|
|
194
|
+
// Load - should read from disk
|
|
195
|
+
const loaded = await storage.loadGraph();
|
|
196
|
+
expect(loaded.entities).toHaveLength(1);
|
|
197
|
+
expect(loaded.entities[0].name).toBe('NewEntity');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('getFilePath', () => {
|
|
201
|
+
it('should return the file path', () => {
|
|
202
|
+
expect(storage.getFilePath()).toBe(testFilePath);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|