@softerist/heuristic-mcp 2.1.47 → 3.0.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/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -75
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
// Mock fs
|
|
5
|
+
vi.mock('fs/promises');
|
|
6
|
+
vi.mock('../lib/json-writer.js', () => ({
|
|
7
|
+
StreamingJsonWriter: class {
|
|
8
|
+
writeStart() { return Promise.resolve(); }
|
|
9
|
+
writeItem() {}
|
|
10
|
+
writeEnd() { return Promise.resolve(); }
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Define mocks at top level to ensure stability across module resets
|
|
15
|
+
const mockIndex = {
|
|
16
|
+
initIndex: vi.fn(),
|
|
17
|
+
readIndexSync: vi.fn(),
|
|
18
|
+
writeIndexSync: vi.fn(),
|
|
19
|
+
addPoint: vi.fn(),
|
|
20
|
+
setEf: vi.fn(),
|
|
21
|
+
searchKnn: vi.fn().mockReturnValue({ distances: [], neighbors: [] }),
|
|
22
|
+
getMaxElements: vi.fn().mockReturnValue(100),
|
|
23
|
+
getCurrentCount: vi.fn().mockReturnValue(0),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Use a regular function for constructor
|
|
27
|
+
const mockConstructor = vi.fn(function () {
|
|
28
|
+
return mockIndex;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Mock hnswlib-node with stable constructor
|
|
32
|
+
vi.mock('hnswlib-node', () => {
|
|
33
|
+
return {
|
|
34
|
+
default: {
|
|
35
|
+
HierarchicalNSW: mockConstructor,
|
|
36
|
+
},
|
|
37
|
+
HierarchicalNSW: mockConstructor,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('EmbeddingsCache Error Handling', () => {
|
|
42
|
+
let cache;
|
|
43
|
+
let config;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
vi.clearAllMocks(); // Clear call history
|
|
47
|
+
vi.resetModules(); // Reset module cache
|
|
48
|
+
|
|
49
|
+
// Reset default implementations for consistency
|
|
50
|
+
mockIndex.initIndex.mockImplementation(() => undefined);
|
|
51
|
+
mockIndex.readIndexSync.mockImplementation(() => true);
|
|
52
|
+
mockIndex.addPoint.mockImplementation(() => undefined);
|
|
53
|
+
mockIndex.writeIndexSync.mockImplementation(() => undefined);
|
|
54
|
+
|
|
55
|
+
// Dynamic import to pick up fresh mock and reset module state
|
|
56
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
57
|
+
|
|
58
|
+
config = {
|
|
59
|
+
enableCache: true,
|
|
60
|
+
cacheDirectory: '/mock/cache',
|
|
61
|
+
fileExtensions: ['js'],
|
|
62
|
+
embeddingModel: 'test-model',
|
|
63
|
+
annEnabled: true,
|
|
64
|
+
annMinChunks: 1,
|
|
65
|
+
annMetric: 'cosine',
|
|
66
|
+
annM: 48,
|
|
67
|
+
annEfConstruction: 200,
|
|
68
|
+
annEfSearch: 10,
|
|
69
|
+
verbose: true,
|
|
70
|
+
};
|
|
71
|
+
cache = new EmbeddingsCache(config);
|
|
72
|
+
|
|
73
|
+
// Spy on console warn/error but verify calls
|
|
74
|
+
vi.spyOn(console, 'warn');
|
|
75
|
+
vi.spyOn(console, 'error');
|
|
76
|
+
|
|
77
|
+
fs.readFile.mockResolvedValue(null);
|
|
78
|
+
fs.writeFile.mockResolvedValue();
|
|
79
|
+
fs.mkdir.mockResolvedValue();
|
|
80
|
+
fs.rm.mockResolvedValue();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.clearAllMocks();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('HNSW Initialization Retries', () => {
|
|
88
|
+
it('should retry initIndex with different parameters on failure', async () => {
|
|
89
|
+
mockIndex.initIndex
|
|
90
|
+
.mockImplementationOnce(() => {
|
|
91
|
+
throw new Error('Fail 1');
|
|
92
|
+
})
|
|
93
|
+
.mockImplementationOnce(() => {
|
|
94
|
+
throw new Error('Fail 2');
|
|
95
|
+
})
|
|
96
|
+
.mockImplementationOnce(() => Promise.resolve());
|
|
97
|
+
|
|
98
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
99
|
+
await cache.ensureAnnIndex();
|
|
100
|
+
|
|
101
|
+
expect(mockIndex.initIndex).toHaveBeenCalledTimes(3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should fallback to linear search if all initIndex attempts fail', async () => {
|
|
105
|
+
mockIndex.initIndex.mockImplementation(() => {
|
|
106
|
+
throw new Error('Fail Always');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
110
|
+
const index = await cache.ensureAnnIndex();
|
|
111
|
+
|
|
112
|
+
expect(index).toBeNull();
|
|
113
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
114
|
+
expect.stringContaining('Failed to build ANN index')
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('HNSW Read Retries', () => {
|
|
120
|
+
it('should retry readIndexSync on failure', async () => {
|
|
121
|
+
fs.readFile.mockResolvedValue(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
version: 1,
|
|
124
|
+
embeddingModel: 'test-model',
|
|
125
|
+
dim: 1,
|
|
126
|
+
count: 1,
|
|
127
|
+
metric: 'cosine',
|
|
128
|
+
m: 48,
|
|
129
|
+
efConstruction: 200,
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
mockIndex.readIndexSync
|
|
134
|
+
.mockImplementationOnce(() => {
|
|
135
|
+
throw new Error('Fail 1');
|
|
136
|
+
})
|
|
137
|
+
.mockReturnValue(true);
|
|
138
|
+
|
|
139
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
140
|
+
const index = await cache.ensureAnnIndex();
|
|
141
|
+
|
|
142
|
+
expect(index).toBeDefined();
|
|
143
|
+
expect(mockIndex.readIndexSync).toHaveBeenCalledTimes(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should rebuild index if readIndexSync fails completely', async () => {
|
|
147
|
+
fs.readFile.mockResolvedValue(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
version: 1,
|
|
150
|
+
embeddingModel: 'test-model',
|
|
151
|
+
dim: 1,
|
|
152
|
+
count: 1,
|
|
153
|
+
metric: 'cosine',
|
|
154
|
+
m: 48,
|
|
155
|
+
efConstruction: 200,
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
mockIndex.readIndexSync.mockImplementation(() => {
|
|
160
|
+
throw new Error('Read Fail');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
164
|
+
|
|
165
|
+
const index = await cache.ensureAnnIndex();
|
|
166
|
+
|
|
167
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
168
|
+
expect.stringContaining('Failed to load ANN index')
|
|
169
|
+
);
|
|
170
|
+
expect(index).toBeDefined();
|
|
171
|
+
expect(mockIndex.initIndex).toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('File System Errors', () => {
|
|
176
|
+
it('should handle fs errors during load', async () => {
|
|
177
|
+
fs.mkdir.mockRejectedValue(new Error('Permission denied'));
|
|
178
|
+
await cache.load();
|
|
179
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
180
|
+
'[Cache] Failed to load cache:',
|
|
181
|
+
'Permission denied'
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle fs errors during save', async () => {
|
|
186
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
187
|
+
fs.mkdir.mockRejectedValue(new Error('Read-only file system'));
|
|
188
|
+
await cache.save();
|
|
189
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
190
|
+
'[Cache] Failed to save cache:',
|
|
191
|
+
'Read-only file system'
|
|
192
|
+
);
|
|
193
|
+
expect(cache.isSaving).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle clear cache errors', async () => {
|
|
197
|
+
fs.rm.mockRejectedValue(new Error('Locked'));
|
|
198
|
+
await expect(cache.clear()).rejects.toThrow('Locked');
|
|
199
|
+
expect(console.error).toHaveBeenCalledWith('[Cache] Failed to clear cache:', 'Locked');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle call graph save errors', async () => {
|
|
203
|
+
fs.mkdir.mockResolvedValue();
|
|
204
|
+
fs.writeFile.mockImplementation((path) => {
|
|
205
|
+
if (path.includes('call-graph')) return Promise.reject(new Error('Graph Write Fail'));
|
|
206
|
+
return Promise.resolve();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
cache.setFileCallData('f.js', {});
|
|
210
|
+
await cache.save();
|
|
211
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
212
|
+
'[Cache] Failed to save cache:',
|
|
213
|
+
'Graph Write Fail'
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('ANN Rebuild Edge Cases', () => {
|
|
219
|
+
it('should handle metadata mismatch forcing rebuild', async () => {
|
|
220
|
+
fs.readFile.mockResolvedValue(
|
|
221
|
+
JSON.stringify({
|
|
222
|
+
version: 999, // Mismatch
|
|
223
|
+
embeddingModel: 'test-model',
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
228
|
+
const index = await cache.ensureAnnIndex();
|
|
229
|
+
|
|
230
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('version mismatch'));
|
|
231
|
+
expect(mockIndex.initIndex).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should handle addPoint failure during build', async () => {
|
|
235
|
+
mockIndex.addPoint.mockImplementation(() => {
|
|
236
|
+
throw new Error('Add Fail');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
240
|
+
|
|
241
|
+
const index = await cache.ensureAnnIndex();
|
|
242
|
+
|
|
243
|
+
expect(index).toBeNull();
|
|
244
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
245
|
+
expect.stringContaining('Failed to build ANN index')
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle save ANN index failure', async () => {
|
|
250
|
+
mockIndex.writeIndexSync.mockImplementation(() => {
|
|
251
|
+
throw new Error('Write Fail');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
cache.vectorStore = [{ vector: [1] }];
|
|
255
|
+
|
|
256
|
+
const index = await cache.ensureAnnIndex();
|
|
257
|
+
|
|
258
|
+
expect(index).toBeDefined();
|
|
259
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
260
|
+
expect.stringContaining('Failed to save ANN index')
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const makeConfig = (cacheDir, overrides = {}) => ({
|
|
7
|
+
cacheDirectory: cacheDir,
|
|
8
|
+
searchDirectory: cacheDir,
|
|
9
|
+
enableCache: true,
|
|
10
|
+
callGraphEnabled: true,
|
|
11
|
+
embeddingModel: 'test-model',
|
|
12
|
+
fileExtensions: ['js'],
|
|
13
|
+
excludePatterns: [],
|
|
14
|
+
annEnabled: true,
|
|
15
|
+
annMinChunks: 1,
|
|
16
|
+
annMetric: 'cosine',
|
|
17
|
+
annM: 48,
|
|
18
|
+
annEfConstruction: 200,
|
|
19
|
+
annEfSearch: 10,
|
|
20
|
+
annIndexCache: true,
|
|
21
|
+
verbose: true,
|
|
22
|
+
...overrides,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function withTempDir(testFn) {
|
|
26
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'heuristic-cache-extra-'));
|
|
27
|
+
try {
|
|
28
|
+
await testFn(dir);
|
|
29
|
+
} finally {
|
|
30
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('EmbeddingsCache additional coverage', () => {
|
|
35
|
+
let warnSpy;
|
|
36
|
+
let infoSpy;
|
|
37
|
+
let errorSpy;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
41
|
+
infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
42
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
43
|
+
vi.resetModules();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
warnSpy.mockRestore();
|
|
48
|
+
infoSpy.mockRestore();
|
|
49
|
+
errorSpy.mockRestore();
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
vi.resetModules();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('logs missing metadata, version mismatch, model mismatch, and call-graph loads', async () => {
|
|
55
|
+
await withTempDir(async (dir) => {
|
|
56
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
57
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(path.join(dir, 'embeddings.json'), JSON.stringify([]));
|
|
60
|
+
await fs.writeFile(path.join(dir, 'file-hashes.json'), JSON.stringify({}));
|
|
61
|
+
|
|
62
|
+
await cache.load();
|
|
63
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Missing cache metadata'));
|
|
64
|
+
|
|
65
|
+
await fs.writeFile(
|
|
66
|
+
path.join(dir, 'meta.json'),
|
|
67
|
+
JSON.stringify({ version: 999, embeddingModel: 'test-model' })
|
|
68
|
+
);
|
|
69
|
+
await cache.load();
|
|
70
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Cache version mismatch'));
|
|
71
|
+
|
|
72
|
+
await fs.writeFile(
|
|
73
|
+
path.join(dir, 'meta.json'),
|
|
74
|
+
JSON.stringify({ version: 1, embeddingModel: 'other-model' })
|
|
75
|
+
);
|
|
76
|
+
await cache.load();
|
|
77
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Embedding model changed'));
|
|
78
|
+
|
|
79
|
+
await fs.writeFile(
|
|
80
|
+
path.join(dir, 'meta.json'),
|
|
81
|
+
JSON.stringify({ version: 1, embeddingModel: 'test-model' })
|
|
82
|
+
);
|
|
83
|
+
await fs.writeFile(path.join(dir, 'call-graph.json'), JSON.stringify({ 'a.js': {} }));
|
|
84
|
+
await cache.load();
|
|
85
|
+
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('Loaded call-graph data'));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('normalizes labels via Array.from and filters invalid ANN results', async () => {
|
|
90
|
+
await withTempDir(async (dir) => {
|
|
91
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
92
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
93
|
+
cache.vectorStore = [{ vector: [1, 2, 3] }];
|
|
94
|
+
const searchKnn = vi.fn().mockReturnValue({ labels: new Set([-1, 0, 5]) });
|
|
95
|
+
cache.annIndex = { searchKnn };
|
|
96
|
+
cache.annDirty = false;
|
|
97
|
+
|
|
98
|
+
const result = await cache.queryAnn([1, 2, 3], 3);
|
|
99
|
+
|
|
100
|
+
expect(result).toEqual([0]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('deletes file hashes and returns null for missing vectors', async () => {
|
|
105
|
+
await withTempDir(async (dir) => {
|
|
106
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
107
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
108
|
+
|
|
109
|
+
cache.setFileHash('a.js', 'hash');
|
|
110
|
+
cache.deleteFileHash('a.js');
|
|
111
|
+
expect(cache.getFileHash('a.js')).toBeUndefined();
|
|
112
|
+
|
|
113
|
+
cache.vectorStore = [{}];
|
|
114
|
+
expect(cache.getAnnVector(0)).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles invalid ANN metadata and config mismatch', async () => {
|
|
119
|
+
await withTempDir(async (dir) => {
|
|
120
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
121
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
122
|
+
cache.vectorStore = [{ vector: [1, 2, 3] }];
|
|
123
|
+
|
|
124
|
+
const metaFile = path.join(dir, 'ann-meta.json');
|
|
125
|
+
await fs.writeFile(metaFile, 'not-json');
|
|
126
|
+
await cache.loadAnnIndexFromDisk(class {}, 3);
|
|
127
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid ANN metadata'));
|
|
128
|
+
|
|
129
|
+
await fs.writeFile(
|
|
130
|
+
metaFile,
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
version: 1,
|
|
133
|
+
embeddingModel: 'test-model',
|
|
134
|
+
dim: 3,
|
|
135
|
+
count: 1,
|
|
136
|
+
metric: 'l2',
|
|
137
|
+
m: 48,
|
|
138
|
+
efConstruction: 200,
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
await cache.loadAnnIndexFromDisk(class {}, 3);
|
|
142
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ANN index config changed'));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('throws when building ANN index encounters missing vectors', async () => {
|
|
147
|
+
await withTempDir(async (dir) => {
|
|
148
|
+
vi.doMock('hnswlib-node', () => ({
|
|
149
|
+
HierarchicalNSW: class {
|
|
150
|
+
initIndex() {}
|
|
151
|
+
addPoint() {}
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
155
|
+
const cache = new EmbeddingsCache(makeConfig(dir, { annIndexCache: false }));
|
|
156
|
+
cache.vectorStore = [{ vector: [1, 2] }, { vector: null }];
|
|
157
|
+
|
|
158
|
+
const result = await cache.ensureAnnIndex();
|
|
159
|
+
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to build ANN index'));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('reports call graph stats', async () => {
|
|
166
|
+
await withTempDir(async (dir) => {
|
|
167
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
168
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
169
|
+
|
|
170
|
+
const stats = cache.getCallGraphStats();
|
|
171
|
+
|
|
172
|
+
expect(stats.enabled).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('logs hnswlib missing export errors', async () => {
|
|
177
|
+
await withTempDir(async (dir) => {
|
|
178
|
+
vi.doMock('hnswlib-node', () => ({
|
|
179
|
+
default: { HierarchicalNSW: null },
|
|
180
|
+
HierarchicalNSW: null,
|
|
181
|
+
}));
|
|
182
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
183
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
184
|
+
cache.vectorStore = [{ vector: [1, 2, 3] }];
|
|
185
|
+
|
|
186
|
+
const result = await cache.ensureAnnIndex();
|
|
187
|
+
|
|
188
|
+
expect(result).toBeNull();
|
|
189
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
190
|
+
expect.stringContaining('HierarchicalNSW export not found')
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('short-circuits load/save/clear when cache is disabled', async () => {
|
|
196
|
+
await withTempDir(async (dir) => {
|
|
197
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
198
|
+
const cache = new EmbeddingsCache(makeConfig(dir, { enableCache: false }));
|
|
199
|
+
const rmSpy = vi.spyOn(fs, 'rm').mockResolvedValue();
|
|
200
|
+
|
|
201
|
+
await cache.load();
|
|
202
|
+
await cache.save();
|
|
203
|
+
await cache.clear();
|
|
204
|
+
|
|
205
|
+
expect(rmSpy).not.toHaveBeenCalled();
|
|
206
|
+
rmSpy.mockRestore();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('normalizes empty and array ANN label results', async () => {
|
|
211
|
+
await withTempDir(async (dir) => {
|
|
212
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
213
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
214
|
+
cache.vectorStore = [{ vector: [1, 2, 3] }];
|
|
215
|
+
cache.annDirty = false;
|
|
216
|
+
|
|
217
|
+
cache.annIndex = { searchKnn: vi.fn().mockReturnValue(null) };
|
|
218
|
+
const emptyResult = await cache.queryAnn([1, 2, 3], 1);
|
|
219
|
+
expect(emptyResult).toEqual([]);
|
|
220
|
+
|
|
221
|
+
cache.annIndex = { searchKnn: vi.fn().mockReturnValue([0]) };
|
|
222
|
+
const arrayResult = await cache.queryAnn([1, 2, 3], 1);
|
|
223
|
+
expect(arrayResult).toEqual([0]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns existing annLoading promises and handles missing dims', async () => {
|
|
228
|
+
await withTempDir(async (dir) => {
|
|
229
|
+
vi.doMock('hnswlib-node', () => ({
|
|
230
|
+
HierarchicalNSW: class {},
|
|
231
|
+
default: { HierarchicalNSW: class {} },
|
|
232
|
+
}));
|
|
233
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
234
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
235
|
+
cache.vectorStore = [{}];
|
|
236
|
+
|
|
237
|
+
const loading = Promise.resolve('loading');
|
|
238
|
+
cache.annLoading = loading;
|
|
239
|
+
|
|
240
|
+
const result = await cache.ensureAnnIndex();
|
|
241
|
+
expect(result).toBe('loading');
|
|
242
|
+
|
|
243
|
+
cache.annLoading = null;
|
|
244
|
+
const dimResult = await cache.ensureAnnIndex();
|
|
245
|
+
expect(dimResult).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('handles missing ANN metadata and empty ANN builds', async () => {
|
|
250
|
+
await withTempDir(async (dir) => {
|
|
251
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
252
|
+
const cache = new EmbeddingsCache(makeConfig(dir));
|
|
253
|
+
|
|
254
|
+
const loaded = await cache.loadAnnIndexFromDisk(class {}, 3);
|
|
255
|
+
expect(loaded).toBe(false);
|
|
256
|
+
|
|
257
|
+
cache.vectorStore = [];
|
|
258
|
+
const built = await cache.buildAnnIndex(class {}, 3);
|
|
259
|
+
expect(built).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('uses default error message when JSON worker fails without error details (line 38)', async () => {
|
|
264
|
+
const { EventEmitter } = await import('events');
|
|
265
|
+
const Worker = vi.fn(function () {
|
|
266
|
+
const worker = new EventEmitter();
|
|
267
|
+
setImmediate(() => {
|
|
268
|
+
worker.emit('message', { ok: false });
|
|
269
|
+
});
|
|
270
|
+
return worker;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const fsMock = {
|
|
274
|
+
stat: vi.fn().mockResolvedValue({ size: 10 * 1024 * 1024 }),
|
|
275
|
+
readFile: vi.fn().mockResolvedValue(JSON.stringify({ version: 1, embeddingModel: 'test-model' })),
|
|
276
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
vi.doMock('worker_threads', () => ({ Worker }));
|
|
280
|
+
vi.doMock('fs/promises', () => ({ default: fsMock, ...fsMock }));
|
|
281
|
+
|
|
282
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
283
|
+
const cache = new EmbeddingsCache(makeConfig('/tmp'));
|
|
284
|
+
|
|
285
|
+
await cache.load();
|
|
286
|
+
|
|
287
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('JSON worker failed'));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('handles non-array store in setVectorStore (line 320)', async () => {
|
|
291
|
+
const { EmbeddingsCache } = await import('../lib/cache.js');
|
|
292
|
+
const cache = new EmbeddingsCache(makeConfig('/tmp'));
|
|
293
|
+
|
|
294
|
+
cache.setVectorStore(null);
|
|
295
|
+
expect(cache.vectorStore).toBeNull();
|
|
296
|
+
|
|
297
|
+
cache.setVectorStore({ not: 'an array' });
|
|
298
|
+
expect(cache.vectorStore).toEqual({ not: 'an array' });
|
|
299
|
+
});
|
|
300
|
+
});
|