@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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BasicSearch Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { BasicSearch } from '../../../search/BasicSearch.js';
|
|
6
|
+
import { EntityManager } from '../../../core/EntityManager.js';
|
|
7
|
+
import { RelationManager } from '../../../core/RelationManager.js';
|
|
8
|
+
import { GraphStorage } from '../../../core/GraphStorage.js';
|
|
9
|
+
import { promises as fs } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { tmpdir } from 'os';
|
|
12
|
+
describe('BasicSearch', () => {
|
|
13
|
+
let storage;
|
|
14
|
+
let basicSearch;
|
|
15
|
+
let entityManager;
|
|
16
|
+
let relationManager;
|
|
17
|
+
let testDir;
|
|
18
|
+
let testFilePath;
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
// Create unique temp directory for each test
|
|
21
|
+
testDir = join(tmpdir(), `basic-search-test-${Date.now()}-${Math.random()}`);
|
|
22
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
23
|
+
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
24
|
+
storage = new GraphStorage(testFilePath);
|
|
25
|
+
basicSearch = new BasicSearch(storage);
|
|
26
|
+
entityManager = new EntityManager(storage);
|
|
27
|
+
relationManager = new RelationManager(storage);
|
|
28
|
+
// Create test data
|
|
29
|
+
await entityManager.createEntities([
|
|
30
|
+
{
|
|
31
|
+
name: 'Alice',
|
|
32
|
+
entityType: 'person',
|
|
33
|
+
observations: ['Software engineer', 'Loves Python', 'Works on AI projects'],
|
|
34
|
+
tags: ['engineering', 'python', 'ai'],
|
|
35
|
+
importance: 9,
|
|
36
|
+
createdAt: '2024-01-15T10:00:00.000Z',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Bob',
|
|
40
|
+
entityType: 'person',
|
|
41
|
+
observations: ['Product manager', 'Leads roadmap planning'],
|
|
42
|
+
tags: ['product', 'management'],
|
|
43
|
+
importance: 8,
|
|
44
|
+
createdAt: '2024-02-20T10:00:00.000Z',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Charlie',
|
|
48
|
+
entityType: 'person',
|
|
49
|
+
observations: ['Designer', 'Creates beautiful UIs'],
|
|
50
|
+
tags: ['design', 'ui'],
|
|
51
|
+
importance: 7,
|
|
52
|
+
createdAt: '2024-03-10T10:00:00.000Z',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Project_X',
|
|
56
|
+
entityType: 'project',
|
|
57
|
+
observations: ['Internal tool for automation', 'Built with Python'],
|
|
58
|
+
tags: ['engineering', 'automation', 'python'],
|
|
59
|
+
importance: 10,
|
|
60
|
+
createdAt: '2024-01-01T10:00:00.000Z',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Company',
|
|
64
|
+
entityType: 'organization',
|
|
65
|
+
observations: ['Tech startup', 'AI-focused company'],
|
|
66
|
+
tags: ['business', 'ai'],
|
|
67
|
+
importance: 10,
|
|
68
|
+
createdAt: '2024-01-01T10:00:00.000Z',
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
await relationManager.createRelations([
|
|
72
|
+
{ from: 'Alice', to: 'Project_X', relationType: 'works_on' },
|
|
73
|
+
{ from: 'Bob', to: 'Project_X', relationType: 'manages' },
|
|
74
|
+
{ from: 'Alice', to: 'Bob', relationType: 'reports_to' },
|
|
75
|
+
{ from: 'Charlie', to: 'Company', relationType: 'works_for' },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
// Clean up test files
|
|
80
|
+
try {
|
|
81
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore cleanup errors
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
describe('searchNodes', () => {
|
|
88
|
+
it('should find entities by name', async () => {
|
|
89
|
+
const result = await basicSearch.searchNodes('Alice');
|
|
90
|
+
expect(result.entities).toHaveLength(1);
|
|
91
|
+
expect(result.entities[0].name).toBe('Alice');
|
|
92
|
+
});
|
|
93
|
+
it('should be case-insensitive', async () => {
|
|
94
|
+
const result = await basicSearch.searchNodes('alice');
|
|
95
|
+
expect(result.entities).toHaveLength(1);
|
|
96
|
+
expect(result.entities[0].name).toBe('Alice');
|
|
97
|
+
});
|
|
98
|
+
it('should find entities by entityType', async () => {
|
|
99
|
+
const result = await basicSearch.searchNodes('person');
|
|
100
|
+
expect(result.entities).toHaveLength(3);
|
|
101
|
+
expect(result.entities.map(e => e.name)).toContain('Alice');
|
|
102
|
+
expect(result.entities.map(e => e.name)).toContain('Bob');
|
|
103
|
+
expect(result.entities.map(e => e.name)).toContain('Charlie');
|
|
104
|
+
});
|
|
105
|
+
it('should find entities by observation content', async () => {
|
|
106
|
+
const result = await basicSearch.searchNodes('Python');
|
|
107
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
108
|
+
expect(result.entities.map(e => e.name)).toContain('Alice');
|
|
109
|
+
expect(result.entities.map(e => e.name)).toContain('Project_X');
|
|
110
|
+
});
|
|
111
|
+
it('should filter by single tag', async () => {
|
|
112
|
+
const result = await basicSearch.searchNodes('', ['python']);
|
|
113
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
114
|
+
expect(result.entities.every(e => e.tags?.includes('python'))).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('should filter by multiple tags (OR logic)', async () => {
|
|
117
|
+
const result = await basicSearch.searchNodes('', ['python', 'design']);
|
|
118
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(3);
|
|
119
|
+
const names = result.entities.map(e => e.name);
|
|
120
|
+
expect(names).toContain('Alice');
|
|
121
|
+
expect(names).toContain('Project_X');
|
|
122
|
+
expect(names).toContain('Charlie');
|
|
123
|
+
});
|
|
124
|
+
it('should filter by minimum importance', async () => {
|
|
125
|
+
const result = await basicSearch.searchNodes('', undefined, 9);
|
|
126
|
+
expect(result.entities).toHaveLength(3); // Alice (9), Project_X (10), Company (10)
|
|
127
|
+
expect(result.entities.every(e => e.importance >= 9)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('should filter by maximum importance', async () => {
|
|
130
|
+
const result = await basicSearch.searchNodes('', undefined, undefined, 7);
|
|
131
|
+
expect(result.entities).toHaveLength(1);
|
|
132
|
+
expect(result.entities[0].name).toBe('Charlie');
|
|
133
|
+
});
|
|
134
|
+
it('should filter by importance range', async () => {
|
|
135
|
+
const result = await basicSearch.searchNodes('', undefined, 8, 9);
|
|
136
|
+
expect(result.entities).toHaveLength(2);
|
|
137
|
+
const names = result.entities.map(e => e.name);
|
|
138
|
+
expect(names).toContain('Alice');
|
|
139
|
+
expect(names).toContain('Bob');
|
|
140
|
+
});
|
|
141
|
+
it('should combine text search with tag filter', async () => {
|
|
142
|
+
const result = await basicSearch.searchNodes('Project', ['python']);
|
|
143
|
+
expect(result.entities).toHaveLength(2); // Alice (has 'projects' in observations), Project_X
|
|
144
|
+
const names = result.entities.map(e => e.name);
|
|
145
|
+
expect(names).toContain('Alice');
|
|
146
|
+
expect(names).toContain('Project_X');
|
|
147
|
+
expect(result.entities.every(e => e.tags?.includes('python'))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
it('should combine text search with importance filter', async () => {
|
|
150
|
+
const result = await basicSearch.searchNodes('person', undefined, 8);
|
|
151
|
+
expect(result.entities).toHaveLength(2);
|
|
152
|
+
const names = result.entities.map(e => e.name);
|
|
153
|
+
expect(names).toContain('Alice');
|
|
154
|
+
expect(names).toContain('Bob');
|
|
155
|
+
});
|
|
156
|
+
it('should return empty result when no matches', async () => {
|
|
157
|
+
const result = await basicSearch.searchNodes('NonExistent');
|
|
158
|
+
expect(result.entities).toHaveLength(0);
|
|
159
|
+
expect(result.relations).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
it('should include relations between matched entities', async () => {
|
|
162
|
+
const result = await basicSearch.searchNodes('person');
|
|
163
|
+
expect(result.entities).toHaveLength(3);
|
|
164
|
+
expect(result.relations.length).toBeGreaterThan(0);
|
|
165
|
+
expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('should exclude relations to non-matched entities', async () => {
|
|
168
|
+
const result = await basicSearch.searchNodes('Alice');
|
|
169
|
+
expect(result.entities).toHaveLength(1);
|
|
170
|
+
// Relations should only include those where both entities are in the result
|
|
171
|
+
expect(result.relations).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
it('should handle empty query string', async () => {
|
|
174
|
+
const result = await basicSearch.searchNodes('');
|
|
175
|
+
// Empty query matches all entities
|
|
176
|
+
expect(result.entities).toHaveLength(5);
|
|
177
|
+
});
|
|
178
|
+
it('should handle entities without tags when filtering by tags', async () => {
|
|
179
|
+
// Create entity without tags
|
|
180
|
+
await entityManager.createEntities([
|
|
181
|
+
{ name: 'NoTags', entityType: 'test', observations: ['Test'] },
|
|
182
|
+
]);
|
|
183
|
+
const result = await basicSearch.searchNodes('', ['python']);
|
|
184
|
+
expect(result.entities.map(e => e.name)).not.toContain('NoTags');
|
|
185
|
+
});
|
|
186
|
+
it('should handle entities without importance when filtering', async () => {
|
|
187
|
+
// Create entity without importance
|
|
188
|
+
await entityManager.createEntities([
|
|
189
|
+
{ name: 'NoImportance', entityType: 'test', observations: ['Test'] },
|
|
190
|
+
]);
|
|
191
|
+
const result = await basicSearch.searchNodes('', undefined, 5);
|
|
192
|
+
expect(result.entities.map(e => e.name)).not.toContain('NoImportance');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('openNodes', () => {
|
|
196
|
+
it('should retrieve specific entities by name', async () => {
|
|
197
|
+
const result = await basicSearch.openNodes(['Alice', 'Bob']);
|
|
198
|
+
expect(result.entities).toHaveLength(2);
|
|
199
|
+
const names = result.entities.map(e => e.name);
|
|
200
|
+
expect(names).toContain('Alice');
|
|
201
|
+
expect(names).toContain('Bob');
|
|
202
|
+
});
|
|
203
|
+
it('should include relations between retrieved entities', async () => {
|
|
204
|
+
const result = await basicSearch.openNodes(['Alice', 'Bob']);
|
|
205
|
+
expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'reports_to')).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
it('should exclude relations to non-retrieved entities', async () => {
|
|
208
|
+
const result = await basicSearch.openNodes(['Alice']);
|
|
209
|
+
// Alice has relations to Bob and Project_X, but they're not in the result
|
|
210
|
+
expect(result.relations).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
it('should handle non-existent entity names', async () => {
|
|
213
|
+
const result = await basicSearch.openNodes(['NonExistent', 'AlsoNonExistent']);
|
|
214
|
+
expect(result.entities).toHaveLength(0);
|
|
215
|
+
expect(result.relations).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
it('should handle mix of existing and non-existing names', async () => {
|
|
218
|
+
const result = await basicSearch.openNodes(['Alice', 'NonExistent', 'Bob']);
|
|
219
|
+
expect(result.entities).toHaveLength(2);
|
|
220
|
+
const names = result.entities.map(e => e.name);
|
|
221
|
+
expect(names).toContain('Alice');
|
|
222
|
+
expect(names).toContain('Bob');
|
|
223
|
+
});
|
|
224
|
+
it('should handle empty names array', async () => {
|
|
225
|
+
const result = await basicSearch.openNodes([]);
|
|
226
|
+
expect(result.entities).toHaveLength(0);
|
|
227
|
+
expect(result.relations).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
it('should retrieve all requested entities with their subgraph', async () => {
|
|
230
|
+
const result = await basicSearch.openNodes(['Alice', 'Bob', 'Project_X']);
|
|
231
|
+
expect(result.entities).toHaveLength(3);
|
|
232
|
+
expect(result.relations.length).toBeGreaterThan(0);
|
|
233
|
+
expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Project_X')).toBe(true);
|
|
234
|
+
expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Project_X')).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
it('should be case-sensitive for entity names', async () => {
|
|
237
|
+
const result = await basicSearch.openNodes(['alice']); // lowercase
|
|
238
|
+
expect(result.entities).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('searchByDateRange', () => {
|
|
242
|
+
it('should find entities created after start date', async () => {
|
|
243
|
+
const result = await basicSearch.searchByDateRange('2024-02-01T00:00:00.000Z');
|
|
244
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
245
|
+
const names = result.entities.map(e => e.name);
|
|
246
|
+
expect(names).toContain('Bob');
|
|
247
|
+
expect(names).toContain('Charlie');
|
|
248
|
+
});
|
|
249
|
+
it('should find entities created before end date', async () => {
|
|
250
|
+
const result = await basicSearch.searchByDateRange(undefined, '2024-02-01T00:00:00.000Z');
|
|
251
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(3);
|
|
252
|
+
const names = result.entities.map(e => e.name);
|
|
253
|
+
expect(names).toContain('Alice');
|
|
254
|
+
expect(names).toContain('Project_X');
|
|
255
|
+
expect(names).toContain('Company');
|
|
256
|
+
});
|
|
257
|
+
it('should find entities within date range', async () => {
|
|
258
|
+
const result = await basicSearch.searchByDateRange('2024-01-15T00:00:00.000Z', '2024-02-28T23:59:59.999Z');
|
|
259
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
260
|
+
const names = result.entities.map(e => e.name);
|
|
261
|
+
expect(names).toContain('Alice');
|
|
262
|
+
expect(names).toContain('Bob');
|
|
263
|
+
});
|
|
264
|
+
it('should filter by entity type', async () => {
|
|
265
|
+
const result = await basicSearch.searchByDateRange(undefined, undefined, 'person');
|
|
266
|
+
expect(result.entities).toHaveLength(3);
|
|
267
|
+
expect(result.entities.every(e => e.entityType === 'person')).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
it('should filter by tags', async () => {
|
|
270
|
+
const result = await basicSearch.searchByDateRange(undefined, undefined, undefined, ['python']);
|
|
271
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
272
|
+
expect(result.entities.every(e => e.tags?.includes('python'))).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
it('should combine date range with entity type filter', async () => {
|
|
275
|
+
const result = await basicSearch.searchByDateRange('2024-02-01T00:00:00.000Z', undefined, 'person');
|
|
276
|
+
expect(result.entities).toHaveLength(2);
|
|
277
|
+
const names = result.entities.map(e => e.name);
|
|
278
|
+
expect(names).toContain('Bob');
|
|
279
|
+
expect(names).toContain('Charlie');
|
|
280
|
+
});
|
|
281
|
+
it('should combine date range with tags filter', async () => {
|
|
282
|
+
const result = await basicSearch.searchByDateRange('2024-01-01T00:00:00.000Z', '2024-01-31T23:59:59.999Z', undefined, ['python']);
|
|
283
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(2);
|
|
284
|
+
const names = result.entities.map(e => e.name);
|
|
285
|
+
expect(names).toContain('Alice');
|
|
286
|
+
expect(names).toContain('Project_X');
|
|
287
|
+
});
|
|
288
|
+
it('should include relations within date range', async () => {
|
|
289
|
+
const result = await basicSearch.searchByDateRange('2024-01-01T00:00:00.000Z');
|
|
290
|
+
expect(result.entities).toHaveLength(5);
|
|
291
|
+
expect(result.relations.length).toBeGreaterThan(0);
|
|
292
|
+
});
|
|
293
|
+
it('should handle entities without createdAt timestamp', async () => {
|
|
294
|
+
// This shouldn't happen in practice, but test the fallback to lastModified
|
|
295
|
+
const result = await basicSearch.searchByDateRange('2024-01-01T00:00:00.000Z');
|
|
296
|
+
expect(result.entities.length).toBeGreaterThanOrEqual(0);
|
|
297
|
+
});
|
|
298
|
+
it('should return empty result when no matches in date range', async () => {
|
|
299
|
+
const result = await basicSearch.searchByDateRange('2025-01-01T00:00:00.000Z', '2025-12-31T23:59:59.999Z');
|
|
300
|
+
expect(result.entities).toHaveLength(0);
|
|
301
|
+
});
|
|
302
|
+
it('should handle no date filters (return all)', async () => {
|
|
303
|
+
const result = await basicSearch.searchByDateRange();
|
|
304
|
+
expect(result.entities).toHaveLength(5);
|
|
305
|
+
});
|
|
306
|
+
it('should exclude entities without matching tags', async () => {
|
|
307
|
+
const result = await basicSearch.searchByDateRange(undefined, undefined, undefined, ['nonexistent']);
|
|
308
|
+
expect(result.entities).toHaveLength(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|