@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.
Files changed (61) hide show
  1. package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
  2. package/dist/__tests__/file-path.test.js +5 -5
  3. package/dist/__tests__/integration/workflows.test.js +449 -0
  4. package/dist/__tests__/knowledge-graph.test.js +8 -3
  5. package/dist/__tests__/performance/benchmarks.test.js +411 -0
  6. package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
  7. package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
  8. package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
  9. package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
  10. package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
  11. package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
  12. package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
  13. package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
  14. package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
  15. package/dist/core/EntityManager.js +554 -0
  16. package/dist/core/GraphStorage.js +172 -0
  17. package/dist/core/KnowledgeGraphManager.js +400 -0
  18. package/dist/core/ObservationManager.js +129 -0
  19. package/dist/core/RelationManager.js +186 -0
  20. package/dist/core/TransactionManager.js +389 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/features/AnalyticsManager.js +222 -0
  23. package/dist/features/ArchiveManager.js +74 -0
  24. package/dist/features/BackupManager.js +311 -0
  25. package/dist/features/CompressionManager.js +310 -0
  26. package/dist/features/ExportManager.js +305 -0
  27. package/dist/features/HierarchyManager.js +219 -0
  28. package/dist/features/ImportExportManager.js +50 -0
  29. package/dist/features/ImportManager.js +328 -0
  30. package/dist/features/TagManager.js +210 -0
  31. package/dist/features/index.js +12 -0
  32. package/dist/index.js +13 -997
  33. package/dist/memory.jsonl +225 -0
  34. package/dist/search/BasicSearch.js +161 -0
  35. package/dist/search/BooleanSearch.js +304 -0
  36. package/dist/search/FuzzySearch.js +115 -0
  37. package/dist/search/RankedSearch.js +206 -0
  38. package/dist/search/SavedSearchManager.js +145 -0
  39. package/dist/search/SearchManager.js +305 -0
  40. package/dist/search/SearchSuggestions.js +57 -0
  41. package/dist/search/TFIDFIndexManager.js +217 -0
  42. package/dist/search/index.js +10 -0
  43. package/dist/server/MCPServer.js +889 -0
  44. package/dist/types/analytics.types.js +6 -0
  45. package/dist/types/entity.types.js +7 -0
  46. package/dist/types/import-export.types.js +7 -0
  47. package/dist/types/index.js +12 -0
  48. package/dist/types/search.types.js +7 -0
  49. package/dist/types/tag.types.js +6 -0
  50. package/dist/utils/constants.js +127 -0
  51. package/dist/utils/dateUtils.js +89 -0
  52. package/dist/utils/errors.js +121 -0
  53. package/dist/utils/index.js +13 -0
  54. package/dist/utils/levenshtein.js +62 -0
  55. package/dist/utils/logger.js +33 -0
  56. package/dist/utils/pathUtils.js +115 -0
  57. package/dist/utils/schemas.js +184 -0
  58. package/dist/utils/searchCache.js +209 -0
  59. package/dist/utils/tfidf.js +90 -0
  60. package/dist/utils/validationUtils.js +109 -0
  61. 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
+ });