@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,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge Case Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for unusual inputs, boundary conditions, and error scenarios
|
|
5
|
+
* that stress the system's robustness and error handling.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { GraphStorage } from '../../core/GraphStorage.js';
|
|
9
|
+
import { EntityManager } from '../../core/EntityManager.js';
|
|
10
|
+
import { RelationManager } from '../../core/RelationManager.js';
|
|
11
|
+
import { BasicSearch } from '../../search/BasicSearch.js';
|
|
12
|
+
import { RankedSearch } from '../../search/RankedSearch.js';
|
|
13
|
+
import { BooleanSearch } from '../../search/BooleanSearch.js';
|
|
14
|
+
import { FuzzySearch } from '../../search/FuzzySearch.js';
|
|
15
|
+
import { promises as fs } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { tmpdir } from 'os';
|
|
18
|
+
describe('Edge Cases', () => {
|
|
19
|
+
let storage;
|
|
20
|
+
let entityManager;
|
|
21
|
+
let relationManager;
|
|
22
|
+
let basicSearch;
|
|
23
|
+
let rankedSearch;
|
|
24
|
+
let booleanSearch;
|
|
25
|
+
let fuzzySearch;
|
|
26
|
+
let testDir;
|
|
27
|
+
let testFilePath;
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
testDir = join(tmpdir(), `edge-case-test-${Date.now()}-${Math.random()}`);
|
|
30
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
31
|
+
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
32
|
+
storage = new GraphStorage(testFilePath);
|
|
33
|
+
entityManager = new EntityManager(storage);
|
|
34
|
+
relationManager = new RelationManager(storage);
|
|
35
|
+
basicSearch = new BasicSearch(storage);
|
|
36
|
+
rankedSearch = new RankedSearch(storage);
|
|
37
|
+
booleanSearch = new BooleanSearch(storage);
|
|
38
|
+
fuzzySearch = new FuzzySearch(storage);
|
|
39
|
+
});
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
try {
|
|
42
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore cleanup errors
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
describe('Unicode and Special Characters', () => {
|
|
49
|
+
it('should handle entity names with emoji', async () => {
|
|
50
|
+
const entities = await entityManager.createEntities([
|
|
51
|
+
{ name: 'User 👤', entityType: 'person', observations: ['Has emoji in name'] },
|
|
52
|
+
{ name: 'Project 🚀', entityType: 'project', observations: ['Rocket project'] },
|
|
53
|
+
]);
|
|
54
|
+
expect(entities).toHaveLength(2);
|
|
55
|
+
const results = await basicSearch.searchNodes('User');
|
|
56
|
+
expect(results.entities.length).toBeGreaterThanOrEqual(1);
|
|
57
|
+
expect(results.entities[0].name).toBe('User 👤');
|
|
58
|
+
});
|
|
59
|
+
it('should handle observations with mixed scripts (Latin, Cyrillic, CJK)', async () => {
|
|
60
|
+
await entityManager.createEntities([
|
|
61
|
+
{
|
|
62
|
+
name: 'International',
|
|
63
|
+
entityType: 'document',
|
|
64
|
+
observations: ['English', 'Русский', '中文', '日本語', 'العربية'],
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
const results = await basicSearch.searchNodes('International');
|
|
68
|
+
expect(results.entities).toHaveLength(1);
|
|
69
|
+
expect(results.entities[0].observations).toHaveLength(5);
|
|
70
|
+
});
|
|
71
|
+
it('should handle right-to-left text', async () => {
|
|
72
|
+
await entityManager.createEntities([
|
|
73
|
+
{ name: 'RTL_Text', entityType: 'text', observations: ['שלום', 'مرحبا'] },
|
|
74
|
+
]);
|
|
75
|
+
const results = await basicSearch.searchNodes('RTL');
|
|
76
|
+
expect(results.entities).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
it('should handle zero-width characters', async () => {
|
|
79
|
+
await entityManager.createEntities([
|
|
80
|
+
{
|
|
81
|
+
name: 'Test\u200BZero\u200CWidth',
|
|
82
|
+
entityType: 'test',
|
|
83
|
+
observations: ['Has zero-width\u200Bchars'],
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
const results = await basicSearch.searchNodes('Zero');
|
|
87
|
+
expect(results.entities.length).toBeGreaterThanOrEqual(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('Extreme Values', () => {
|
|
91
|
+
it('should handle entity with maximum observations (array limit)', async () => {
|
|
92
|
+
const manyObservations = Array.from({ length: 100 }, (_, i) => `Observation ${i}`);
|
|
93
|
+
const entities = await entityManager.createEntities([
|
|
94
|
+
{ name: 'ManyObs', entityType: 'test', observations: manyObservations },
|
|
95
|
+
]);
|
|
96
|
+
expect(entities[0].observations).toHaveLength(100);
|
|
97
|
+
const results = await basicSearch.searchNodes('Observation 50');
|
|
98
|
+
expect(results.entities).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
it('should handle entity with maximum tags (array limit)', async () => {
|
|
101
|
+
const manyTags = Array.from({ length: 50 }, (_, i) => `tag${i}`);
|
|
102
|
+
const entities = await entityManager.createEntities([
|
|
103
|
+
{ name: 'ManyTags', entityType: 'test', observations: ['Test'], tags: manyTags },
|
|
104
|
+
]);
|
|
105
|
+
expect(entities[0].tags).toHaveLength(50);
|
|
106
|
+
});
|
|
107
|
+
it('should handle importance at exact boundaries (0 and 10)', async () => {
|
|
108
|
+
await entityManager.createEntities([
|
|
109
|
+
{ name: 'MinImportance', entityType: 'test', observations: ['Test'], importance: 0 },
|
|
110
|
+
{ name: 'MaxImportance', entityType: 'test', observations: ['Test'], importance: 10 },
|
|
111
|
+
]);
|
|
112
|
+
const minResults = await basicSearch.searchNodes('', undefined, 0, 0);
|
|
113
|
+
expect(minResults.entities).toHaveLength(1);
|
|
114
|
+
expect(minResults.entities[0].importance).toBe(0);
|
|
115
|
+
const maxResults = await basicSearch.searchNodes('', undefined, 10, 10);
|
|
116
|
+
expect(maxResults.entities).toHaveLength(1);
|
|
117
|
+
expect(maxResults.entities[0].importance).toBe(10);
|
|
118
|
+
});
|
|
119
|
+
it('should handle very long entity names (200+ characters)', async () => {
|
|
120
|
+
const longName = 'A'.repeat(250);
|
|
121
|
+
const entities = await entityManager.createEntities([
|
|
122
|
+
{ name: longName, entityType: 'test', observations: ['Long name entity'] },
|
|
123
|
+
]);
|
|
124
|
+
expect(entities[0].name).toHaveLength(250);
|
|
125
|
+
const results = await basicSearch.searchNodes(longName.substring(0, 10));
|
|
126
|
+
expect(results.entities).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
it('should handle very long observations (500+ characters)', async () => {
|
|
129
|
+
const longObservation = 'This is a very long observation. '.repeat(20);
|
|
130
|
+
await entityManager.createEntities([
|
|
131
|
+
{ name: 'LongObs', entityType: 'test', observations: [longObservation] },
|
|
132
|
+
]);
|
|
133
|
+
const results = await basicSearch.searchNodes('long observation');
|
|
134
|
+
expect(results.entities).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('Empty and Null-like Values', () => {
|
|
138
|
+
it('should handle entity with empty string name (if allowed by validation)', async () => {
|
|
139
|
+
// This should fail validation
|
|
140
|
+
await expect(entityManager.createEntities([{ name: '', entityType: 'test', observations: ['Test'] }])).rejects.toThrow();
|
|
141
|
+
});
|
|
142
|
+
it('should handle entity with whitespace-only name', async () => {
|
|
143
|
+
// System allows whitespace-only names
|
|
144
|
+
const entities = await entityManager.createEntities([
|
|
145
|
+
{ name: ' ', entityType: 'test', observations: ['Test'] }
|
|
146
|
+
]);
|
|
147
|
+
expect(entities).toHaveLength(1);
|
|
148
|
+
expect(entities[0].name).toBe(' ');
|
|
149
|
+
});
|
|
150
|
+
it('should handle entity with empty observations array', async () => {
|
|
151
|
+
const entities = await entityManager.createEntities([
|
|
152
|
+
{ name: 'EmptyObs', entityType: 'test', observations: [] },
|
|
153
|
+
]);
|
|
154
|
+
expect(entities[0].observations).toHaveLength(0);
|
|
155
|
+
const results = await basicSearch.searchNodes('EmptyObs');
|
|
156
|
+
expect(results.entities).toHaveLength(1);
|
|
157
|
+
});
|
|
158
|
+
it('should handle entity with empty tags array', async () => {
|
|
159
|
+
const entities = await entityManager.createEntities([
|
|
160
|
+
{ name: 'NoTags', entityType: 'test', observations: ['Test'], tags: [] },
|
|
161
|
+
]);
|
|
162
|
+
expect(entities[0].tags).toHaveLength(0);
|
|
163
|
+
});
|
|
164
|
+
it('should handle entity without optional fields', async () => {
|
|
165
|
+
const entities = await entityManager.createEntities([
|
|
166
|
+
{ name: 'Minimal', entityType: 'test', observations: ['Test'] },
|
|
167
|
+
]);
|
|
168
|
+
expect(entities[0].tags).toBeUndefined();
|
|
169
|
+
expect(entities[0].importance).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('Search Edge Cases', () => {
|
|
173
|
+
it('should handle search with very long query string', async () => {
|
|
174
|
+
await entityManager.createEntities([
|
|
175
|
+
{ name: 'Test', entityType: 'test', observations: ['Short observation'] },
|
|
176
|
+
]);
|
|
177
|
+
const longQuery = 'test '.repeat(100);
|
|
178
|
+
const results = await basicSearch.searchNodes(longQuery);
|
|
179
|
+
expect(Array.isArray(results.entities)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it('should handle ranked search with empty query', async () => {
|
|
182
|
+
await entityManager.createEntities([
|
|
183
|
+
{ name: 'Test', entityType: 'test', observations: ['Test'] },
|
|
184
|
+
]);
|
|
185
|
+
const results = await rankedSearch.searchNodesRanked('');
|
|
186
|
+
expect(results).toHaveLength(0);
|
|
187
|
+
});
|
|
188
|
+
it('should handle boolean search with deeply nested parentheses', async () => {
|
|
189
|
+
await entityManager.createEntities([
|
|
190
|
+
{ name: 'Test', entityType: 'test', observations: ['Deep nesting'] },
|
|
191
|
+
]);
|
|
192
|
+
const results = await booleanSearch.booleanSearch('((((Test))))');
|
|
193
|
+
expect(results.entities).toHaveLength(1);
|
|
194
|
+
});
|
|
195
|
+
it('should handle fuzzy search with threshold at boundaries (0 and 1)', async () => {
|
|
196
|
+
await entityManager.createEntities([
|
|
197
|
+
{ name: 'Alice', entityType: 'person', observations: ['Test'] },
|
|
198
|
+
]);
|
|
199
|
+
// Threshold 0 should match everything
|
|
200
|
+
const results0 = await fuzzySearch.fuzzySearch('xyz', 0);
|
|
201
|
+
expect(results0.entities.length).toBeGreaterThan(0);
|
|
202
|
+
// Threshold 1 should only match exact
|
|
203
|
+
const results1 = await fuzzySearch.fuzzySearch('Alice', 1);
|
|
204
|
+
expect(results1.entities).toHaveLength(1);
|
|
205
|
+
});
|
|
206
|
+
it('should handle search by date range with same start and end date', async () => {
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
await entityManager.createEntities([
|
|
209
|
+
{ name: 'Test', entityType: 'test', observations: ['Test'] },
|
|
210
|
+
]);
|
|
211
|
+
const results = await basicSearch.searchByDateRange(now, now);
|
|
212
|
+
expect(Array.isArray(results.entities)).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
it('should handle search by date range with end before start', async () => {
|
|
215
|
+
const now = new Date();
|
|
216
|
+
const future = new Date(now.getTime() + 86400000).toISOString();
|
|
217
|
+
const past = new Date(now.getTime() - 86400000).toISOString();
|
|
218
|
+
await entityManager.createEntities([
|
|
219
|
+
{ name: 'Test', entityType: 'test', observations: ['Test'] },
|
|
220
|
+
]);
|
|
221
|
+
// End before start should return empty results
|
|
222
|
+
const results = await basicSearch.searchByDateRange(future, past);
|
|
223
|
+
expect(results.entities).toHaveLength(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('Relation Edge Cases', () => {
|
|
227
|
+
it('should handle self-referencing relation', async () => {
|
|
228
|
+
await entityManager.createEntities([
|
|
229
|
+
{ name: 'SelfRef', entityType: 'test', observations: ['Test'] },
|
|
230
|
+
]);
|
|
231
|
+
const relations = await relationManager.createRelations([
|
|
232
|
+
{ from: 'SelfRef', to: 'SelfRef', relationType: 'relates_to' },
|
|
233
|
+
]);
|
|
234
|
+
expect(relations).toHaveLength(1);
|
|
235
|
+
expect(relations[0].from).toBe('SelfRef');
|
|
236
|
+
expect(relations[0].to).toBe('SelfRef');
|
|
237
|
+
});
|
|
238
|
+
it('should handle circular relations (A->B->C->A)', async () => {
|
|
239
|
+
await entityManager.createEntities([
|
|
240
|
+
{ name: 'A', entityType: 'test', observations: ['Test'] },
|
|
241
|
+
{ name: 'B', entityType: 'test', observations: ['Test'] },
|
|
242
|
+
{ name: 'C', entityType: 'test', observations: ['Test'] },
|
|
243
|
+
]);
|
|
244
|
+
await relationManager.createRelations([
|
|
245
|
+
{ from: 'A', to: 'B', relationType: 'links' },
|
|
246
|
+
{ from: 'B', to: 'C', relationType: 'links' },
|
|
247
|
+
{ from: 'C', to: 'A', relationType: 'links' },
|
|
248
|
+
]);
|
|
249
|
+
const results = await basicSearch.searchNodes('');
|
|
250
|
+
expect(results.entities).toHaveLength(3);
|
|
251
|
+
expect(results.relations).toHaveLength(3);
|
|
252
|
+
});
|
|
253
|
+
it('should handle very long relation type names (up to 100 chars)', async () => {
|
|
254
|
+
await entityManager.createEntities([
|
|
255
|
+
{ name: 'Entity1', entityType: 'test', observations: ['Test'] },
|
|
256
|
+
{ name: 'Entity2', entityType: 'test', observations: ['Test'] },
|
|
257
|
+
]);
|
|
258
|
+
// Max length is 100 characters, use 90 to be safe
|
|
259
|
+
const longRelationType = 'a'.repeat(90);
|
|
260
|
+
const relations = await relationManager.createRelations([
|
|
261
|
+
{ from: 'Entity1', to: 'Entity2', relationType: longRelationType },
|
|
262
|
+
]);
|
|
263
|
+
expect(relations[0].relationType).toBe(longRelationType);
|
|
264
|
+
expect(relations[0].relationType.length).toBe(90);
|
|
265
|
+
});
|
|
266
|
+
it('should handle multiple relations between same entities', async () => {
|
|
267
|
+
await entityManager.createEntities([
|
|
268
|
+
{ name: 'Person1', entityType: 'person', observations: ['Test'] },
|
|
269
|
+
{ name: 'Person2', entityType: 'person', observations: ['Test'] },
|
|
270
|
+
]);
|
|
271
|
+
await relationManager.createRelations([
|
|
272
|
+
{ from: 'Person1', to: 'Person2', relationType: 'knows' },
|
|
273
|
+
{ from: 'Person1', to: 'Person2', relationType: 'collaborates_with' },
|
|
274
|
+
{ from: 'Person1', to: 'Person2', relationType: 'mentors' },
|
|
275
|
+
]);
|
|
276
|
+
const results = await basicSearch.searchNodes('Person');
|
|
277
|
+
expect(results.relations.length).toBeGreaterThanOrEqual(3);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
describe('Concurrent Operations', () => {
|
|
281
|
+
it('should handle multiple simultaneous entity creations', async () => {
|
|
282
|
+
const promises = Array.from({ length: 10 }, (_, i) => entityManager.createEntities([
|
|
283
|
+
{ name: `Concurrent${i}`, entityType: 'test', observations: ['Test'] },
|
|
284
|
+
]));
|
|
285
|
+
const results = await Promise.all(promises);
|
|
286
|
+
expect(results).toHaveLength(10);
|
|
287
|
+
// Verify each result has the expected entity
|
|
288
|
+
results.forEach((result, i) => {
|
|
289
|
+
expect(result).toHaveLength(1);
|
|
290
|
+
expect(result[0].name).toBe(`Concurrent${i}`);
|
|
291
|
+
});
|
|
292
|
+
// Search should find all created entities (may be less if concurrent writes interfered)
|
|
293
|
+
const searchResults = await basicSearch.searchNodes('Concurrent');
|
|
294
|
+
expect(searchResults.entities.length).toBeGreaterThan(0);
|
|
295
|
+
expect(searchResults.entities.length).toBeLessThanOrEqual(10);
|
|
296
|
+
});
|
|
297
|
+
it('should handle concurrent reads and writes', async () => {
|
|
298
|
+
await entityManager.createEntities([
|
|
299
|
+
{ name: 'Initial', entityType: 'test', observations: ['Test'] },
|
|
300
|
+
]);
|
|
301
|
+
const operations = [
|
|
302
|
+
basicSearch.searchNodes('Initial'),
|
|
303
|
+
entityManager.createEntities([
|
|
304
|
+
{ name: 'New1', entityType: 'test', observations: ['Test'] },
|
|
305
|
+
]),
|
|
306
|
+
basicSearch.searchNodes(''),
|
|
307
|
+
entityManager.createEntities([
|
|
308
|
+
{ name: 'New2', entityType: 'test', observations: ['Test'] },
|
|
309
|
+
]),
|
|
310
|
+
];
|
|
311
|
+
const results = await Promise.all(operations);
|
|
312
|
+
expect(results).toHaveLength(4);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
describe('Validation Edge Cases', () => {
|
|
316
|
+
it('should reject entity with invalid importance (negative)', async () => {
|
|
317
|
+
await expect(entityManager.createEntities([
|
|
318
|
+
{ name: 'Invalid', entityType: 'test', observations: ['Test'], importance: -1 },
|
|
319
|
+
])).rejects.toThrow();
|
|
320
|
+
});
|
|
321
|
+
it('should reject entity with invalid importance (> 10)', async () => {
|
|
322
|
+
await expect(entityManager.createEntities([
|
|
323
|
+
{ name: 'Invalid', entityType: 'test', observations: ['Test'], importance: 11 },
|
|
324
|
+
])).rejects.toThrow();
|
|
325
|
+
});
|
|
326
|
+
it('should reject entity with invalid importance (non-integer)', async () => {
|
|
327
|
+
await expect(entityManager.createEntities([
|
|
328
|
+
{ name: 'Invalid', entityType: 'test', observations: ['Test'], importance: 5.5 },
|
|
329
|
+
])).rejects.toThrow();
|
|
330
|
+
});
|
|
331
|
+
it('should handle entity names with leading/trailing whitespace', async () => {
|
|
332
|
+
// System allows names with leading/trailing whitespace
|
|
333
|
+
const entities = await entityManager.createEntities([
|
|
334
|
+
{ name: ' LeadingSpace', entityType: 'test', observations: ['Test'] },
|
|
335
|
+
{ name: 'TrailingSpace ', entityType: 'test', observations: ['Test'] },
|
|
336
|
+
]);
|
|
337
|
+
expect(entities).toHaveLength(2);
|
|
338
|
+
expect(entities[0].name).toBe(' LeadingSpace');
|
|
339
|
+
expect(entities[1].name).toBe('TrailingSpace ');
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe('Large Graph Operations', () => {
|
|
343
|
+
it('should handle entity with 100+ relations', async () => {
|
|
344
|
+
await entityManager.createEntities([{ name: 'Hub', entityType: 'hub', observations: ['Central hub'] }]);
|
|
345
|
+
// Create 100 spoke entities
|
|
346
|
+
const spokes = Array.from({ length: 100 }, (_, i) => ({
|
|
347
|
+
name: `Spoke${i}`,
|
|
348
|
+
entityType: 'spoke',
|
|
349
|
+
observations: ['Spoke'],
|
|
350
|
+
}));
|
|
351
|
+
await entityManager.createEntities(spokes);
|
|
352
|
+
// Create relations from hub to all spokes
|
|
353
|
+
const relations = Array.from({ length: 100 }, (_, i) => ({
|
|
354
|
+
from: 'Hub',
|
|
355
|
+
to: `Spoke${i}`,
|
|
356
|
+
relationType: 'connects',
|
|
357
|
+
}));
|
|
358
|
+
await relationManager.createRelations(relations);
|
|
359
|
+
// Verify relations exist by opening all nodes
|
|
360
|
+
const allNodeNames = ['Hub', ...Array.from({ length: 100 }, (_, i) => `Spoke${i}`)];
|
|
361
|
+
const results = await basicSearch.openNodes(allNodeNames);
|
|
362
|
+
// Should have Hub + 100 Spokes
|
|
363
|
+
expect(results.entities.length).toBe(101);
|
|
364
|
+
// Count outgoing relations from Hub
|
|
365
|
+
const hubOutgoingRelations = results.relations.filter(r => r.from === 'Hub');
|
|
366
|
+
expect(hubOutgoingRelations.length).toBe(100);
|
|
367
|
+
});
|
|
368
|
+
it('should efficiently handle search on graph with 500+ entities', async () => {
|
|
369
|
+
const entities = Array.from({ length: 500 }, (_, i) => ({
|
|
370
|
+
name: `Entity${i}`,
|
|
371
|
+
entityType: 'test',
|
|
372
|
+
observations: [`Description ${i}`],
|
|
373
|
+
importance: (i % 10) + 1,
|
|
374
|
+
}));
|
|
375
|
+
await entityManager.createEntities(entities);
|
|
376
|
+
const startTime = Date.now();
|
|
377
|
+
const results = await basicSearch.searchNodes('Entity', undefined, 5);
|
|
378
|
+
const duration = Date.now() - startTime;
|
|
379
|
+
expect(results.entities.length).toBeGreaterThan(0);
|
|
380
|
+
expect(duration).toBeLessThan(2000); // Should complete in < 2 seconds
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
describe('Special Query Characters', () => {
|
|
384
|
+
it('should handle boolean search with special regex characters', async () => {
|
|
385
|
+
await entityManager.createEntities([
|
|
386
|
+
{ name: 'Test.*Special', entityType: 'test', observations: ['Has special chars'] },
|
|
387
|
+
]);
|
|
388
|
+
const results = await booleanSearch.booleanSearch('name:Test');
|
|
389
|
+
expect(results.entities.length).toBeGreaterThanOrEqual(1);
|
|
390
|
+
});
|
|
391
|
+
it('should handle search with SQL injection-like patterns', async () => {
|
|
392
|
+
await entityManager.createEntities([
|
|
393
|
+
{ name: "Test'; DROP TABLE entities;--", entityType: 'test', observations: ['SQL injection test'] },
|
|
394
|
+
]);
|
|
395
|
+
const results = await basicSearch.searchNodes("Test'; DROP");
|
|
396
|
+
expect(results.entities).toHaveLength(1);
|
|
397
|
+
});
|
|
398
|
+
it('should handle search with XSS-like patterns', async () => {
|
|
399
|
+
await entityManager.createEntities([
|
|
400
|
+
{ name: '<script>alert("xss")</script>', entityType: 'test', observations: ['XSS test'] },
|
|
401
|
+
]);
|
|
402
|
+
const results = await basicSearch.searchNodes('script');
|
|
403
|
+
expect(results.entities).toHaveLength(1);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -71,7 +71,7 @@ describe('ensureMemoryFilePath', () => {
|
|
|
71
71
|
it('should migrate from memory.json to memory.jsonl when only old file exists', async () => {
|
|
72
72
|
// Create old memory.json file
|
|
73
73
|
await fs.writeFile(oldMemoryPath, '{"test":"data"}');
|
|
74
|
-
const
|
|
74
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
75
75
|
const result = await ensureMemoryFilePath();
|
|
76
76
|
expect(result).toBe(defaultMemoryPath);
|
|
77
77
|
// Verify migration happened
|
|
@@ -79,10 +79,10 @@ describe('ensureMemoryFilePath', () => {
|
|
|
79
79
|
const oldFileExists = await fs.access(oldMemoryPath).then(() => true).catch(() => false);
|
|
80
80
|
expect(newFileExists).toBe(true);
|
|
81
81
|
expect(oldFileExists).toBe(false);
|
|
82
|
-
// Verify console messages
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
85
|
-
|
|
82
|
+
// Verify console messages (now using console.log instead of console.error)
|
|
83
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('[INFO] Found legacy memory.json file'));
|
|
84
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('[INFO] Successfully migrated'));
|
|
85
|
+
consoleLogSpy.mockRestore();
|
|
86
86
|
});
|
|
87
87
|
it('should use new file when both old and new files exist', async () => {
|
|
88
88
|
// Create both files
|