@softerist/heuristic-mcp 2.1.46 → 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 -76
- 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,585 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
// Mock os.cpus
|
|
5
|
+
vi.mock('os', async () => {
|
|
6
|
+
const actual = await vi.importActual('os');
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
cpus: vi.fn().mockReturnValue(Array(4).fill({})),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let workerMode = 'ready';
|
|
14
|
+
|
|
15
|
+
// Mock Worker
|
|
16
|
+
vi.mock('worker_threads', () => {
|
|
17
|
+
class MockWorker {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.once = vi.fn((event, handler) => {
|
|
20
|
+
if (event === 'message') {
|
|
21
|
+
if (workerMode === 'ready') {
|
|
22
|
+
setImmediate(() => handler({ type: 'ready' }));
|
|
23
|
+
} else if (workerMode === 'error') {
|
|
24
|
+
setImmediate(() => handler({ type: 'error', error: 'boom' }));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (event === 'error' && workerMode === 'crash') {
|
|
28
|
+
setImmediate(() => handler(new Error('crash')));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
this.on = vi.fn();
|
|
32
|
+
this.off = vi.fn();
|
|
33
|
+
this.postMessage = vi.fn();
|
|
34
|
+
this.terminate = vi.fn().mockResolvedValue(undefined);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { Worker: MockWorker };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
41
|
+
import { CodebaseIndexer } from '../features/index-codebase.js';
|
|
42
|
+
import * as utils from '../lib/utils.js';
|
|
43
|
+
import fs from 'fs/promises';
|
|
44
|
+
import path from 'path';
|
|
45
|
+
import { Worker } from 'worker_threads';
|
|
46
|
+
|
|
47
|
+
// Store handlers
|
|
48
|
+
const handlers = {};
|
|
49
|
+
const mockWatcher = {
|
|
50
|
+
on: vi.fn((event, handler) => {
|
|
51
|
+
handlers[event] = handler;
|
|
52
|
+
return mockWatcher;
|
|
53
|
+
}),
|
|
54
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Mock dependencies
|
|
58
|
+
vi.mock('fs/promises');
|
|
59
|
+
vi.mock('chokidar', () => ({
|
|
60
|
+
default: {
|
|
61
|
+
watch: vi.fn(() => mockWatcher),
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
describe('CodebaseIndexer Branch Coverage', () => {
|
|
66
|
+
let indexer;
|
|
67
|
+
let mockEmbedder;
|
|
68
|
+
let mockCache;
|
|
69
|
+
let mockConfig;
|
|
70
|
+
let mockServer;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
// Ensure we start with real timers
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
workerMode = 'ready';
|
|
76
|
+
|
|
77
|
+
for (const key in handlers) delete handlers[key];
|
|
78
|
+
|
|
79
|
+
mockEmbedder = vi.fn().mockResolvedValue({ data: [0.1, 0.2, 0.3] });
|
|
80
|
+
mockCache = {
|
|
81
|
+
getFileHash: vi.fn(),
|
|
82
|
+
setFileHash: vi.fn(),
|
|
83
|
+
removeFileFromStore: vi.fn(),
|
|
84
|
+
addToStore: vi.fn(),
|
|
85
|
+
deleteFileHash: vi.fn(),
|
|
86
|
+
save: vi.fn(),
|
|
87
|
+
clearCallGraphData: vi.fn(),
|
|
88
|
+
getVectorStore: vi.fn().mockReturnValue([]),
|
|
89
|
+
setVectorStore: vi.fn(),
|
|
90
|
+
ensureAnnIndex: vi.fn().mockResolvedValue(null),
|
|
91
|
+
pruneCallGraphData: vi.fn(),
|
|
92
|
+
fileCallData: new Map(),
|
|
93
|
+
fileHashes: new Map(),
|
|
94
|
+
rebuildCallGraph: vi.fn(),
|
|
95
|
+
setFileCallData: vi.fn(),
|
|
96
|
+
getAnnVector: vi.fn().mockReturnValue(new Float32Array([0.1])),
|
|
97
|
+
setLastIndexDuration: vi.fn(),
|
|
98
|
+
setLastIndexStats: vi.fn(),
|
|
99
|
+
getFileHashKeys: vi.fn().mockImplementation(() => [...mockCache.fileHashes.keys()]),
|
|
100
|
+
setFileHashes: vi.fn((map) => { mockCache.fileHashes = map; }),
|
|
101
|
+
getFileCallDataKeys: vi.fn().mockImplementation(() => [...mockCache.fileCallData.keys()]),
|
|
102
|
+
setFileCallDataEntries: vi.fn((map) => { mockCache.fileCallData = map; }),
|
|
103
|
+
clearFileCallData: vi.fn(() => { mockCache.fileCallData = new Map(); }),
|
|
104
|
+
};
|
|
105
|
+
mockConfig = {
|
|
106
|
+
searchDirectory: '/test',
|
|
107
|
+
fileExtensions: ['js'],
|
|
108
|
+
fileNames: ['.gitignore'],
|
|
109
|
+
excludePatterns: [],
|
|
110
|
+
maxFileSize: 1024 * 1024,
|
|
111
|
+
batchSize: 10,
|
|
112
|
+
verbose: true,
|
|
113
|
+
callGraphEnabled: false,
|
|
114
|
+
watchFiles: true,
|
|
115
|
+
workerThreads: 1,
|
|
116
|
+
embeddingModel: 'test-model',
|
|
117
|
+
};
|
|
118
|
+
mockServer = {
|
|
119
|
+
hybridSearch: {
|
|
120
|
+
clearFileModTime: vi.fn(),
|
|
121
|
+
},
|
|
122
|
+
sendNotification: vi.fn(),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
indexer = new CodebaseIndexer(mockEmbedder, mockCache, mockConfig, mockServer);
|
|
126
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
127
|
+
vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
vi.restoreAllMocks();
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('re-initializes workers when thread configuration changes', async () => {
|
|
136
|
+
indexer.config.workerThreads = 'auto';
|
|
137
|
+
vi.spyOn(os, 'cpus').mockReturnValue([{}]);
|
|
138
|
+
await indexer.initializeWorkers();
|
|
139
|
+
|
|
140
|
+
indexer.config.workerThreads = 2;
|
|
141
|
+
await indexer.initializeWorkers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('covers initializeWorkers timeout branch', async () => {
|
|
145
|
+
vi.useFakeTimers();
|
|
146
|
+
workerMode = 'none';
|
|
147
|
+
indexer.config.workerThreads = 2;
|
|
148
|
+
const promise = indexer.initializeWorkers();
|
|
149
|
+
|
|
150
|
+
// Advance time to trigger timeout
|
|
151
|
+
vi.advanceTimersByTime(130000);
|
|
152
|
+
|
|
153
|
+
// The timeout callback rejects the promise, which is caught in initializeWorkers
|
|
154
|
+
await promise;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('logs warning when worker initialization fails', async () => {
|
|
158
|
+
indexer.config.workerThreads = 2;
|
|
159
|
+
workerMode = 'error';
|
|
160
|
+
await indexer.initializeWorkers();
|
|
161
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
162
|
+
expect.stringContaining('Worker initialization failed')
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('handles various worker message types correctly', async () => {
|
|
167
|
+
const mockWorker = {
|
|
168
|
+
on: vi.fn(),
|
|
169
|
+
off: vi.fn(),
|
|
170
|
+
once: vi.fn(),
|
|
171
|
+
postMessage: vi.fn(),
|
|
172
|
+
};
|
|
173
|
+
indexer.workers = [mockWorker];
|
|
174
|
+
|
|
175
|
+
const chunks = [{ file: 'test.js', text: 'code' }];
|
|
176
|
+
const promise = indexer.processChunksWithWorkers(chunks);
|
|
177
|
+
|
|
178
|
+
const handler = mockWorker.on.mock.calls.find((call) => call[0] === 'message')[1];
|
|
179
|
+
|
|
180
|
+
handler({ batchId: 'wrong' }); // L249 false
|
|
181
|
+
|
|
182
|
+
const batchId = mockWorker.postMessage.mock.calls[0][0].batchId;
|
|
183
|
+
handler({ batchId, type: 'unknown' }); // L254 unknown
|
|
184
|
+
handler({ batchId, type: 'error', error: 'fail' }); // L254 error
|
|
185
|
+
|
|
186
|
+
const results = await promise;
|
|
187
|
+
expect(results).toHaveLength(1); // Fallback ran
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('falls back to single-threaded execution on worker error', async () => {
|
|
191
|
+
const mockWorker = {
|
|
192
|
+
on: vi.fn(),
|
|
193
|
+
off: vi.fn(),
|
|
194
|
+
once: vi.fn(),
|
|
195
|
+
postMessage: vi.fn(),
|
|
196
|
+
};
|
|
197
|
+
indexer.workers = [mockWorker];
|
|
198
|
+
const fallbackSpy = vi
|
|
199
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
200
|
+
.mockResolvedValue([{ success: true }]);
|
|
201
|
+
|
|
202
|
+
const promise = indexer.processChunksWithWorkers([{ file: 'a.js', text: 'c' }]);
|
|
203
|
+
const handler = mockWorker.on.mock.calls.find((call) => call[0] === 'message')[1];
|
|
204
|
+
const batchId = mockWorker.postMessage.mock.calls[0][0].batchId;
|
|
205
|
+
handler({ batchId, type: 'error', error: 'boom' });
|
|
206
|
+
|
|
207
|
+
await promise;
|
|
208
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('covers processChunksWithWorkers reset timeout and done=false branch', async () => {
|
|
212
|
+
vi.useFakeTimers();
|
|
213
|
+
const mockWorker = {
|
|
214
|
+
on: vi.fn(),
|
|
215
|
+
off: vi.fn(),
|
|
216
|
+
once: vi.fn(),
|
|
217
|
+
postMessage: vi.fn(),
|
|
218
|
+
};
|
|
219
|
+
indexer.workers = [mockWorker];
|
|
220
|
+
const fallbackSpy = vi
|
|
221
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
222
|
+
.mockResolvedValue([{ success: true }]);
|
|
223
|
+
|
|
224
|
+
const promise = indexer.processChunksWithWorkers([{ file: 'a.js', text: 'c' }]);
|
|
225
|
+
const handler = mockWorker.on.mock.calls.find((call) => call[0] === 'message')[1];
|
|
226
|
+
const batchId = mockWorker.postMessage.mock.calls[0][0].batchId;
|
|
227
|
+
|
|
228
|
+
handler({
|
|
229
|
+
batchId,
|
|
230
|
+
type: 'results',
|
|
231
|
+
results: [{ success: true }],
|
|
232
|
+
done: false,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
vi.advanceTimersByTime(1000);
|
|
236
|
+
|
|
237
|
+
await promise;
|
|
238
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('covers processChunksWithWorkers postMessage failure', async () => {
|
|
242
|
+
const mockWorker = {
|
|
243
|
+
on: vi.fn(),
|
|
244
|
+
off: vi.fn(),
|
|
245
|
+
once: vi.fn(),
|
|
246
|
+
postMessage: vi.fn(() => {
|
|
247
|
+
throw new Error('post boom');
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
indexer.workers = [mockWorker];
|
|
251
|
+
const fallbackSpy = vi
|
|
252
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
253
|
+
.mockResolvedValue([{ success: true }]);
|
|
254
|
+
|
|
255
|
+
await indexer.processChunksWithWorkers([{ file: 'a.js', text: 'c' }]);
|
|
256
|
+
|
|
257
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
258
|
+
expect.stringContaining('postMessage failed')
|
|
259
|
+
);
|
|
260
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('returns empty array when input chunks are empty', async () => {
|
|
264
|
+
const mockWorker = {
|
|
265
|
+
on: vi.fn(),
|
|
266
|
+
off: vi.fn(),
|
|
267
|
+
once: vi.fn(),
|
|
268
|
+
postMessage: vi.fn(),
|
|
269
|
+
};
|
|
270
|
+
indexer.workers = [mockWorker];
|
|
271
|
+
const results = await indexer.processChunksWithWorkers([]);
|
|
272
|
+
expect(results).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('covers processChunksWithWorkers worker crash and timeout', async () => {
|
|
276
|
+
vi.useFakeTimers();
|
|
277
|
+
const mockWorker = {
|
|
278
|
+
on: vi.fn(),
|
|
279
|
+
off: vi.fn(),
|
|
280
|
+
once: vi.fn(),
|
|
281
|
+
postMessage: vi.fn(),
|
|
282
|
+
};
|
|
283
|
+
indexer.workers = [mockWorker];
|
|
284
|
+
|
|
285
|
+
const promise = indexer.processChunksWithWorkers([{ file: 'a.js', text: 'c' }]);
|
|
286
|
+
|
|
287
|
+
// 1. Crash
|
|
288
|
+
const errorHandler = mockWorker.once.mock.calls.find((c) => c[0] === 'error')[1];
|
|
289
|
+
errorHandler(new Error('crash'));
|
|
290
|
+
|
|
291
|
+
// 2. Timeout
|
|
292
|
+
const promise2 = indexer.processChunksWithWorkers([{ file: 'b.js', text: 'c' }]);
|
|
293
|
+
vi.advanceTimersByTime(310000);
|
|
294
|
+
|
|
295
|
+
await promise;
|
|
296
|
+
await promise2;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('covers missing call-graph stats guard (invalid stat)', async () => {
|
|
300
|
+
indexer.config.callGraphEnabled = true;
|
|
301
|
+
indexer.config.verbose = false;
|
|
302
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/a.js']);
|
|
303
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([]);
|
|
304
|
+
mockCache.getVectorStore.mockReturnValue([{ file: '/test/a.js' }]);
|
|
305
|
+
mockCache.clearFileCallData();
|
|
306
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({});
|
|
307
|
+
|
|
308
|
+
const result = await indexer.indexAll();
|
|
309
|
+
|
|
310
|
+
expect(result?.message).toBe('All files up to date');
|
|
311
|
+
expect(fs.stat).toHaveBeenCalledWith('/test/a.js');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('logs warning when file indexing fails (verbose mode)', async () => {
|
|
315
|
+
indexer.config.verbose = true;
|
|
316
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({ isDirectory: () => false, size: 100, mtimeMs: 123 });
|
|
317
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('content');
|
|
318
|
+
vi.spyOn(utils, 'hashContent').mockReturnValue('new-hash');
|
|
319
|
+
mockCache.getFileHash.mockReturnValue('old-hash');
|
|
320
|
+
|
|
321
|
+
vi.spyOn(utils, 'smartChunk').mockReturnValue([{ text: 'chunk1', startLine: 1, endLine: 2 }]);
|
|
322
|
+
mockEmbedder.mockRejectedValue(new Error('fail'));
|
|
323
|
+
|
|
324
|
+
await indexer.indexFile('file.js');
|
|
325
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Skipped hash update'));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('updates file hash registry after successful indexing', async () => {
|
|
329
|
+
indexer.config.verbose = false;
|
|
330
|
+
indexer.config.batchSize = 1;
|
|
331
|
+
indexer.config.workerThreads = 0;
|
|
332
|
+
indexer.config.allowSingleThreadFallback = true;
|
|
333
|
+
|
|
334
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/a.js']);
|
|
335
|
+
indexer.preFilterFiles = vi
|
|
336
|
+
.fn()
|
|
337
|
+
.mockResolvedValue([{ file: '/test/a.js', content: 'c', hash: 'h' }]);
|
|
338
|
+
vi.spyOn(utils, 'smartChunk').mockReturnValue([{ text: 'chunk', startLine: 1, endLine: 1 }]);
|
|
339
|
+
indexer.processChunksSingleThreaded = vi
|
|
340
|
+
.fn()
|
|
341
|
+
.mockResolvedValue([
|
|
342
|
+
{ success: true, file: '/test/a.js', startLine: 1, endLine: 1, vector: [0.1] },
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
await indexer.indexAll();
|
|
346
|
+
expect(mockCache.setFileHash).toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('logs warnings for hash skip and ANN failures', async () => {
|
|
350
|
+
indexer.config.verbose = true;
|
|
351
|
+
indexer.config.batchSize = 1;
|
|
352
|
+
indexer.config.workerThreads = 0;
|
|
353
|
+
indexer.config.allowSingleThreadFallback = true;
|
|
354
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/a.js']);
|
|
355
|
+
indexer.preFilterFiles = vi
|
|
356
|
+
.fn()
|
|
357
|
+
.mockResolvedValue([{ file: '/test/a.js', content: 'c', hash: 'h' }]);
|
|
358
|
+
vi.spyOn(utils, 'smartChunk').mockReturnValue([{ text: 'chunk', startLine: 1, endLine: 1 }]);
|
|
359
|
+
indexer.processChunksSingleThreaded = vi
|
|
360
|
+
.fn()
|
|
361
|
+
.mockResolvedValue([
|
|
362
|
+
{ success: false, file: '/test/a.js', startLine: 1, endLine: 1, error: 'fail' },
|
|
363
|
+
]);
|
|
364
|
+
mockCache.ensureAnnIndex.mockRejectedValue(new Error('ANN Boom'));
|
|
365
|
+
|
|
366
|
+
await indexer.indexAll();
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
368
|
+
|
|
369
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Skipped hash update'));
|
|
370
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
371
|
+
expect.stringContaining('Background ANN build failed')
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('logs skip warnings in verbose mode', async () => {
|
|
376
|
+
indexer.config.verbose = true;
|
|
377
|
+
indexer.config.batchSize = 1;
|
|
378
|
+
indexer.config.workerThreads = 0;
|
|
379
|
+
indexer.config.allowSingleThreadFallback = true;
|
|
380
|
+
|
|
381
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/a.js']);
|
|
382
|
+
indexer.preFilterFiles = vi
|
|
383
|
+
.fn()
|
|
384
|
+
.mockResolvedValue([{ file: '/test/a.js', content: 'c', hash: 'h' }]);
|
|
385
|
+
|
|
386
|
+
vi.spyOn(utils, 'smartChunk').mockReturnValue([{ text: 'chunk' }]);
|
|
387
|
+
|
|
388
|
+
indexer.processChunksSingleThreaded = vi
|
|
389
|
+
.fn()
|
|
390
|
+
.mockResolvedValue([{ success: false, file: '/test/a.js', error: 'fail' }]);
|
|
391
|
+
|
|
392
|
+
mockCache.ensureAnnIndex.mockRejectedValue(new Error('ANN Boom'));
|
|
393
|
+
|
|
394
|
+
await indexer.indexAll();
|
|
395
|
+
|
|
396
|
+
// Wait for background promise
|
|
397
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
398
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Skipped hash update'));
|
|
399
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
400
|
+
expect.stringContaining('Background ANN build failed')
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('processes batches and tracks progress', async () => {
|
|
405
|
+
indexer.config.verbose = false;
|
|
406
|
+
indexer.config.batchSize = 1;
|
|
407
|
+
|
|
408
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['a', 'b', 'c', 'd']);
|
|
409
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
410
|
+
{ file: 'a', content: 'c', hash: 'h' },
|
|
411
|
+
{ file: 'b', content: 'c', hash: 'h' },
|
|
412
|
+
{ file: 'c', content: 'c', hash: 'h' },
|
|
413
|
+
{ file: 'd', content: 'c', hash: 'h' },
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
indexer.processChunksSingleThreaded = vi.fn().mockImplementation(async (chunks) => {
|
|
417
|
+
return chunks.map((c) => ({ success: true, file: c.file, vector: [0.1] }));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await indexer.indexAll();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('logs when queued watch events processing fails', async () => {
|
|
424
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue([]);
|
|
425
|
+
indexer.processPendingWatchEvents = vi
|
|
426
|
+
.fn()
|
|
427
|
+
.mockRejectedValue(new Error('queue boom'));
|
|
428
|
+
|
|
429
|
+
await indexer.indexAll();
|
|
430
|
+
|
|
431
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
432
|
+
expect.stringContaining('Failed to apply queued file updates')
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('overwrites queued unlink when a new event arrives', () => {
|
|
437
|
+
const filePath = '/test/file.js';
|
|
438
|
+
indexer.pendingWatchEvents.set(filePath, 'unlink');
|
|
439
|
+
|
|
440
|
+
CodebaseIndexer.prototype.enqueueWatchEvent.call(indexer, 'change', filePath);
|
|
441
|
+
|
|
442
|
+
expect(indexer.pendingWatchEvents.get(filePath)).toBe('change');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('covers setupFileWatcher branches', async () => {
|
|
446
|
+
indexer.config.watchFiles = true;
|
|
447
|
+
await indexer.setupFileWatcher();
|
|
448
|
+
await indexer.setupFileWatcher();
|
|
449
|
+
|
|
450
|
+
// TRUE branches
|
|
451
|
+
await handlers['add']('file.js');
|
|
452
|
+
await handlers['change']('file.js');
|
|
453
|
+
await handlers['unlink']('file.js');
|
|
454
|
+
|
|
455
|
+
// FALSE branches
|
|
456
|
+
indexer.server = null;
|
|
457
|
+
await handlers['add']('file.js');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('skips files provided with content if too large', async () => {
|
|
461
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['file-large-content.js']);
|
|
462
|
+
// Mock preFilterFiles to return an entry with content that exceeds maxFileSize
|
|
463
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
464
|
+
{
|
|
465
|
+
file: 'file-large-content.js',
|
|
466
|
+
content: 'x'.repeat(mockConfig.maxFileSize + 100),
|
|
467
|
+
hash: 'hash1',
|
|
468
|
+
force: false
|
|
469
|
+
}
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
await indexer.indexAll();
|
|
473
|
+
|
|
474
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
475
|
+
expect.stringContaining('Skipped file-large-content.js (too large:')
|
|
476
|
+
);
|
|
477
|
+
expect(mockEmbedder).not.toHaveBeenCalled();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('skips files with invalid stat results', async () => {
|
|
481
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['invalid-stat.js']);
|
|
482
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
483
|
+
{ file: 'invalid-stat.js', force: false }
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
vi.spyOn(fs, 'stat').mockResolvedValue(null);
|
|
487
|
+
|
|
488
|
+
await indexer.indexAll();
|
|
489
|
+
|
|
490
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
491
|
+
expect.stringContaining('Invalid stat result for invalid-stat.js')
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('skips files that are too large via stat check', async () => {
|
|
496
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['large-stat.js']);
|
|
497
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
498
|
+
{ file: 'large-stat.js', force: false }
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
502
|
+
isDirectory: () => false,
|
|
503
|
+
size: mockConfig.maxFileSize + 100,
|
|
504
|
+
mtimeMs: 123
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
await indexer.indexAll();
|
|
508
|
+
|
|
509
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
510
|
+
expect.stringContaining('Skipped large-stat.js (too large:')
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('handles read file failures', async () => {
|
|
515
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['read-error.js']);
|
|
516
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
517
|
+
{ file: 'read-error.js', force: false }
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
521
|
+
isDirectory: () => false,
|
|
522
|
+
size: 50,
|
|
523
|
+
mtimeMs: 123
|
|
524
|
+
});
|
|
525
|
+
vi.spyOn(fs, 'readFile').mockRejectedValue(new Error('Read failed'));
|
|
526
|
+
|
|
527
|
+
await indexer.indexAll();
|
|
528
|
+
|
|
529
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
530
|
+
expect.stringContaining('Failed to read read-error.js: Read failed')
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('skips unchanged files when hash matches', async () => {
|
|
535
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['unchanged.js']);
|
|
536
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
537
|
+
{ file: 'unchanged.js', force: false }
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
541
|
+
isDirectory: () => false,
|
|
542
|
+
size: 50,
|
|
543
|
+
mtimeMs: 123
|
|
544
|
+
});
|
|
545
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('content');
|
|
546
|
+
|
|
547
|
+
vi.spyOn(utils, 'hashContent').mockReturnValue('same-hash');
|
|
548
|
+
mockCache.getFileHash.mockReturnValue('same-hash');
|
|
549
|
+
|
|
550
|
+
await indexer.indexAll();
|
|
551
|
+
|
|
552
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
553
|
+
expect.stringContaining('Skipped unchanged.js (unchanged)')
|
|
554
|
+
);
|
|
555
|
+
expect(mockEmbedder).not.toHaveBeenCalled();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('queues watch events when indexing is in progress', async () => {
|
|
559
|
+
await indexer.setupFileWatcher();
|
|
560
|
+
|
|
561
|
+
// Simulate indexing in progress
|
|
562
|
+
indexer.isIndexing = true;
|
|
563
|
+
|
|
564
|
+
// Trigger ADD event
|
|
565
|
+
await handlers['add']('added.js');
|
|
566
|
+
expect(indexer.pendingWatchEvents.get(path.join('/test', 'added.js'))).toBe('add');
|
|
567
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
568
|
+
expect.stringContaining('Queued add event during indexing')
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// Trigger CHANGE event
|
|
572
|
+
await handlers['change']('changed.js');
|
|
573
|
+
expect(indexer.pendingWatchEvents.get(path.join('/test', 'changed.js'))).toBe('change');
|
|
574
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
575
|
+
expect.stringContaining('Queued change event during indexing')
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// Trigger UNLINK event
|
|
579
|
+
await handlers['unlink']('deleted.js');
|
|
580
|
+
expect(indexer.pendingWatchEvents.get(path.join('/test', 'deleted.js'))).toBe('unlink');
|
|
581
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
582
|
+
expect.stringContaining('Queued delete event during indexing')
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
});
|