@danielsimonjr/memory-mcp 0.48.0 → 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.
- package/LICENSE +22 -0
- package/README.md +2000 -194
- package/dist/__tests__/file-path.test.js +7 -11
- package/dist/__tests__/knowledge-graph.test.js +3 -8
- package/dist/core/EntityManager.d.ts +266 -0
- package/dist/core/EntityManager.d.ts.map +1 -0
- package/dist/core/EntityManager.js +85 -133
- package/dist/core/GraphEventEmitter.d.ts +202 -0
- package/dist/core/GraphEventEmitter.d.ts.map +1 -0
- package/dist/core/GraphEventEmitter.js +346 -0
- package/dist/core/GraphStorage.d.ts +395 -0
- package/dist/core/GraphStorage.d.ts.map +1 -0
- package/dist/core/GraphStorage.js +643 -31
- package/dist/core/GraphTraversal.d.ts +141 -0
- package/dist/core/GraphTraversal.d.ts.map +1 -0
- package/dist/core/GraphTraversal.js +573 -0
- package/dist/core/HierarchyManager.d.ts +111 -0
- package/dist/core/HierarchyManager.d.ts.map +1 -0
- package/dist/{features → core}/HierarchyManager.js +14 -9
- package/dist/core/ManagerContext.d.ts +72 -0
- package/dist/core/ManagerContext.d.ts.map +1 -0
- package/dist/core/ManagerContext.js +118 -0
- package/dist/core/ObservationManager.d.ts +85 -0
- package/dist/core/ObservationManager.d.ts.map +1 -0
- package/dist/core/ObservationManager.js +51 -57
- package/dist/core/RelationManager.d.ts +131 -0
- package/dist/core/RelationManager.d.ts.map +1 -0
- package/dist/core/RelationManager.js +31 -7
- package/dist/core/SQLiteStorage.d.ts +354 -0
- package/dist/core/SQLiteStorage.d.ts.map +1 -0
- package/dist/core/SQLiteStorage.js +917 -0
- package/dist/core/StorageFactory.d.ts +45 -0
- package/dist/core/StorageFactory.d.ts.map +1 -0
- package/dist/core/StorageFactory.js +64 -0
- package/dist/core/TransactionManager.d.ts +464 -0
- package/dist/core/TransactionManager.d.ts.map +1 -0
- package/dist/core/TransactionManager.js +490 -13
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +12 -2
- package/dist/features/AnalyticsManager.d.ts +44 -0
- package/dist/features/AnalyticsManager.d.ts.map +1 -0
- package/dist/features/AnalyticsManager.js +3 -2
- package/dist/features/ArchiveManager.d.ts +133 -0
- package/dist/features/ArchiveManager.d.ts.map +1 -0
- package/dist/features/ArchiveManager.js +221 -14
- package/dist/features/CompressionManager.d.ts +117 -0
- package/dist/features/CompressionManager.d.ts.map +1 -0
- package/dist/features/CompressionManager.js +189 -20
- package/dist/features/IOManager.d.ts +225 -0
- package/dist/features/IOManager.d.ts.map +1 -0
- package/dist/features/IOManager.js +1041 -0
- package/dist/features/StreamingExporter.d.ts +123 -0
- package/dist/features/StreamingExporter.d.ts.map +1 -0
- package/dist/features/StreamingExporter.js +203 -0
- package/dist/features/TagManager.d.ts +147 -0
- package/dist/features/TagManager.d.ts.map +1 -0
- package/dist/features/index.d.ts +12 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +5 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -10
- package/dist/memory.jsonl +1 -26
- package/dist/search/BasicSearch.d.ts +51 -0
- package/dist/search/BasicSearch.d.ts.map +1 -0
- package/dist/search/BasicSearch.js +9 -3
- package/dist/search/BooleanSearch.d.ts +98 -0
- package/dist/search/BooleanSearch.d.ts.map +1 -0
- package/dist/search/BooleanSearch.js +156 -9
- package/dist/search/EmbeddingService.d.ts +178 -0
- package/dist/search/EmbeddingService.d.ts.map +1 -0
- package/dist/search/EmbeddingService.js +358 -0
- package/dist/search/FuzzySearch.d.ts +118 -0
- package/dist/search/FuzzySearch.d.ts.map +1 -0
- package/dist/search/FuzzySearch.js +241 -25
- package/dist/search/QueryCostEstimator.d.ts +111 -0
- package/dist/search/QueryCostEstimator.d.ts.map +1 -0
- package/dist/search/QueryCostEstimator.js +355 -0
- package/dist/search/RankedSearch.d.ts +71 -0
- package/dist/search/RankedSearch.d.ts.map +1 -0
- package/dist/search/RankedSearch.js +54 -6
- package/dist/search/SavedSearchManager.d.ts +79 -0
- package/dist/search/SavedSearchManager.d.ts.map +1 -0
- package/dist/search/SearchFilterChain.d.ts +120 -0
- package/dist/search/SearchFilterChain.d.ts.map +1 -0
- package/dist/search/SearchFilterChain.js +2 -4
- package/dist/search/SearchManager.d.ts +326 -0
- package/dist/search/SearchManager.d.ts.map +1 -0
- package/dist/search/SearchManager.js +148 -0
- package/dist/search/SearchSuggestions.d.ts +27 -0
- package/dist/search/SearchSuggestions.d.ts.map +1 -0
- package/dist/search/SearchSuggestions.js +1 -1
- package/dist/search/SemanticSearch.d.ts +149 -0
- package/dist/search/SemanticSearch.d.ts.map +1 -0
- package/dist/search/SemanticSearch.js +323 -0
- package/dist/search/TFIDFEventSync.d.ts +85 -0
- package/dist/search/TFIDFEventSync.d.ts.map +1 -0
- package/dist/search/TFIDFEventSync.js +133 -0
- package/dist/search/TFIDFIndexManager.d.ts +151 -0
- package/dist/search/TFIDFIndexManager.d.ts.map +1 -0
- package/dist/search/TFIDFIndexManager.js +232 -17
- package/dist/search/VectorStore.d.ts +235 -0
- package/dist/search/VectorStore.d.ts.map +1 -0
- package/dist/search/VectorStore.js +311 -0
- package/dist/search/index.d.ts +21 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +12 -0
- package/dist/server/MCPServer.d.ts +21 -0
- package/dist/server/MCPServer.d.ts.map +1 -0
- package/dist/server/MCPServer.js +4 -4
- package/dist/server/responseCompressor.d.ts +94 -0
- package/dist/server/responseCompressor.d.ts.map +1 -0
- package/dist/server/responseCompressor.js +127 -0
- package/dist/server/toolDefinitions.d.ts +27 -0
- package/dist/server/toolDefinitions.d.ts.map +1 -0
- package/dist/server/toolDefinitions.js +188 -17
- package/dist/server/toolHandlers.d.ts +41 -0
- package/dist/server/toolHandlers.d.ts.map +1 -0
- package/dist/server/toolHandlers.js +467 -75
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/types.d.ts +1654 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +9 -0
- package/dist/utils/compressedCache.d.ts +192 -0
- package/dist/utils/compressedCache.d.ts.map +1 -0
- package/dist/utils/compressedCache.js +309 -0
- package/dist/utils/compressionUtil.d.ts +214 -0
- package/dist/utils/compressionUtil.d.ts.map +1 -0
- package/dist/utils/compressionUtil.js +247 -0
- package/dist/utils/constants.d.ts +245 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +124 -0
- package/dist/utils/entityUtils.d.ts +321 -0
- package/dist/utils/entityUtils.d.ts.map +1 -0
- package/dist/utils/entityUtils.js +434 -4
- package/dist/utils/errors.d.ts +95 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +24 -0
- package/dist/utils/formatters.d.ts +145 -0
- package/dist/utils/formatters.d.ts.map +1 -0
- package/dist/utils/{paginationUtils.js → formatters.js} +54 -3
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +69 -31
- package/dist/utils/indexes.d.ts +270 -0
- package/dist/utils/indexes.d.ts.map +1 -0
- package/dist/utils/indexes.js +526 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/operationUtils.d.ts +124 -0
- package/dist/utils/operationUtils.d.ts.map +1 -0
- package/dist/utils/operationUtils.js +175 -0
- package/dist/utils/parallelUtils.d.ts +72 -0
- package/dist/utils/parallelUtils.d.ts.map +1 -0
- package/dist/utils/parallelUtils.js +169 -0
- package/dist/utils/schemas.d.ts +374 -0
- package/dist/utils/schemas.d.ts.map +1 -0
- package/dist/utils/schemas.js +302 -2
- package/dist/utils/searchAlgorithms.d.ts +99 -0
- package/dist/utils/searchAlgorithms.d.ts.map +1 -0
- package/dist/utils/searchAlgorithms.js +167 -0
- package/dist/utils/searchCache.d.ts +108 -0
- package/dist/utils/searchCache.d.ts.map +1 -0
- package/dist/utils/taskScheduler.d.ts +290 -0
- package/dist/utils/taskScheduler.d.ts.map +1 -0
- package/dist/utils/taskScheduler.js +466 -0
- package/dist/workers/index.d.ts +12 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +9 -0
- package/dist/workers/levenshteinWorker.d.ts +60 -0
- package/dist/workers/levenshteinWorker.d.ts.map +1 -0
- package/dist/workers/levenshteinWorker.js +98 -0
- package/package.json +17 -4
- package/dist/__tests__/edge-cases/edge-cases.test.js +0 -406
- package/dist/__tests__/integration/workflows.test.js +0 -449
- package/dist/__tests__/performance/benchmarks.test.js +0 -413
- package/dist/__tests__/unit/core/EntityManager.test.js +0 -334
- package/dist/__tests__/unit/core/GraphStorage.test.js +0 -205
- package/dist/__tests__/unit/core/RelationManager.test.js +0 -274
- package/dist/__tests__/unit/features/CompressionManager.test.js +0 -350
- package/dist/__tests__/unit/search/BasicSearch.test.js +0 -311
- package/dist/__tests__/unit/search/BooleanSearch.test.js +0 -432
- package/dist/__tests__/unit/search/FuzzySearch.test.js +0 -448
- package/dist/__tests__/unit/search/RankedSearch.test.js +0 -379
- package/dist/__tests__/unit/utils/levenshtein.test.js +0 -77
- package/dist/core/KnowledgeGraphManager.js +0 -423
- package/dist/features/BackupManager.js +0 -311
- package/dist/features/ExportManager.js +0 -305
- package/dist/features/ImportExportManager.js +0 -50
- package/dist/features/ImportManager.js +0 -328
- package/dist/memory-saved-searches.jsonl +0 -0
- package/dist/memory-tag-aliases.jsonl +0 -0
- package/dist/types/analytics.types.js +0 -6
- package/dist/types/entity.types.js +0 -7
- package/dist/types/import-export.types.js +0 -7
- package/dist/types/search.types.js +0 -7
- package/dist/types/tag.types.js +0 -6
- package/dist/utils/dateUtils.js +0 -89
- package/dist/utils/filterUtils.js +0 -155
- package/dist/utils/levenshtein.js +0 -62
- package/dist/utils/pathUtils.js +0 -115
- package/dist/utils/responseFormatter.js +0 -55
- package/dist/utils/tagUtils.js +0 -107
- package/dist/utils/tfidf.js +0 -90
- package/dist/utils/validationHelper.js +0 -99
- package/dist/utils/validationUtils.js +0 -109
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RankedSearch Unit Tests
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { RankedSearch } from '../../../search/RankedSearch.js';
|
|
6
|
-
import { EntityManager } from '../../../core/EntityManager.js';
|
|
7
|
-
import { GraphStorage } from '../../../core/GraphStorage.js';
|
|
8
|
-
import { promises as fs } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { tmpdir } from 'os';
|
|
11
|
-
describe('RankedSearch', () => {
|
|
12
|
-
let storage;
|
|
13
|
-
let rankedSearch;
|
|
14
|
-
let entityManager;
|
|
15
|
-
let testDir;
|
|
16
|
-
let testFilePath;
|
|
17
|
-
beforeEach(async () => {
|
|
18
|
-
// Create unique temp directory for each test
|
|
19
|
-
testDir = join(tmpdir(), `ranked-search-test-${Date.now()}-${Math.random()}`);
|
|
20
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
21
|
-
testFilePath = join(testDir, 'test-graph.jsonl');
|
|
22
|
-
storage = new GraphStorage(testFilePath);
|
|
23
|
-
rankedSearch = new RankedSearch(storage);
|
|
24
|
-
entityManager = new EntityManager(storage);
|
|
25
|
-
// Create test data
|
|
26
|
-
await entityManager.createEntities([
|
|
27
|
-
{
|
|
28
|
-
name: 'Alice',
|
|
29
|
-
entityType: 'person',
|
|
30
|
-
observations: ['Software engineer', 'Loves Python programming', 'Works on AI projects'],
|
|
31
|
-
tags: ['engineering', 'python', 'ai'],
|
|
32
|
-
importance: 9,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'Bob',
|
|
36
|
-
entityType: 'person',
|
|
37
|
-
observations: ['Product manager', 'Leads roadmap planning', 'Python enthusiast'],
|
|
38
|
-
tags: ['product', 'management', 'python'],
|
|
39
|
-
importance: 8,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: 'Charlie',
|
|
43
|
-
entityType: 'person',
|
|
44
|
-
observations: ['Designer', 'Creates beautiful UIs', 'Expert in Figma'],
|
|
45
|
-
tags: ['design', 'ui'],
|
|
46
|
-
importance: 7,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: 'Project_Python',
|
|
50
|
-
entityType: 'project',
|
|
51
|
-
observations: ['Internal Python automation tool', 'Used by engineering team'],
|
|
52
|
-
tags: ['engineering', 'automation', 'python'],
|
|
53
|
-
importance: 10,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'Project_Design',
|
|
57
|
-
entityType: 'project',
|
|
58
|
-
observations: ['Design system project', 'Component library'],
|
|
59
|
-
tags: ['design', 'ui'],
|
|
60
|
-
importance: 8,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'Company',
|
|
64
|
-
entityType: 'organization',
|
|
65
|
-
observations: ['Tech startup', 'AI-focused company', 'Python-first culture'],
|
|
66
|
-
tags: ['business', 'ai', 'python'],
|
|
67
|
-
importance: 10,
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
70
|
-
});
|
|
71
|
-
afterEach(async () => {
|
|
72
|
-
// Clean up test files
|
|
73
|
-
try {
|
|
74
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Ignore cleanup errors
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
describe('TF-IDF Scoring and Ranking', () => {
|
|
81
|
-
it('should rank results by relevance score', async () => {
|
|
82
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
83
|
-
expect(results.length).toBeGreaterThan(0);
|
|
84
|
-
// Results should be sorted by score descending
|
|
85
|
-
for (let i = 1; i < results.length; i++) {
|
|
86
|
-
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
it('should give higher scores to terms appearing in multiple fields', async () => {
|
|
90
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
91
|
-
// Find Alice (has "Python" in name and observations)
|
|
92
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
93
|
-
expect(aliceResult).toBeDefined();
|
|
94
|
-
expect(aliceResult.score).toBeGreaterThan(0);
|
|
95
|
-
});
|
|
96
|
-
it('should prioritize rare terms over common terms (IDF)', async () => {
|
|
97
|
-
// "Figma" appears only once (Charlie), "Python" appears multiple times
|
|
98
|
-
const figmaResults = await rankedSearch.searchNodesRanked('Figma');
|
|
99
|
-
const pythonResults = await rankedSearch.searchNodesRanked('Python');
|
|
100
|
-
// Figma should have a higher IDF (rarer term)
|
|
101
|
-
const figmaResult = figmaResults[0];
|
|
102
|
-
expect(figmaResult).toBeDefined();
|
|
103
|
-
expect(figmaResult.entity.name).toBe('Charlie');
|
|
104
|
-
// Python appears in multiple entities
|
|
105
|
-
expect(pythonResults.length).toBeGreaterThan(1);
|
|
106
|
-
});
|
|
107
|
-
it('should handle multi-term queries with combined scores', async () => {
|
|
108
|
-
const results = await rankedSearch.searchNodesRanked('Python engineer');
|
|
109
|
-
expect(results.length).toBeGreaterThan(0);
|
|
110
|
-
// Alice should rank highly (has both "Python" and "engineer")
|
|
111
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
112
|
-
expect(aliceResult).toBeDefined();
|
|
113
|
-
expect(aliceResult.score).toBeGreaterThan(0);
|
|
114
|
-
});
|
|
115
|
-
it('should only include entities with non-zero scores', async () => {
|
|
116
|
-
const results = await rankedSearch.searchNodesRanked('NonExistentTerm');
|
|
117
|
-
expect(results).toHaveLength(0);
|
|
118
|
-
});
|
|
119
|
-
it('should calculate scores based on term frequency', async () => {
|
|
120
|
-
// Create entity with repeated term
|
|
121
|
-
await entityManager.createEntities([
|
|
122
|
-
{
|
|
123
|
-
name: 'Repetitive',
|
|
124
|
-
entityType: 'test',
|
|
125
|
-
observations: ['Python Python Python', 'More Python content'],
|
|
126
|
-
},
|
|
127
|
-
]);
|
|
128
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
129
|
-
// Should have non-zero scores for all matches
|
|
130
|
-
expect(results.every(r => r.score > 0)).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('Matched Fields Tracking', () => {
|
|
134
|
-
it('should track name field matches', async () => {
|
|
135
|
-
const results = await rankedSearch.searchNodesRanked('Alice');
|
|
136
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
137
|
-
expect(aliceResult).toBeDefined();
|
|
138
|
-
expect(aliceResult.matchedFields.name).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
it('should track entityType field matches', async () => {
|
|
141
|
-
const results = await rankedSearch.searchNodesRanked('person');
|
|
142
|
-
expect(results.length).toBeGreaterThan(0);
|
|
143
|
-
results.forEach(result => {
|
|
144
|
-
if (result.entity.entityType === 'person') {
|
|
145
|
-
expect(result.matchedFields.entityType).toBe(true);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
it('should track observation matches with matched content', async () => {
|
|
150
|
-
const results = await rankedSearch.searchNodesRanked('engineer');
|
|
151
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
152
|
-
expect(aliceResult).toBeDefined();
|
|
153
|
-
expect(aliceResult.matchedFields.observations).toBeDefined();
|
|
154
|
-
expect(aliceResult.matchedFields.observations.length).toBeGreaterThan(0);
|
|
155
|
-
expect(aliceResult.matchedFields.observations[0]).toContain('engineer');
|
|
156
|
-
});
|
|
157
|
-
it('should track matches across multiple fields', async () => {
|
|
158
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
159
|
-
// Alice has "Python" in observations
|
|
160
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
161
|
-
expect(aliceResult).toBeDefined();
|
|
162
|
-
expect(aliceResult.matchedFields.observations).toBeDefined();
|
|
163
|
-
// Project_Python has "Python" in name and observations
|
|
164
|
-
const projectResult = results.find(r => r.entity.name === 'Project_Python');
|
|
165
|
-
expect(projectResult).toBeDefined();
|
|
166
|
-
expect(projectResult.matchedFields.name).toBe(true);
|
|
167
|
-
expect(projectResult.matchedFields.observations).toBeDefined();
|
|
168
|
-
});
|
|
169
|
-
it('should handle case-insensitive matching', async () => {
|
|
170
|
-
const results = await rankedSearch.searchNodesRanked('PYTHON');
|
|
171
|
-
expect(results.length).toBeGreaterThan(0);
|
|
172
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
173
|
-
expect(aliceResult).toBeDefined();
|
|
174
|
-
expect(aliceResult.matchedFields.observations).toBeDefined();
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('Tag Filtering', () => {
|
|
178
|
-
it('should filter by single tag', async () => {
|
|
179
|
-
const results = await rankedSearch.searchNodesRanked('engineer', ['python']);
|
|
180
|
-
expect(results.length).toBeGreaterThan(0);
|
|
181
|
-
results.forEach(result => {
|
|
182
|
-
expect(result.entity.tags).toContain('python');
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
it('should filter by multiple tags (OR logic)', async () => {
|
|
186
|
-
const results = await rankedSearch.searchNodesRanked('project', ['python', 'design']);
|
|
187
|
-
expect(results.length).toBeGreaterThan(0);
|
|
188
|
-
results.forEach(result => {
|
|
189
|
-
const hasPython = result.entity.tags?.includes('python');
|
|
190
|
-
const hasDesign = result.entity.tags?.includes('design');
|
|
191
|
-
expect(hasPython || hasDesign).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
it('should combine tag filter with text search', async () => {
|
|
195
|
-
const results = await rankedSearch.searchNodesRanked('engineer', ['python']);
|
|
196
|
-
expect(results.length).toBeGreaterThan(0);
|
|
197
|
-
results.forEach(result => {
|
|
198
|
-
expect(result.entity.tags).toContain('python');
|
|
199
|
-
expect(result.score).toBeGreaterThan(0);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
it('should exclude entities without matching tags', async () => {
|
|
203
|
-
const results = await rankedSearch.searchNodesRanked('test', ['nonexistent']);
|
|
204
|
-
expect(results).toHaveLength(0);
|
|
205
|
-
});
|
|
206
|
-
it('should handle entities without tags when filtering', async () => {
|
|
207
|
-
await entityManager.createEntities([
|
|
208
|
-
{ name: 'NoTags', entityType: 'test', observations: ['Test observation'] },
|
|
209
|
-
]);
|
|
210
|
-
const results = await rankedSearch.searchNodesRanked('', ['python']);
|
|
211
|
-
expect(results.every(r => r.entity.name !== 'NoTags')).toBe(true);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
describe('Importance Filtering', () => {
|
|
215
|
-
it('should filter by minimum importance', async () => {
|
|
216
|
-
const results = await rankedSearch.searchNodesRanked('project', undefined, 9);
|
|
217
|
-
// Should return Project_Python (10), Company (10), Alice (9 - has "projects" in observations)
|
|
218
|
-
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
219
|
-
results.forEach(result => {
|
|
220
|
-
expect(result.entity.importance).toBeGreaterThanOrEqual(9);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
it('should filter by maximum importance', async () => {
|
|
224
|
-
const results = await rankedSearch.searchNodesRanked('person', undefined, undefined, 8);
|
|
225
|
-
expect(results.length).toBeGreaterThan(0);
|
|
226
|
-
results.forEach(result => {
|
|
227
|
-
expect(result.entity.importance).toBeLessThanOrEqual(8);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
it('should filter by importance range', async () => {
|
|
231
|
-
const results = await rankedSearch.searchNodesRanked('project', undefined, 8, 9);
|
|
232
|
-
expect(results.length).toBeGreaterThan(0);
|
|
233
|
-
results.forEach(result => {
|
|
234
|
-
expect(result.entity.importance).toBeGreaterThanOrEqual(8);
|
|
235
|
-
expect(result.entity.importance).toBeLessThanOrEqual(9);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
it('should exclude entities without importance when filtering', async () => {
|
|
239
|
-
await entityManager.createEntities([
|
|
240
|
-
{ name: 'NoImportance', entityType: 'test', observations: ['Test'] },
|
|
241
|
-
]);
|
|
242
|
-
const results = await rankedSearch.searchNodesRanked('test', undefined, 5);
|
|
243
|
-
expect(results.every(r => r.entity.name !== 'NoImportance')).toBe(true);
|
|
244
|
-
});
|
|
245
|
-
it('should combine importance filter with text search', async () => {
|
|
246
|
-
const results = await rankedSearch.searchNodesRanked('engineer', undefined, 8);
|
|
247
|
-
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
248
|
-
results.forEach(result => {
|
|
249
|
-
expect(result.entity.importance).toBeGreaterThanOrEqual(8);
|
|
250
|
-
expect(result.score).toBeGreaterThan(0);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
describe('Search Limits', () => {
|
|
255
|
-
it('should use default limit of 50', async () => {
|
|
256
|
-
// Create more than 50 entities
|
|
257
|
-
const manyEntities = Array.from({ length: 60 }, (_, i) => ({
|
|
258
|
-
name: `Entity_${i}`,
|
|
259
|
-
entityType: 'test',
|
|
260
|
-
observations: ['Contains searchterm for testing'],
|
|
261
|
-
}));
|
|
262
|
-
await entityManager.createEntities(manyEntities);
|
|
263
|
-
const results = await rankedSearch.searchNodesRanked('searchterm');
|
|
264
|
-
expect(results.length).toBeLessThanOrEqual(50);
|
|
265
|
-
});
|
|
266
|
-
it('should respect custom limit', async () => {
|
|
267
|
-
const results = await rankedSearch.searchNodesRanked('test', undefined, undefined, undefined, 3);
|
|
268
|
-
expect(results.length).toBeLessThanOrEqual(3);
|
|
269
|
-
});
|
|
270
|
-
it('should enforce maximum limit of 200', async () => {
|
|
271
|
-
// Try to request 500 results
|
|
272
|
-
const results = await rankedSearch.searchNodesRanked('searchterm', undefined, undefined, undefined, 500);
|
|
273
|
-
// Should be capped at 200
|
|
274
|
-
expect(results.length).toBeLessThanOrEqual(200);
|
|
275
|
-
});
|
|
276
|
-
it('should handle limit smaller than result count', async () => {
|
|
277
|
-
const results = await rankedSearch.searchNodesRanked('Python', undefined, undefined, undefined, 2);
|
|
278
|
-
expect(results.length).toBeLessThanOrEqual(2);
|
|
279
|
-
// Should still be sorted by score
|
|
280
|
-
if (results.length === 2) {
|
|
281
|
-
expect(results[0].score).toBeGreaterThanOrEqual(results[1].score);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
describe('Edge Cases', () => {
|
|
286
|
-
it('should handle empty query string', async () => {
|
|
287
|
-
const results = await rankedSearch.searchNodesRanked('');
|
|
288
|
-
// Empty query has no terms, so no TF-IDF scores
|
|
289
|
-
expect(results).toHaveLength(0);
|
|
290
|
-
});
|
|
291
|
-
it('should handle entities with empty observations', async () => {
|
|
292
|
-
await entityManager.createEntities([
|
|
293
|
-
{ name: 'EmptyObs', entityType: 'test', observations: [] },
|
|
294
|
-
]);
|
|
295
|
-
const results = await rankedSearch.searchNodesRanked('test');
|
|
296
|
-
// Should match entityType
|
|
297
|
-
const emptyObsResult = results.find(r => r.entity.name === 'EmptyObs');
|
|
298
|
-
expect(emptyObsResult).toBeDefined();
|
|
299
|
-
expect(emptyObsResult.matchedFields.entityType).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
it('should handle special characters and punctuation', async () => {
|
|
302
|
-
await entityManager.createEntities([
|
|
303
|
-
{
|
|
304
|
-
name: 'Special-Chars',
|
|
305
|
-
entityType: 'test',
|
|
306
|
-
observations: ['Has special! characters? and, punctuation.'],
|
|
307
|
-
},
|
|
308
|
-
]);
|
|
309
|
-
const results = await rankedSearch.searchNodesRanked('special characters');
|
|
310
|
-
expect(results.length).toBeGreaterThan(0);
|
|
311
|
-
const specialResult = results.find(r => r.entity.name === 'Special-Chars');
|
|
312
|
-
expect(specialResult).toBeDefined();
|
|
313
|
-
});
|
|
314
|
-
it('should handle very long observation texts', async () => {
|
|
315
|
-
const longText = 'word '.repeat(200) + 'unique';
|
|
316
|
-
await entityManager.createEntities([
|
|
317
|
-
{ name: 'LongText', entityType: 'test', observations: [longText] },
|
|
318
|
-
]);
|
|
319
|
-
const results = await rankedSearch.searchNodesRanked('unique');
|
|
320
|
-
expect(results.length).toBeGreaterThan(0);
|
|
321
|
-
const longTextResult = results.find(r => r.entity.name === 'LongText');
|
|
322
|
-
expect(longTextResult).toBeDefined();
|
|
323
|
-
});
|
|
324
|
-
it('should handle unicode characters', async () => {
|
|
325
|
-
await entityManager.createEntities([
|
|
326
|
-
{ name: 'Café', entityType: 'location', observations: ['Paris café with déjà vu'] },
|
|
327
|
-
]);
|
|
328
|
-
const results = await rankedSearch.searchNodesRanked('café');
|
|
329
|
-
expect(results.length).toBeGreaterThan(0);
|
|
330
|
-
const cafeResult = results.find(r => r.entity.name === 'Café');
|
|
331
|
-
expect(cafeResult).toBeDefined();
|
|
332
|
-
});
|
|
333
|
-
it('should handle queries with only stopwords', async () => {
|
|
334
|
-
// Tokenization removes punctuation but not stopwords
|
|
335
|
-
const results = await rankedSearch.searchNodesRanked('the a an');
|
|
336
|
-
// These common words might match, but scores will be low due to IDF
|
|
337
|
-
// Just verify no errors occur
|
|
338
|
-
expect(Array.isArray(results)).toBe(true);
|
|
339
|
-
});
|
|
340
|
-
it('should handle combined filters with no matches', async () => {
|
|
341
|
-
const results = await rankedSearch.searchNodesRanked('NonExistent', ['nonexistent-tag'], 100, // impossible importance
|
|
342
|
-
200);
|
|
343
|
-
expect(results).toHaveLength(0);
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
describe('Return Value Structure', () => {
|
|
347
|
-
it('should return SearchResult objects with required fields', async () => {
|
|
348
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
349
|
-
expect(results.length).toBeGreaterThan(0);
|
|
350
|
-
results.forEach(result => {
|
|
351
|
-
expect(result).toHaveProperty('entity');
|
|
352
|
-
expect(result).toHaveProperty('score');
|
|
353
|
-
expect(result).toHaveProperty('matchedFields');
|
|
354
|
-
expect(typeof result.score).toBe('number');
|
|
355
|
-
expect(result.score).toBeGreaterThan(0);
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
it('should include complete entity objects', async () => {
|
|
359
|
-
const results = await rankedSearch.searchNodesRanked('Alice');
|
|
360
|
-
const aliceResult = results.find(r => r.entity.name === 'Alice');
|
|
361
|
-
expect(aliceResult).toBeDefined();
|
|
362
|
-
expect(aliceResult.entity).toHaveProperty('name');
|
|
363
|
-
expect(aliceResult.entity).toHaveProperty('entityType');
|
|
364
|
-
expect(aliceResult.entity).toHaveProperty('observations');
|
|
365
|
-
expect(aliceResult.entity).toHaveProperty('tags');
|
|
366
|
-
expect(aliceResult.entity).toHaveProperty('importance');
|
|
367
|
-
});
|
|
368
|
-
it('should have matchedFields as optional properties', async () => {
|
|
369
|
-
const results = await rankedSearch.searchNodesRanked('Python');
|
|
370
|
-
results.forEach(result => {
|
|
371
|
-
const fields = result.matchedFields;
|
|
372
|
-
// At least one field should be matched
|
|
373
|
-
expect(fields.name === true ||
|
|
374
|
-
fields.entityType === true ||
|
|
375
|
-
(fields.observations && fields.observations.length > 0)).toBe(true);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { levenshteinDistance } from '../../../utils/levenshtein.js';
|
|
3
|
-
describe('levenshteinDistance', () => {
|
|
4
|
-
describe('identical strings', () => {
|
|
5
|
-
it('should return 0 for identical strings', () => {
|
|
6
|
-
expect(levenshteinDistance('hello', 'hello')).toBe(0);
|
|
7
|
-
expect(levenshteinDistance('test', 'test')).toBe(0);
|
|
8
|
-
expect(levenshteinDistance('', '')).toBe(0);
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
describe('empty strings', () => {
|
|
12
|
-
it('should return length when one string is empty', () => {
|
|
13
|
-
expect(levenshteinDistance('', 'hello')).toBe(5);
|
|
14
|
-
expect(levenshteinDistance('world', '')).toBe(5);
|
|
15
|
-
expect(levenshteinDistance('', '')).toBe(0);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
describe('single character difference', () => {
|
|
19
|
-
it('should return 1 for single insertion', () => {
|
|
20
|
-
expect(levenshteinDistance('cat', 'cats')).toBe(1);
|
|
21
|
-
});
|
|
22
|
-
it('should return 1 for single deletion', () => {
|
|
23
|
-
expect(levenshteinDistance('cats', 'cat')).toBe(1);
|
|
24
|
-
});
|
|
25
|
-
it('should return 1 for single substitution', () => {
|
|
26
|
-
expect(levenshteinDistance('cat', 'bat')).toBe(1);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
describe('multiple edits', () => {
|
|
30
|
-
it('should calculate distance for multiple edits', () => {
|
|
31
|
-
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
|
|
32
|
-
expect(levenshteinDistance('saturday', 'sunday')).toBe(3);
|
|
33
|
-
expect(levenshteinDistance('book', 'back')).toBe(2);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe('completely different strings', () => {
|
|
37
|
-
it('should handle completely different strings', () => {
|
|
38
|
-
expect(levenshteinDistance('abc', 'xyz')).toBe(3);
|
|
39
|
-
expect(levenshteinDistance('hello', 'world')).toBe(4);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('different lengths', () => {
|
|
43
|
-
it('should handle strings of different lengths', () => {
|
|
44
|
-
expect(levenshteinDistance('short', 'muchlonger')).toBe(8);
|
|
45
|
-
expect(levenshteinDistance('a', 'abc')).toBe(2);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('case sensitivity', () => {
|
|
49
|
-
it('should be case-sensitive', () => {
|
|
50
|
-
expect(levenshteinDistance('Hello', 'hello')).toBe(1);
|
|
51
|
-
expect(levenshteinDistance('WORLD', 'world')).toBe(5);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
describe('unicode characters', () => {
|
|
55
|
-
it('should handle unicode characters', () => {
|
|
56
|
-
expect(levenshteinDistance('café', 'cafe')).toBe(1);
|
|
57
|
-
expect(levenshteinDistance('🙂', '🙃')).toBe(1);
|
|
58
|
-
expect(levenshteinDistance('test', 'tëst')).toBe(1);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
describe('performance edge cases', () => {
|
|
62
|
-
it('should handle long strings efficiently', () => {
|
|
63
|
-
const longStr1 = 'a'.repeat(100);
|
|
64
|
-
const longStr2 = 'b'.repeat(100);
|
|
65
|
-
// Should complete without timeout
|
|
66
|
-
const distance = levenshteinDistance(longStr1, longStr2);
|
|
67
|
-
expect(distance).toBe(100);
|
|
68
|
-
});
|
|
69
|
-
it('should handle moderately long similar strings', () => {
|
|
70
|
-
const str1 = 'abcdefghij'.repeat(10);
|
|
71
|
-
const str2 = 'abcdefghik'.repeat(10);
|
|
72
|
-
// Last char different in each repeat
|
|
73
|
-
const distance = levenshteinDistance(str1, str2);
|
|
74
|
-
expect(distance).toBe(10);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|