@danielsimonjr/memory-mcp 0.47.1 → 9.8.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 (207) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +2000 -194
  3. package/dist/__tests__/file-path.test.js +5 -5
  4. package/dist/__tests__/knowledge-graph.test.js +3 -8
  5. package/dist/core/EntityManager.d.ts +266 -0
  6. package/dist/core/EntityManager.d.ts.map +1 -0
  7. package/dist/core/EntityManager.js +85 -133
  8. package/dist/core/GraphEventEmitter.d.ts +202 -0
  9. package/dist/core/GraphEventEmitter.d.ts.map +1 -0
  10. package/dist/core/GraphEventEmitter.js +346 -0
  11. package/dist/core/GraphStorage.d.ts +395 -0
  12. package/dist/core/GraphStorage.d.ts.map +1 -0
  13. package/dist/core/GraphStorage.js +643 -31
  14. package/dist/core/GraphTraversal.d.ts +141 -0
  15. package/dist/core/GraphTraversal.d.ts.map +1 -0
  16. package/dist/core/GraphTraversal.js +573 -0
  17. package/dist/core/HierarchyManager.d.ts +111 -0
  18. package/dist/core/HierarchyManager.d.ts.map +1 -0
  19. package/dist/{features → core}/HierarchyManager.js +14 -9
  20. package/dist/core/ManagerContext.d.ts +72 -0
  21. package/dist/core/ManagerContext.d.ts.map +1 -0
  22. package/dist/core/ManagerContext.js +118 -0
  23. package/dist/core/ObservationManager.d.ts +85 -0
  24. package/dist/core/ObservationManager.d.ts.map +1 -0
  25. package/dist/core/ObservationManager.js +51 -57
  26. package/dist/core/RelationManager.d.ts +131 -0
  27. package/dist/core/RelationManager.d.ts.map +1 -0
  28. package/dist/core/RelationManager.js +31 -7
  29. package/dist/core/SQLiteStorage.d.ts +354 -0
  30. package/dist/core/SQLiteStorage.d.ts.map +1 -0
  31. package/dist/core/SQLiteStorage.js +917 -0
  32. package/dist/core/StorageFactory.d.ts +45 -0
  33. package/dist/core/StorageFactory.d.ts.map +1 -0
  34. package/dist/core/StorageFactory.js +64 -0
  35. package/dist/core/TransactionManager.d.ts +464 -0
  36. package/dist/core/TransactionManager.d.ts.map +1 -0
  37. package/dist/core/TransactionManager.js +490 -13
  38. package/dist/core/index.d.ts +17 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +12 -2
  41. package/dist/features/AnalyticsManager.d.ts +44 -0
  42. package/dist/features/AnalyticsManager.d.ts.map +1 -0
  43. package/dist/features/AnalyticsManager.js +14 -13
  44. package/dist/features/ArchiveManager.d.ts +133 -0
  45. package/dist/features/ArchiveManager.d.ts.map +1 -0
  46. package/dist/features/ArchiveManager.js +221 -14
  47. package/dist/features/CompressionManager.d.ts +117 -0
  48. package/dist/features/CompressionManager.d.ts.map +1 -0
  49. package/dist/features/CompressionManager.js +189 -20
  50. package/dist/features/IOManager.d.ts +225 -0
  51. package/dist/features/IOManager.d.ts.map +1 -0
  52. package/dist/features/IOManager.js +1041 -0
  53. package/dist/features/StreamingExporter.d.ts +123 -0
  54. package/dist/features/StreamingExporter.d.ts.map +1 -0
  55. package/dist/features/StreamingExporter.js +203 -0
  56. package/dist/features/TagManager.d.ts +147 -0
  57. package/dist/features/TagManager.d.ts.map +1 -0
  58. package/dist/features/index.d.ts +12 -0
  59. package/dist/features/index.d.ts.map +1 -0
  60. package/dist/features/index.js +5 -6
  61. package/dist/index.d.ts +9 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +12 -45
  64. package/dist/memory.jsonl +1 -18
  65. package/dist/search/BasicSearch.d.ts +51 -0
  66. package/dist/search/BasicSearch.d.ts.map +1 -0
  67. package/dist/search/BasicSearch.js +9 -3
  68. package/dist/search/BooleanSearch.d.ts +98 -0
  69. package/dist/search/BooleanSearch.d.ts.map +1 -0
  70. package/dist/search/BooleanSearch.js +156 -9
  71. package/dist/search/EmbeddingService.d.ts +178 -0
  72. package/dist/search/EmbeddingService.d.ts.map +1 -0
  73. package/dist/search/EmbeddingService.js +358 -0
  74. package/dist/search/FuzzySearch.d.ts +118 -0
  75. package/dist/search/FuzzySearch.d.ts.map +1 -0
  76. package/dist/search/FuzzySearch.js +241 -25
  77. package/dist/search/QueryCostEstimator.d.ts +111 -0
  78. package/dist/search/QueryCostEstimator.d.ts.map +1 -0
  79. package/dist/search/QueryCostEstimator.js +355 -0
  80. package/dist/search/RankedSearch.d.ts +71 -0
  81. package/dist/search/RankedSearch.d.ts.map +1 -0
  82. package/dist/search/RankedSearch.js +54 -6
  83. package/dist/search/SavedSearchManager.d.ts +79 -0
  84. package/dist/search/SavedSearchManager.d.ts.map +1 -0
  85. package/dist/search/SearchFilterChain.d.ts +120 -0
  86. package/dist/search/SearchFilterChain.d.ts.map +1 -0
  87. package/dist/search/SearchFilterChain.js +2 -4
  88. package/dist/search/SearchManager.d.ts +326 -0
  89. package/dist/search/SearchManager.d.ts.map +1 -0
  90. package/dist/search/SearchManager.js +148 -0
  91. package/dist/search/SearchSuggestions.d.ts +27 -0
  92. package/dist/search/SearchSuggestions.d.ts.map +1 -0
  93. package/dist/search/SearchSuggestions.js +1 -1
  94. package/dist/search/SemanticSearch.d.ts +149 -0
  95. package/dist/search/SemanticSearch.d.ts.map +1 -0
  96. package/dist/search/SemanticSearch.js +323 -0
  97. package/dist/search/TFIDFEventSync.d.ts +85 -0
  98. package/dist/search/TFIDFEventSync.d.ts.map +1 -0
  99. package/dist/search/TFIDFEventSync.js +133 -0
  100. package/dist/search/TFIDFIndexManager.d.ts +151 -0
  101. package/dist/search/TFIDFIndexManager.d.ts.map +1 -0
  102. package/dist/search/TFIDFIndexManager.js +232 -17
  103. package/dist/search/VectorStore.d.ts +235 -0
  104. package/dist/search/VectorStore.d.ts.map +1 -0
  105. package/dist/search/VectorStore.js +311 -0
  106. package/dist/search/index.d.ts +21 -0
  107. package/dist/search/index.d.ts.map +1 -0
  108. package/dist/search/index.js +12 -0
  109. package/dist/server/MCPServer.d.ts +21 -0
  110. package/dist/server/MCPServer.d.ts.map +1 -0
  111. package/dist/server/MCPServer.js +4 -4
  112. package/dist/server/responseCompressor.d.ts +94 -0
  113. package/dist/server/responseCompressor.d.ts.map +1 -0
  114. package/dist/server/responseCompressor.js +127 -0
  115. package/dist/server/toolDefinitions.d.ts +27 -0
  116. package/dist/server/toolDefinitions.d.ts.map +1 -0
  117. package/dist/server/toolDefinitions.js +189 -18
  118. package/dist/server/toolHandlers.d.ts +41 -0
  119. package/dist/server/toolHandlers.d.ts.map +1 -0
  120. package/dist/server/toolHandlers.js +467 -75
  121. package/dist/types/index.d.ts +13 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +1 -1
  124. package/dist/types/types.d.ts +1654 -0
  125. package/dist/types/types.d.ts.map +1 -0
  126. package/dist/types/types.js +9 -0
  127. package/dist/utils/compressedCache.d.ts +192 -0
  128. package/dist/utils/compressedCache.d.ts.map +1 -0
  129. package/dist/utils/compressedCache.js +309 -0
  130. package/dist/utils/compressionUtil.d.ts +214 -0
  131. package/dist/utils/compressionUtil.d.ts.map +1 -0
  132. package/dist/utils/compressionUtil.js +247 -0
  133. package/dist/utils/constants.d.ts +245 -0
  134. package/dist/utils/constants.d.ts.map +1 -0
  135. package/dist/utils/constants.js +124 -0
  136. package/dist/utils/entityUtils.d.ts +321 -0
  137. package/dist/utils/entityUtils.d.ts.map +1 -0
  138. package/dist/utils/entityUtils.js +434 -4
  139. package/dist/utils/errors.d.ts +95 -0
  140. package/dist/utils/errors.d.ts.map +1 -0
  141. package/dist/utils/errors.js +24 -0
  142. package/dist/utils/formatters.d.ts +145 -0
  143. package/dist/utils/formatters.d.ts.map +1 -0
  144. package/dist/utils/{paginationUtils.js → formatters.js} +54 -3
  145. package/dist/utils/index.d.ts +23 -0
  146. package/dist/utils/index.d.ts.map +1 -0
  147. package/dist/utils/index.js +69 -31
  148. package/dist/utils/indexes.d.ts +270 -0
  149. package/dist/utils/indexes.d.ts.map +1 -0
  150. package/dist/utils/indexes.js +526 -0
  151. package/dist/utils/logger.d.ts +24 -0
  152. package/dist/utils/logger.d.ts.map +1 -0
  153. package/dist/utils/operationUtils.d.ts +124 -0
  154. package/dist/utils/operationUtils.d.ts.map +1 -0
  155. package/dist/utils/operationUtils.js +175 -0
  156. package/dist/utils/parallelUtils.d.ts +72 -0
  157. package/dist/utils/parallelUtils.d.ts.map +1 -0
  158. package/dist/utils/parallelUtils.js +169 -0
  159. package/dist/utils/schemas.d.ts +374 -0
  160. package/dist/utils/schemas.d.ts.map +1 -0
  161. package/dist/utils/schemas.js +302 -2
  162. package/dist/utils/searchAlgorithms.d.ts +99 -0
  163. package/dist/utils/searchAlgorithms.d.ts.map +1 -0
  164. package/dist/utils/searchAlgorithms.js +167 -0
  165. package/dist/utils/searchCache.d.ts +108 -0
  166. package/dist/utils/searchCache.d.ts.map +1 -0
  167. package/dist/utils/taskScheduler.d.ts +290 -0
  168. package/dist/utils/taskScheduler.d.ts.map +1 -0
  169. package/dist/utils/taskScheduler.js +466 -0
  170. package/dist/workers/index.d.ts +12 -0
  171. package/dist/workers/index.d.ts.map +1 -0
  172. package/dist/workers/index.js +9 -0
  173. package/dist/workers/levenshteinWorker.d.ts +60 -0
  174. package/dist/workers/levenshteinWorker.d.ts.map +1 -0
  175. package/dist/workers/levenshteinWorker.js +98 -0
  176. package/package.json +17 -4
  177. package/dist/__tests__/edge-cases/edge-cases.test.js +0 -406
  178. package/dist/__tests__/integration/workflows.test.js +0 -449
  179. package/dist/__tests__/performance/benchmarks.test.js +0 -413
  180. package/dist/__tests__/unit/core/EntityManager.test.js +0 -334
  181. package/dist/__tests__/unit/core/GraphStorage.test.js +0 -205
  182. package/dist/__tests__/unit/core/RelationManager.test.js +0 -274
  183. package/dist/__tests__/unit/features/CompressionManager.test.js +0 -350
  184. package/dist/__tests__/unit/search/BasicSearch.test.js +0 -311
  185. package/dist/__tests__/unit/search/BooleanSearch.test.js +0 -432
  186. package/dist/__tests__/unit/search/FuzzySearch.test.js +0 -448
  187. package/dist/__tests__/unit/search/RankedSearch.test.js +0 -379
  188. package/dist/__tests__/unit/utils/levenshtein.test.js +0 -77
  189. package/dist/core/KnowledgeGraphManager.js +0 -423
  190. package/dist/features/BackupManager.js +0 -311
  191. package/dist/features/ExportManager.js +0 -305
  192. package/dist/features/ImportExportManager.js +0 -50
  193. package/dist/features/ImportManager.js +0 -328
  194. package/dist/types/analytics.types.js +0 -6
  195. package/dist/types/entity.types.js +0 -7
  196. package/dist/types/import-export.types.js +0 -7
  197. package/dist/types/search.types.js +0 -7
  198. package/dist/types/tag.types.js +0 -6
  199. package/dist/utils/dateUtils.js +0 -89
  200. package/dist/utils/filterUtils.js +0 -155
  201. package/dist/utils/levenshtein.js +0 -62
  202. package/dist/utils/pathUtils.js +0 -115
  203. package/dist/utils/responseFormatter.js +0 -55
  204. package/dist/utils/tagUtils.js +0 -107
  205. package/dist/utils/tfidf.js +0 -90
  206. package/dist/utils/validationHelper.js +0 -99
  207. package/dist/utils/validationUtils.js +0 -109
@@ -1,274 +0,0 @@
1
- /**
2
- * RelationManager Unit Tests
3
- */
4
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
- import { RelationManager } from '../../../core/RelationManager.js';
6
- import { EntityManager } from '../../../core/EntityManager.js';
7
- import { GraphStorage } from '../../../core/GraphStorage.js';
8
- import { ValidationError } from '../../../utils/errors.js';
9
- import { promises as fs } from 'fs';
10
- import { join } from 'path';
11
- import { tmpdir } from 'os';
12
- describe('RelationManager', () => {
13
- let storage;
14
- let relationManager;
15
- let entityManager;
16
- let testDir;
17
- let testFilePath;
18
- beforeEach(async () => {
19
- // Create unique temp directory for each test
20
- testDir = join(tmpdir(), `relation-manager-test-${Date.now()}-${Math.random()}`);
21
- await fs.mkdir(testDir, { recursive: true });
22
- testFilePath = join(testDir, 'test-graph.jsonl');
23
- storage = new GraphStorage(testFilePath);
24
- relationManager = new RelationManager(storage);
25
- entityManager = new EntityManager(storage);
26
- // Create test entities for relation tests
27
- await entityManager.createEntities([
28
- { name: 'Alice', entityType: 'person', observations: ['Software engineer'] },
29
- { name: 'Bob', entityType: 'person', observations: ['Product manager'] },
30
- { name: 'Charlie', entityType: 'person', observations: ['Designer'] },
31
- { name: 'Project_X', entityType: 'project', observations: ['Internal tool'] },
32
- { name: 'Company', entityType: 'organization', observations: ['Tech startup'] },
33
- ]);
34
- });
35
- afterEach(async () => {
36
- // Clean up test files
37
- try {
38
- await fs.rm(testDir, { recursive: true, force: true });
39
- }
40
- catch {
41
- // Ignore cleanup errors
42
- }
43
- });
44
- describe('createRelations', () => {
45
- it('should create a single relation with timestamps', async () => {
46
- const relations = await relationManager.createRelations([
47
- {
48
- from: 'Alice',
49
- to: 'Bob',
50
- relationType: 'works_with',
51
- },
52
- ]);
53
- expect(relations).toHaveLength(1);
54
- expect(relations[0].from).toBe('Alice');
55
- expect(relations[0].to).toBe('Bob');
56
- expect(relations[0].relationType).toBe('works_with');
57
- expect(relations[0].createdAt).toBeDefined();
58
- expect(relations[0].lastModified).toBeDefined();
59
- });
60
- it('should create multiple relations in batch', async () => {
61
- const relations = await relationManager.createRelations([
62
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
63
- { from: 'Alice', to: 'Project_X', relationType: 'contributes_to' },
64
- { from: 'Bob', to: 'Project_X', relationType: 'leads' },
65
- ]);
66
- expect(relations).toHaveLength(3);
67
- expect(relations.map(r => r.relationType)).toEqual([
68
- 'works_with',
69
- 'contributes_to',
70
- 'leads',
71
- ]);
72
- });
73
- it('should filter out duplicate relations', async () => {
74
- await relationManager.createRelations([
75
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
76
- ]);
77
- const result = await relationManager.createRelations([
78
- { from: 'Alice', to: 'Bob', relationType: 'works_with' }, // Duplicate
79
- { from: 'Bob', to: 'Charlie', relationType: 'works_with' }, // New
80
- ]);
81
- expect(result).toHaveLength(1);
82
- expect(result[0].from).toBe('Bob');
83
- expect(result[0].to).toBe('Charlie');
84
- });
85
- it('should allow same entities with different relation types', async () => {
86
- const relations = await relationManager.createRelations([
87
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
88
- { from: 'Alice', to: 'Bob', relationType: 'mentors' },
89
- { from: 'Alice', to: 'Bob', relationType: 'friends_with' },
90
- ]);
91
- expect(relations).toHaveLength(3);
92
- });
93
- it('should throw ValidationError for invalid relation data', async () => {
94
- await expect(relationManager.createRelations([
95
- {
96
- from: '', // Invalid: empty string
97
- to: 'Bob',
98
- relationType: 'works_with',
99
- },
100
- ])).rejects.toThrow(ValidationError);
101
- });
102
- it('should preserve custom timestamps if provided', async () => {
103
- const customTimestamp = '2024-01-01T00:00:00.000Z';
104
- const relations = await relationManager.createRelations([
105
- {
106
- from: 'Alice',
107
- to: 'Bob',
108
- relationType: 'works_with',
109
- createdAt: customTimestamp,
110
- lastModified: customTimestamp,
111
- },
112
- ]);
113
- expect(relations[0].createdAt).toBe(customTimestamp);
114
- expect(relations[0].lastModified).toBe(customTimestamp);
115
- });
116
- it('should handle relations between same entity type', async () => {
117
- const relations = await relationManager.createRelations([
118
- { from: 'Alice', to: 'Bob', relationType: 'reports_to' },
119
- { from: 'Bob', to: 'Charlie', relationType: 'reports_to' },
120
- ]);
121
- expect(relations).toHaveLength(2);
122
- });
123
- it('should handle relations across different entity types', async () => {
124
- const relations = await relationManager.createRelations([
125
- { from: 'Alice', to: 'Project_X', relationType: 'contributes_to' },
126
- { from: 'Project_X', to: 'Company', relationType: 'owned_by' },
127
- ]);
128
- expect(relations).toHaveLength(2);
129
- });
130
- });
131
- describe('deleteRelations', () => {
132
- beforeEach(async () => {
133
- // Create some test relations
134
- await relationManager.createRelations([
135
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
136
- { from: 'Alice', to: 'Project_X', relationType: 'contributes_to' },
137
- { from: 'Bob', to: 'Project_X', relationType: 'leads' },
138
- { from: 'Charlie', to: 'Alice', relationType: 'reports_to' },
139
- ]);
140
- });
141
- it('should delete a single relation', async () => {
142
- await relationManager.deleteRelations([
143
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
144
- ]);
145
- const aliceRelations = await relationManager.getRelations('Alice');
146
- expect(aliceRelations.some(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'works_with')).toBe(false);
147
- });
148
- it('should delete multiple relations in batch', async () => {
149
- await relationManager.deleteRelations([
150
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
151
- { from: 'Alice', to: 'Project_X', relationType: 'contributes_to' },
152
- ]);
153
- const aliceRelations = await relationManager.getRelations('Alice');
154
- expect(aliceRelations).toHaveLength(1); // Only 'reports_to' from Charlie remains
155
- expect(aliceRelations[0].from).toBe('Charlie');
156
- });
157
- it('should update lastModified for affected entities', async () => {
158
- const beforeDelete = await entityManager.getEntity('Alice');
159
- const originalTimestamp = beforeDelete.lastModified;
160
- // Wait a bit to ensure timestamp difference
161
- await new Promise(resolve => setTimeout(resolve, 10));
162
- await relationManager.deleteRelations([
163
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
164
- ]);
165
- const afterDelete = await entityManager.getEntity('Alice');
166
- expect(afterDelete.lastModified).not.toBe(originalTimestamp);
167
- const bobAfter = await entityManager.getEntity('Bob');
168
- expect(bobAfter.lastModified).not.toBe(originalTimestamp);
169
- });
170
- it('should silently ignore non-existent relations', async () => {
171
- await expect(relationManager.deleteRelations([
172
- { from: 'NonExistent', to: 'AlsoNonExistent', relationType: 'fake' },
173
- ])).resolves.not.toThrow();
174
- });
175
- it('should handle partial deletion (some exist, some don\'t)', async () => {
176
- await relationManager.deleteRelations([
177
- { from: 'Alice', to: 'Bob', relationType: 'works_with' }, // Exists
178
- { from: 'Alice', to: 'Bob', relationType: 'fake_relation' }, // Doesn't exist
179
- ]);
180
- const aliceRelations = await relationManager.getRelations('Alice');
181
- expect(aliceRelations.some(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'works_with')).toBe(false);
182
- });
183
- it('should throw ValidationError for invalid relation data', async () => {
184
- await expect(relationManager.deleteRelations([
185
- {
186
- from: '', // Invalid
187
- to: 'Bob',
188
- relationType: 'works_with',
189
- },
190
- ])).rejects.toThrow(ValidationError);
191
- });
192
- });
193
- describe('getRelations', () => {
194
- beforeEach(async () => {
195
- // Create test relations
196
- await relationManager.createRelations([
197
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
198
- { from: 'Alice', to: 'Project_X', relationType: 'contributes_to' },
199
- { from: 'Bob', to: 'Project_X', relationType: 'leads' },
200
- { from: 'Charlie', to: 'Alice', relationType: 'reports_to' },
201
- ]);
202
- });
203
- it('should get all relations for an entity (incoming and outgoing)', async () => {
204
- const aliceRelations = await relationManager.getRelations('Alice');
205
- expect(aliceRelations).toHaveLength(3);
206
- });
207
- it('should return outgoing relations', async () => {
208
- const relations = await relationManager.getRelations('Alice');
209
- const outgoing = relations.filter(r => r.from === 'Alice');
210
- expect(outgoing).toHaveLength(2);
211
- expect(outgoing.map(r => r.to)).toEqual(expect.arrayContaining(['Bob', 'Project_X']));
212
- });
213
- it('should return incoming relations', async () => {
214
- const relations = await relationManager.getRelations('Alice');
215
- const incoming = relations.filter(r => r.to === 'Alice');
216
- expect(incoming).toHaveLength(1);
217
- expect(incoming[0].from).toBe('Charlie');
218
- });
219
- it('should return empty array for entity with no relations', async () => {
220
- const relations = await relationManager.getRelations('Company');
221
- expect(relations).toEqual([]);
222
- });
223
- it('should be case-sensitive for entity names', async () => {
224
- const relations = await relationManager.getRelations('alice'); // lowercase
225
- expect(relations).toEqual([]);
226
- });
227
- it('should handle entity that doesn\'t exist', async () => {
228
- const relations = await relationManager.getRelations('NonExistentEntity');
229
- expect(relations).toEqual([]);
230
- });
231
- it('should return all relation types for an entity', async () => {
232
- await relationManager.createRelations([
233
- { from: 'Bob', to: 'Alice', relationType: 'mentors' },
234
- { from: 'Bob', to: 'Alice', relationType: 'friends_with' },
235
- ]);
236
- const relations = await relationManager.getRelations('Alice');
237
- const typesWithBob = relations
238
- .filter(r => (r.from === 'Bob' && r.to === 'Alice') || (r.from === 'Alice' && r.to === 'Bob'))
239
- .map(r => r.relationType);
240
- expect(typesWithBob).toContain('works_with');
241
- expect(typesWithBob).toContain('mentors');
242
- expect(typesWithBob).toContain('friends_with');
243
- });
244
- });
245
- describe('graph integrity', () => {
246
- it('should maintain referential integrity after relation operations', async () => {
247
- await relationManager.createRelations([
248
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
249
- ]);
250
- await relationManager.deleteRelations([
251
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
252
- ]);
253
- const graph = await storage.loadGraph();
254
- expect(graph.entities).toHaveLength(5); // All entities still exist
255
- expect(graph.relations).toHaveLength(0); // Relation deleted
256
- });
257
- it('should allow circular relations', async () => {
258
- const relations = await relationManager.createRelations([
259
- { from: 'Alice', to: 'Bob', relationType: 'helps' },
260
- { from: 'Bob', to: 'Charlie', relationType: 'helps' },
261
- { from: 'Charlie', to: 'Alice', relationType: 'helps' },
262
- ]);
263
- expect(relations).toHaveLength(3);
264
- });
265
- it('should allow self-referential relations', async () => {
266
- const relations = await relationManager.createRelations([
267
- { from: 'Alice', to: 'Alice', relationType: 'self_reference' },
268
- ]);
269
- expect(relations).toHaveLength(1);
270
- expect(relations[0].from).toBe('Alice');
271
- expect(relations[0].to).toBe('Alice');
272
- });
273
- });
274
- });
@@ -1,350 +0,0 @@
1
- /**
2
- * CompressionManager Unit Tests
3
- */
4
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
- import { CompressionManager } from '../../../features/CompressionManager.js';
6
- import { EntityManager } from '../../../core/EntityManager.js';
7
- import { RelationManager } from '../../../core/RelationManager.js';
8
- import { GraphStorage } from '../../../core/GraphStorage.js';
9
- import { EntityNotFoundError, InsufficientEntitiesError } from '../../../utils/errors.js';
10
- import { promises as fs } from 'fs';
11
- import { join } from 'path';
12
- import { tmpdir } from 'os';
13
- describe('CompressionManager', () => {
14
- let storage;
15
- let compressionManager;
16
- let entityManager;
17
- let relationManager;
18
- let testDir;
19
- let testFilePath;
20
- beforeEach(async () => {
21
- // Create unique temp directory for each test
22
- testDir = join(tmpdir(), `compression-manager-test-${Date.now()}-${Math.random()}`);
23
- await fs.mkdir(testDir, { recursive: true });
24
- testFilePath = join(testDir, 'test-graph.jsonl');
25
- storage = new GraphStorage(testFilePath);
26
- compressionManager = new CompressionManager(storage);
27
- entityManager = new EntityManager(storage);
28
- relationManager = new RelationManager(storage);
29
- });
30
- afterEach(async () => {
31
- // Clean up test files
32
- try {
33
- await fs.rm(testDir, { recursive: true, force: true });
34
- }
35
- catch {
36
- // Ignore cleanup errors
37
- }
38
- });
39
- describe('findDuplicates', () => {
40
- it('should find duplicate entities with high similarity', async () => {
41
- await entityManager.createEntities([
42
- { name: 'Alice Smith', entityType: 'person', observations: ['Engineer', 'Loves coding'] },
43
- { name: 'Alice Smyth', entityType: 'person', observations: ['Engineer', 'Loves coding'] },
44
- { name: 'Bob Jones', entityType: 'person', observations: ['Manager'] },
45
- ]);
46
- const duplicates = await compressionManager.findDuplicates(0.8);
47
- expect(duplicates).toHaveLength(1);
48
- expect(duplicates[0]).toContain('Alice Smith');
49
- expect(duplicates[0]).toContain('Alice Smyth');
50
- });
51
- it('should not find duplicates when similarity is below threshold', async () => {
52
- await entityManager.createEntities([
53
- { name: 'Alice', entityType: 'person', observations: ['Engineer'] },
54
- { name: 'Bob', entityType: 'person', observations: ['Manager'] },
55
- ]);
56
- const duplicates = await compressionManager.findDuplicates(0.9);
57
- expect(duplicates).toHaveLength(0);
58
- });
59
- it('should only compare entities of the same type', async () => {
60
- await entityManager.createEntities([
61
- { name: 'ProjectX', entityType: 'project', observations: ['Software'] },
62
- { name: 'ProjectX', entityType: 'company', observations: ['Software'] },
63
- ]);
64
- const duplicates = await compressionManager.findDuplicates(0.7);
65
- expect(duplicates).toHaveLength(0); // Different types, not duplicates
66
- });
67
- it('should handle empty graph', async () => {
68
- const duplicates = await compressionManager.findDuplicates();
69
- expect(duplicates).toEqual([]);
70
- });
71
- it('should handle graph with single entity', async () => {
72
- await entityManager.createEntities([
73
- { name: 'Alice', entityType: 'person', observations: [] },
74
- ]);
75
- const duplicates = await compressionManager.findDuplicates();
76
- expect(duplicates).toEqual([]);
77
- });
78
- it('should detect duplicates with similar names but different cases', async () => {
79
- await entityManager.createEntities([
80
- { name: 'alice smith', entityType: 'person', observations: ['Engineer'] },
81
- { name: 'ALICE SMITH', entityType: 'person', observations: ['Engineer'] },
82
- ]);
83
- const duplicates = await compressionManager.findDuplicates(0.9);
84
- expect(duplicates).toHaveLength(1);
85
- expect(duplicates[0]).toHaveLength(2);
86
- });
87
- it('should detect duplicates with overlapping observations', async () => {
88
- await entityManager.createEntities([
89
- { name: 'Alice', entityType: 'person', observations: ['Software engineer', 'Loves Python', 'Works remotely'] },
90
- { name: 'Alicia', entityType: 'person', observations: ['Software engineer', 'Loves Python', 'Works remotely'] },
91
- ]);
92
- const duplicates = await compressionManager.findDuplicates(0.7);
93
- expect(duplicates).toHaveLength(1);
94
- });
95
- it('should detect duplicates with matching tags', async () => {
96
- await entityManager.createEntities([
97
- {
98
- name: 'Alice Johnson',
99
- entityType: 'person',
100
- observations: ['Engineer'],
101
- tags: ['engineering', 'python', 'remote']
102
- },
103
- {
104
- name: 'Alice Jonson',
105
- entityType: 'person',
106
- observations: ['Engineer'],
107
- tags: ['engineering', 'python', 'remote']
108
- },
109
- ]);
110
- const duplicates = await compressionManager.findDuplicates(0.85);
111
- expect(duplicates).toHaveLength(1);
112
- });
113
- it('should handle multiple duplicate groups', async () => {
114
- await entityManager.createEntities([
115
- { name: 'Alice Smith', entityType: 'person', observations: ['A'] },
116
- { name: 'Alice Smyth', entityType: 'person', observations: ['A'] },
117
- { name: 'Bob Jones', entityType: 'person', observations: ['B'] },
118
- { name: 'Bob Johnes', entityType: 'person', observations: ['B'] },
119
- { name: 'Charlie', entityType: 'person', observations: ['C'] },
120
- ]);
121
- const duplicates = await compressionManager.findDuplicates(0.8);
122
- expect(duplicates).toHaveLength(2); // Two separate duplicate groups
123
- });
124
- it('should use efficient bucketing to reduce comparisons', async () => {
125
- // Create many entities with different prefixes (shouldn't compare across buckets)
126
- await entityManager.createEntities([
127
- { name: 'Alice', entityType: 'person', observations: [] },
128
- { name: 'Alicia', entityType: 'person', observations: [] },
129
- { name: 'Bob', entityType: 'person', observations: [] },
130
- { name: 'Bobby', entityType: 'person', observations: [] },
131
- { name: 'Charlie', entityType: 'person', observations: [] },
132
- { name: 'Charles', entityType: 'person', observations: [] },
133
- ]);
134
- const duplicates = await compressionManager.findDuplicates(0.7);
135
- // Each prefix group might have duplicates, but they don't cross-compare
136
- expect(duplicates.length).toBeGreaterThanOrEqual(0);
137
- });
138
- });
139
- describe('mergeEntities', () => {
140
- beforeEach(async () => {
141
- // Create test entities for merging
142
- await entityManager.createEntities([
143
- {
144
- name: 'Alice',
145
- entityType: 'person',
146
- observations: ['Engineer', 'Loves coding'],
147
- tags: ['tech', 'python'],
148
- importance: 8,
149
- createdAt: '2024-01-01T00:00:00.000Z'
150
- },
151
- {
152
- name: 'Alicia',
153
- entityType: 'person',
154
- observations: ['Engineer', 'Loves music'],
155
- tags: ['tech', 'music'],
156
- importance: 9,
157
- createdAt: '2024-01-02T00:00:00.000Z'
158
- },
159
- {
160
- name: 'Bob',
161
- entityType: 'person',
162
- observations: ['Manager']
163
- },
164
- ]);
165
- });
166
- it('should merge two entities and combine observations', async () => {
167
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia']);
168
- expect(merged.name).toBe('Alice');
169
- expect(merged.observations).toContain('Engineer');
170
- expect(merged.observations).toContain('Loves coding');
171
- expect(merged.observations).toContain('Loves music');
172
- expect(merged.observations).toHaveLength(3);
173
- });
174
- it('should merge tags from all entities', async () => {
175
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia']);
176
- expect(merged.tags).toContain('tech');
177
- expect(merged.tags).toContain('python');
178
- expect(merged.tags).toContain('music');
179
- expect(merged.tags).toHaveLength(3);
180
- });
181
- it('should use highest importance value', async () => {
182
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia']);
183
- expect(merged.importance).toBe(9); // Alicia has 9, Alice has 8
184
- });
185
- it('should use earliest createdAt timestamp', async () => {
186
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia']);
187
- expect(merged.createdAt).toBe('2024-01-01T00:00:00.000Z');
188
- });
189
- it('should update lastModified timestamp', async () => {
190
- const beforeMerge = new Date().toISOString();
191
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia']);
192
- expect(merged.lastModified).toBeDefined();
193
- expect(merged.lastModified >= beforeMerge).toBe(true);
194
- });
195
- it('should remove merged entities from graph', async () => {
196
- await compressionManager.mergeEntities(['Alice', 'Alicia']);
197
- const alice = await entityManager.getEntity('Alice');
198
- const alicia = await entityManager.getEntity('Alicia');
199
- expect(alice).not.toBeNull();
200
- expect(alicia).toBeNull(); // Alicia was merged into Alice
201
- });
202
- it('should throw error when merging less than 2 entities', async () => {
203
- await expect(compressionManager.mergeEntities(['Alice'])).rejects.toThrow(InsufficientEntitiesError);
204
- });
205
- it('should throw error when entity not found', async () => {
206
- await expect(compressionManager.mergeEntities(['Alice', 'NonExistent'])).rejects.toThrow(EntityNotFoundError);
207
- });
208
- it('should rename merged entity if targetName provided', async () => {
209
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia'], 'Alice Smith');
210
- expect(merged.name).toBe('Alice Smith');
211
- const alice = await entityManager.getEntity('Alice');
212
- const aliceSmith = await entityManager.getEntity('Alice Smith');
213
- expect(alice).toBeNull();
214
- expect(aliceSmith).not.toBeNull();
215
- });
216
- it('should redirect relations to merged entity', async () => {
217
- await relationManager.createRelations([
218
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
219
- { from: 'Alicia', to: 'Bob', relationType: 'reports_to' },
220
- ]);
221
- await compressionManager.mergeEntities(['Alice', 'Alicia']);
222
- const aliceRelations = await relationManager.getRelations('Alice');
223
- expect(aliceRelations).toHaveLength(2);
224
- expect(aliceRelations.some(r => r.relationType === 'works_with')).toBe(true);
225
- expect(aliceRelations.some(r => r.relationType === 'reports_to')).toBe(true);
226
- });
227
- it('should remove duplicate relations after merge', async () => {
228
- await relationManager.createRelations([
229
- { from: 'Alice', to: 'Bob', relationType: 'works_with' },
230
- { from: 'Alicia', to: 'Bob', relationType: 'works_with' }, // Duplicate after merge
231
- ]);
232
- await compressionManager.mergeEntities(['Alice', 'Alicia']);
233
- const relations = await relationManager.getRelations('Alice');
234
- const worksWithRelations = relations.filter(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'works_with');
235
- expect(worksWithRelations).toHaveLength(1); // Only one, duplicate removed
236
- });
237
- it('should handle merging entities with no tags', async () => {
238
- const merged = await compressionManager.mergeEntities(['Bob', 'Alice']);
239
- // Bob has no tags, Alice has tags
240
- expect(merged.tags).toContain('tech');
241
- expect(merged.tags).toContain('python');
242
- });
243
- it('should merge multiple entities (more than 2)', async () => {
244
- await entityManager.createEntities([
245
- { name: 'Alice2', entityType: 'person', observations: ['Observation 1'] },
246
- ]);
247
- const merged = await compressionManager.mergeEntities(['Alice', 'Alicia', 'Alice2']);
248
- const alice = await entityManager.getEntity('Alice');
249
- const alicia = await entityManager.getEntity('Alicia');
250
- const alice2 = await entityManager.getEntity('Alice2');
251
- expect(alice).not.toBeNull();
252
- expect(alicia).toBeNull();
253
- expect(alice2).toBeNull();
254
- expect(merged.observations).toContain('Observation 1');
255
- });
256
- });
257
- describe('compressGraph', () => {
258
- beforeEach(async () => {
259
- // Create entities with duplicates
260
- await entityManager.createEntities([
261
- { name: 'Alice Smith', entityType: 'person', observations: ['Engineer'] },
262
- { name: 'Alice Smyth', entityType: 'person', observations: ['Engineer'] },
263
- { name: 'Bob Jones', entityType: 'person', observations: ['Manager'] },
264
- { name: 'Bob Johnes', entityType: 'person', observations: ['Manager'] },
265
- { name: 'Charlie', entityType: 'person', observations: ['Designer'] },
266
- ]);
267
- });
268
- it('should compress graph and return statistics', async () => {
269
- const result = await compressionManager.compressGraph(0.8);
270
- expect(result.duplicatesFound).toBe(4); // 4 total duplicates in 2 groups
271
- expect(result.entitiesMerged).toBe(2); // 2 entities merged into others
272
- expect(result.spaceFreed).toBeGreaterThan(0);
273
- expect(result.mergedEntities).toHaveLength(2);
274
- });
275
- it('should perform dry run without modifying graph', async () => {
276
- const beforeGraph = await storage.loadGraph();
277
- const beforeEntityCount = beforeGraph.entities.length;
278
- const result = await compressionManager.compressGraph(0.8, true);
279
- const afterGraph = await storage.loadGraph();
280
- expect(afterGraph.entities).toHaveLength(beforeEntityCount);
281
- expect(result.duplicatesFound).toBeGreaterThan(0);
282
- expect(result.entitiesMerged).toBeGreaterThan(0);
283
- expect(result.mergedEntities.length).toBeGreaterThan(0);
284
- });
285
- it('should calculate space freed correctly', async () => {
286
- const result = await compressionManager.compressGraph(0.8);
287
- expect(result.spaceFreed).toBeGreaterThan(0);
288
- expect(result.duplicatesFound).toBeGreaterThan(0);
289
- expect(result.entitiesMerged).toBeGreaterThan(0);
290
- });
291
- it('should handle graph with no duplicates', async () => {
292
- // Remove all entities and add distinct ones
293
- const graph = await storage.loadGraph();
294
- graph.entities = [];
295
- graph.relations = [];
296
- await storage.saveGraph(graph);
297
- await entityManager.createEntities([
298
- { name: 'Alice', entityType: 'person', observations: ['Engineer'] },
299
- { name: 'Bob', entityType: 'person', observations: ['Manager'] },
300
- { name: 'Charlie', entityType: 'person', observations: ['Designer'] },
301
- ]);
302
- const result = await compressionManager.compressGraph(0.9);
303
- expect(result.duplicatesFound).toBe(0);
304
- expect(result.entitiesMerged).toBe(0);
305
- expect(result.spaceFreed).toBe(0); // No compression
306
- });
307
- it('should work with different thresholds', async () => {
308
- const resultHigh = await compressionManager.compressGraph(0.95, true);
309
- const resultLow = await compressionManager.compressGraph(0.6, true);
310
- // Lower threshold should find more duplicates
311
- expect(resultLow.duplicatesFound).toBeGreaterThanOrEqual(resultHigh.duplicatesFound);
312
- });
313
- });
314
- describe('edge cases', () => {
315
- it('should handle entities with empty observations', async () => {
316
- await entityManager.createEntities([
317
- { name: 'Alice', entityType: 'person', observations: [] },
318
- { name: 'Alicia', entityType: 'person', observations: [] },
319
- ]);
320
- const duplicates = await compressionManager.findDuplicates(0.7);
321
- expect(duplicates.length).toBeGreaterThanOrEqual(0);
322
- });
323
- it('should handle entities with very long names', async () => {
324
- const longName1 = 'A'.repeat(200);
325
- const longName2 = 'A'.repeat(195) + 'B'.repeat(5);
326
- await entityManager.createEntities([
327
- { name: longName1, entityType: 'person', observations: ['Test'] },
328
- { name: longName2, entityType: 'person', observations: ['Test'] },
329
- ]);
330
- const duplicates = await compressionManager.findDuplicates(0.8);
331
- expect(duplicates.length).toBeGreaterThanOrEqual(0);
332
- });
333
- it('should handle entities with special characters', async () => {
334
- await entityManager.createEntities([
335
- { name: 'Alice-Smith', entityType: 'person', observations: ['Engineer'] },
336
- { name: 'Alice_Smith', entityType: 'person', observations: ['Engineer'] },
337
- ]);
338
- const duplicates = await compressionManager.findDuplicates(0.7);
339
- expect(duplicates.length).toBeGreaterThanOrEqual(0);
340
- });
341
- it('should handle entities with unicode characters', async () => {
342
- await entityManager.createEntities([
343
- { name: 'Café', entityType: 'location', observations: ['Coffee shop'] },
344
- { name: 'Cafe', entityType: 'location', observations: ['Coffee shop'] },
345
- ]);
346
- const duplicates = await compressionManager.findDuplicates(0.8);
347
- expect(duplicates.length).toBeGreaterThanOrEqual(0);
348
- });
349
- });
350
- });