@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,1032 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
let mockFiles = [];
|
|
4
|
+
let cpuCount = 2;
|
|
5
|
+
let workerMessageType = 'ready';
|
|
6
|
+
const fsMock = {};
|
|
7
|
+
const smartChunkMock = vi.fn();
|
|
8
|
+
const hashContentMock = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock('fdir', () => ({
|
|
11
|
+
fdir: class {
|
|
12
|
+
withFullPaths() {
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
exclude() {
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
filter(fn) {
|
|
19
|
+
this.filterFn = fn;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
crawl() {
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
withPromise() {
|
|
26
|
+
const filtered = this.filterFn ? mockFiles.filter(this.filterFn) : mockFiles;
|
|
27
|
+
return Promise.resolve(filtered);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('os', () => ({
|
|
32
|
+
default: { cpus: () => new Array(cpuCount).fill({}) },
|
|
33
|
+
cpus: () => new Array(cpuCount).fill({}),
|
|
34
|
+
}));
|
|
35
|
+
class MockWorker {
|
|
36
|
+
once(event, handler) {
|
|
37
|
+
if (event === 'message') {
|
|
38
|
+
setImmediate(() => handler({ type: workerMessageType, error: 'worker fail' }));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
on() {}
|
|
42
|
+
off() {}
|
|
43
|
+
postMessage() {}
|
|
44
|
+
terminate() {
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
vi.mock('worker_threads', () => ({
|
|
49
|
+
Worker: MockWorker,
|
|
50
|
+
}));
|
|
51
|
+
vi.mock('fs/promises', () => ({
|
|
52
|
+
default: fsMock,
|
|
53
|
+
...fsMock,
|
|
54
|
+
}));
|
|
55
|
+
vi.mock('../lib/utils.js', () => ({
|
|
56
|
+
smartChunk: (...args) => smartChunkMock(...args),
|
|
57
|
+
hashContent: (...args) => hashContentMock(...args),
|
|
58
|
+
}));
|
|
59
|
+
vi.mock('../lib/call-graph.js', () => ({
|
|
60
|
+
extractCallData: vi.fn().mockReturnValue({ definitions: [], calls: [] }),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const createCache = () => ({
|
|
64
|
+
vectorStore: [],
|
|
65
|
+
fileHashes: new Map(),
|
|
66
|
+
fileCallData: new Map(),
|
|
67
|
+
getFileHashKeys() {
|
|
68
|
+
return Array.from(this.fileHashes.keys());
|
|
69
|
+
},
|
|
70
|
+
getFileHashCount() {
|
|
71
|
+
return this.fileHashes.size;
|
|
72
|
+
},
|
|
73
|
+
clearFileHashes() {
|
|
74
|
+
this.fileHashes.clear();
|
|
75
|
+
},
|
|
76
|
+
getFileCallDataKeys() {
|
|
77
|
+
return Array.from(this.fileCallData.keys());
|
|
78
|
+
},
|
|
79
|
+
getFileCallDataCount() {
|
|
80
|
+
return this.fileCallData.size;
|
|
81
|
+
},
|
|
82
|
+
clearFileCallData() {
|
|
83
|
+
this.fileCallData.clear();
|
|
84
|
+
},
|
|
85
|
+
getFileHash: vi.fn(),
|
|
86
|
+
setFileHash: vi.fn(),
|
|
87
|
+
deleteFileHash: vi.fn(),
|
|
88
|
+
removeFileFromStore: vi.fn(),
|
|
89
|
+
addToStore: vi.fn(),
|
|
90
|
+
setVectorStore: vi.fn(),
|
|
91
|
+
getVectorStore: vi.fn().mockReturnValue([]),
|
|
92
|
+
pruneCallGraphData: vi.fn().mockReturnValue(0),
|
|
93
|
+
clearCallGraphData: vi.fn(),
|
|
94
|
+
save: vi.fn().mockResolvedValue(undefined),
|
|
95
|
+
rebuildCallGraph: vi.fn(),
|
|
96
|
+
ensureAnnIndex: vi.fn().mockResolvedValue(null),
|
|
97
|
+
setLastIndexDuration: vi.fn(),
|
|
98
|
+
setLastIndexStats: vi.fn(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('index-codebase branch coverage focused', () => {
|
|
102
|
+
let consoleWarn;
|
|
103
|
+
let consoleInfo;
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
vi.resetModules();
|
|
107
|
+
mockFiles = [];
|
|
108
|
+
cpuCount = 2;
|
|
109
|
+
workerMessageType = 'ready';
|
|
110
|
+
fsMock.stat = vi.fn();
|
|
111
|
+
fsMock.readFile = vi.fn();
|
|
112
|
+
fsMock.mkdir = vi.fn();
|
|
113
|
+
consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
114
|
+
consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
consoleWarn.mockRestore();
|
|
119
|
+
consoleInfo.mockRestore();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles auto worker init failures', async () => {
|
|
123
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
124
|
+
const cache = createCache();
|
|
125
|
+
const config = {
|
|
126
|
+
workerThreads: 'auto',
|
|
127
|
+
verbose: true,
|
|
128
|
+
embeddingModel: 'test-model',
|
|
129
|
+
excludePatterns: [],
|
|
130
|
+
};
|
|
131
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
132
|
+
|
|
133
|
+
cpuCount = 4;
|
|
134
|
+
workerMessageType = 'error';
|
|
135
|
+
await indexer.initializeWorkers();
|
|
136
|
+
|
|
137
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
138
|
+
expect.stringContaining('Worker initialization failed')
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects worker ready promise on error message', async () => {
|
|
143
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
144
|
+
const cache = createCache();
|
|
145
|
+
const config = {
|
|
146
|
+
workerThreads: 2,
|
|
147
|
+
verbose: true,
|
|
148
|
+
embeddingModel: 'test-model',
|
|
149
|
+
excludePatterns: [],
|
|
150
|
+
};
|
|
151
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
152
|
+
|
|
153
|
+
workerMessageType = 'error';
|
|
154
|
+
const terminateSpy = vi.spyOn(indexer, 'terminateWorkers');
|
|
155
|
+
|
|
156
|
+
await indexer.initializeWorkers();
|
|
157
|
+
|
|
158
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
159
|
+
expect.stringContaining('Worker initialization failed')
|
|
160
|
+
);
|
|
161
|
+
expect(terminateSpy).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('times out worker init on unexpected message type', async () => {
|
|
165
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
166
|
+
const cache = createCache();
|
|
167
|
+
const config = {
|
|
168
|
+
workerThreads: 2,
|
|
169
|
+
verbose: true,
|
|
170
|
+
embeddingModel: 'test-model',
|
|
171
|
+
excludePatterns: [],
|
|
172
|
+
};
|
|
173
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
174
|
+
|
|
175
|
+
workerMessageType = 'other';
|
|
176
|
+
const timeoutSpy = vi.spyOn(globalThis, 'setTimeout').mockImplementation((fn) => {
|
|
177
|
+
fn();
|
|
178
|
+
return 0;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await indexer.initializeWorkers();
|
|
183
|
+
} finally {
|
|
184
|
+
timeoutSpy.mockRestore();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
188
|
+
expect.stringContaining('Worker initialization failed')
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('matches exclude patterns with and without path separators', async () => {
|
|
193
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
194
|
+
const cache = createCache();
|
|
195
|
+
const config = {
|
|
196
|
+
excludePatterns: ['skip.js', '**/dir/**'],
|
|
197
|
+
fileExtensions: ['js'],
|
|
198
|
+
};
|
|
199
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
200
|
+
|
|
201
|
+
expect(indexer.isExcluded('/root/skip.js')).toBe(true);
|
|
202
|
+
expect(indexer.isExcluded('/root/dir/file.js')).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('covers worker error message handling and retry', async () => {
|
|
206
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
207
|
+
const cache = createCache();
|
|
208
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, { workerThreads: 2 });
|
|
209
|
+
|
|
210
|
+
const makeWorker = (mode) => {
|
|
211
|
+
let handler;
|
|
212
|
+
return {
|
|
213
|
+
on: (event, fn) => {
|
|
214
|
+
if (event === 'message') handler = fn;
|
|
215
|
+
},
|
|
216
|
+
once: () => {},
|
|
217
|
+
off: () => {},
|
|
218
|
+
postMessage: (msg) => {
|
|
219
|
+
if (mode === 'results') {
|
|
220
|
+
handler({ type: 'results', results: [{ success: true }], batchId: msg.batchId });
|
|
221
|
+
} else {
|
|
222
|
+
handler({ type: 'error', error: 'boom', batchId: msg.batchId });
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
indexer.workers = [makeWorker('results'), makeWorker('error')];
|
|
228
|
+
|
|
229
|
+
const fallbackSpy = vi
|
|
230
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
231
|
+
.mockResolvedValue([{ success: true }]);
|
|
232
|
+
|
|
233
|
+
const results = await indexer.processChunksWithWorkers([
|
|
234
|
+
{ file: 'a.js', text: 'x' },
|
|
235
|
+
{ file: 'b.js', text: 'y' },
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
expect(results.length).toBeGreaterThan(0);
|
|
239
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('retries failed worker batches when results are empty', async () => {
|
|
243
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
244
|
+
const cache = createCache();
|
|
245
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, { workerThreads: 1 });
|
|
246
|
+
|
|
247
|
+
let handler;
|
|
248
|
+
const worker = {
|
|
249
|
+
on: (event, fn) => {
|
|
250
|
+
if (event === 'message') handler = fn;
|
|
251
|
+
},
|
|
252
|
+
once: () => {},
|
|
253
|
+
off: () => {},
|
|
254
|
+
postMessage: (msg) => {
|
|
255
|
+
handler({ type: 'results', results: [], batchId: msg.batchId });
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
indexer.workers = [worker];
|
|
259
|
+
|
|
260
|
+
const fallbackSpy = vi
|
|
261
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
262
|
+
.mockResolvedValue([{ success: true }]);
|
|
263
|
+
|
|
264
|
+
await indexer.processChunksWithWorkers([{ file: 'a.js', text: 'x' }]);
|
|
265
|
+
|
|
266
|
+
expect(fallbackSpy).toHaveBeenCalledWith(
|
|
267
|
+
expect.arrayContaining([expect.objectContaining({ file: 'a.js' })])
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('skips retry when worker chunks are emptied after processing', async () => {
|
|
272
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
273
|
+
const cache = createCache();
|
|
274
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, { workerThreads: 1 });
|
|
275
|
+
|
|
276
|
+
// Create a worker that returns empty results
|
|
277
|
+
let handler;
|
|
278
|
+
const worker = {
|
|
279
|
+
on: (event, fn) => {
|
|
280
|
+
if (event === 'message') handler = fn;
|
|
281
|
+
},
|
|
282
|
+
once: () => {},
|
|
283
|
+
off: () => {},
|
|
284
|
+
postMessage: (msg) => {
|
|
285
|
+
// Respond with success but empty results, implying nothing needed retry or all done
|
|
286
|
+
handler({ type: 'results', results: [], batchId: msg.batchId });
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
indexer.workers = [worker];
|
|
290
|
+
|
|
291
|
+
const fallbackSpy = vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([]);
|
|
292
|
+
const results = await indexer.processChunksWithWorkers([{ file: 'a.js', text: 'x' }]);
|
|
293
|
+
expect(results).toEqual([]);
|
|
294
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('handles mismatched batch IDs and times out', async () => {
|
|
298
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
299
|
+
const cache = createCache();
|
|
300
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, { workerThreads: 2 });
|
|
301
|
+
|
|
302
|
+
let handler;
|
|
303
|
+
const worker = {
|
|
304
|
+
on: (event, fn) => {
|
|
305
|
+
if (event === 'message') handler = fn;
|
|
306
|
+
},
|
|
307
|
+
once: () => {},
|
|
308
|
+
off: () => {},
|
|
309
|
+
postMessage: () => {
|
|
310
|
+
handler({ type: 'results', results: [{ success: true }], batchId: 'wrong' });
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
indexer.workers = [worker];
|
|
314
|
+
|
|
315
|
+
const fallbackSpy = vi
|
|
316
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
317
|
+
.mockResolvedValue([{ success: true }]);
|
|
318
|
+
|
|
319
|
+
vi.useFakeTimers();
|
|
320
|
+
const promise = indexer.processChunksWithWorkers([{ file: 'a.js', text: 'x' }]);
|
|
321
|
+
vi.advanceTimersByTime(300001);
|
|
322
|
+
const results = await promise;
|
|
323
|
+
vi.useRealTimers();
|
|
324
|
+
|
|
325
|
+
expect(fallbackSpy).toHaveBeenCalled();
|
|
326
|
+
expect(results).toHaveLength(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('skips hash update logging when verbose is false for single-file failures', async () => {
|
|
330
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
331
|
+
const cache = createCache();
|
|
332
|
+
const embedder = vi.fn().mockRejectedValue(new Error('embed fail'));
|
|
333
|
+
const config = {
|
|
334
|
+
excludePatterns: [],
|
|
335
|
+
verbose: false,
|
|
336
|
+
maxFileSize: 10,
|
|
337
|
+
fileExtensions: ['js'],
|
|
338
|
+
};
|
|
339
|
+
const indexer = new CodebaseIndexer(embedder, cache, config);
|
|
340
|
+
|
|
341
|
+
fsMock.stat.mockResolvedValueOnce({ isDirectory: () => false, size: 1 });
|
|
342
|
+
fsMock.readFile.mockResolvedValueOnce('content');
|
|
343
|
+
hashContentMock.mockReturnValueOnce('newhash');
|
|
344
|
+
cache.getFileHash.mockReturnValueOnce('oldhash');
|
|
345
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
346
|
+
|
|
347
|
+
await indexer.indexFile('/root/fail-quiet.js');
|
|
348
|
+
|
|
349
|
+
const hasSkipLog = consoleWarn.mock.calls.some(
|
|
350
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update')
|
|
351
|
+
);
|
|
352
|
+
expect(hasSkipLog).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('logs indexFile hash skip when embedding fails in verbose mode', async () => {
|
|
356
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
357
|
+
const cache = createCache();
|
|
358
|
+
const embedder = vi.fn().mockRejectedValue(new Error('embed fail'));
|
|
359
|
+
const config = {
|
|
360
|
+
excludePatterns: [],
|
|
361
|
+
verbose: true,
|
|
362
|
+
maxFileSize: 10,
|
|
363
|
+
fileExtensions: ['js'],
|
|
364
|
+
};
|
|
365
|
+
const indexer = new CodebaseIndexer(embedder, cache, config);
|
|
366
|
+
|
|
367
|
+
fsMock.stat.mockResolvedValueOnce({ isDirectory: () => false, size: 1 });
|
|
368
|
+
fsMock.readFile.mockResolvedValueOnce('content');
|
|
369
|
+
hashContentMock.mockReturnValueOnce('newhash');
|
|
370
|
+
cache.getFileHash.mockReturnValueOnce('oldhash');
|
|
371
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
372
|
+
|
|
373
|
+
await indexer.indexFile('/root/fail.js');
|
|
374
|
+
|
|
375
|
+
const hasSkipLog = consoleWarn.mock.calls.some(
|
|
376
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update')
|
|
377
|
+
);
|
|
378
|
+
expect(hasSkipLog).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('logs indexFile hash skip on partial embedding failures', async () => {
|
|
382
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
383
|
+
const cache = createCache();
|
|
384
|
+
const embedder = vi
|
|
385
|
+
.fn()
|
|
386
|
+
.mockResolvedValueOnce({ data: Float32Array.from([1]) })
|
|
387
|
+
.mockRejectedValueOnce(new Error('embed fail'));
|
|
388
|
+
const config = {
|
|
389
|
+
excludePatterns: [],
|
|
390
|
+
verbose: true,
|
|
391
|
+
maxFileSize: 10,
|
|
392
|
+
fileExtensions: ['js'],
|
|
393
|
+
};
|
|
394
|
+
const indexer = new CodebaseIndexer(embedder, cache, config);
|
|
395
|
+
|
|
396
|
+
fsMock.stat.mockResolvedValueOnce({ isDirectory: () => false, size: 1 });
|
|
397
|
+
fsMock.readFile.mockResolvedValueOnce('content');
|
|
398
|
+
hashContentMock.mockReturnValueOnce('newhash');
|
|
399
|
+
cache.getFileHash.mockReturnValueOnce('oldhash');
|
|
400
|
+
smartChunkMock.mockReturnValueOnce([
|
|
401
|
+
{ text: 'a', startLine: 1, endLine: 1 },
|
|
402
|
+
{ text: 'b', startLine: 2, endLine: 2 },
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
await indexer.indexFile('/root/partial.js');
|
|
406
|
+
|
|
407
|
+
const hasSkipLog = consoleWarn.mock.calls.some(
|
|
408
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update')
|
|
409
|
+
);
|
|
410
|
+
expect(hasSkipLog).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('covers batch hash skip and ANN background error logging', async () => {
|
|
414
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
415
|
+
const cache = createCache();
|
|
416
|
+
cache.ensureAnnIndex = vi.fn().mockRejectedValue(new Error('ann fail'));
|
|
417
|
+
cache.getVectorStore.mockReturnValue([]);
|
|
418
|
+
const config = {
|
|
419
|
+
searchDirectory: '/root',
|
|
420
|
+
excludePatterns: [],
|
|
421
|
+
fileExtensions: ['js'],
|
|
422
|
+
fileNames: [],
|
|
423
|
+
batchSize: 1,
|
|
424
|
+
maxFileSize: 1000,
|
|
425
|
+
callGraphEnabled: false,
|
|
426
|
+
verbose: true,
|
|
427
|
+
workerThreads: 0,
|
|
428
|
+
allowSingleThreadFallback: true,
|
|
429
|
+
};
|
|
430
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
431
|
+
|
|
432
|
+
mockFiles = ['/root/a.js'];
|
|
433
|
+
indexer.preFilterFiles = vi
|
|
434
|
+
.fn()
|
|
435
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
436
|
+
smartChunkMock.mockReturnValueOnce([
|
|
437
|
+
{ text: 'a', startLine: 1, endLine: 1 },
|
|
438
|
+
{ text: 'b', startLine: 2, endLine: 2 },
|
|
439
|
+
]);
|
|
440
|
+
const processSpy = vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
441
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
442
|
+
{ file: '/root/a.js', startLine: 2, endLine: 2, content: 'b', vector: [2], success: false },
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
await indexer.indexAll(false);
|
|
446
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
447
|
+
|
|
448
|
+
const hasBatchSkip = consoleWarn.mock.calls.some(
|
|
449
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update for a.js')
|
|
450
|
+
);
|
|
451
|
+
const hasAnnError = consoleWarn.mock.calls.some(
|
|
452
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Background ANN build failed')
|
|
453
|
+
);
|
|
454
|
+
expect(hasBatchSkip).toBe(true);
|
|
455
|
+
expect(hasAnnError).toBe(true);
|
|
456
|
+
expect(processSpy).toHaveBeenCalledWith(
|
|
457
|
+
expect.arrayContaining([
|
|
458
|
+
expect.objectContaining({ startLine: 1 }),
|
|
459
|
+
expect.objectContaining({ startLine: 2 }),
|
|
460
|
+
])
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('ignores results for files not in the current batch', async () => {
|
|
465
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
466
|
+
const cache = createCache();
|
|
467
|
+
const config = {
|
|
468
|
+
searchDirectory: '/root',
|
|
469
|
+
excludePatterns: [],
|
|
470
|
+
fileExtensions: ['js'],
|
|
471
|
+
fileNames: [],
|
|
472
|
+
batchSize: 1,
|
|
473
|
+
maxFileSize: 1000,
|
|
474
|
+
callGraphEnabled: false,
|
|
475
|
+
verbose: false,
|
|
476
|
+
workerThreads: 0,
|
|
477
|
+
allowSingleThreadFallback: true,
|
|
478
|
+
};
|
|
479
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
480
|
+
|
|
481
|
+
cpuCount = 1;
|
|
482
|
+
mockFiles = ['/root/a.js'];
|
|
483
|
+
indexer.preFilterFiles = vi
|
|
484
|
+
.fn()
|
|
485
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
486
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
487
|
+
|
|
488
|
+
// Return a result for a file that wasn't in the batch ('phantom.js')
|
|
489
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
490
|
+
{ file: '/root/phantom.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
491
|
+
]);
|
|
492
|
+
|
|
493
|
+
await indexer.indexAll(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('increments chunk stats and logs batch hash skip in verbose mode', async () => {
|
|
497
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
498
|
+
const cache = createCache();
|
|
499
|
+
const config = {
|
|
500
|
+
searchDirectory: '/root',
|
|
501
|
+
excludePatterns: [],
|
|
502
|
+
fileExtensions: ['js'],
|
|
503
|
+
fileNames: [],
|
|
504
|
+
batchSize: 1,
|
|
505
|
+
maxFileSize: 1000,
|
|
506
|
+
callGraphEnabled: false,
|
|
507
|
+
verbose: true,
|
|
508
|
+
workerThreads: 0,
|
|
509
|
+
allowSingleThreadFallback: true,
|
|
510
|
+
};
|
|
511
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
512
|
+
|
|
513
|
+
cpuCount = 1;
|
|
514
|
+
mockFiles = ['/root/a.js'];
|
|
515
|
+
indexer.preFilterFiles = vi
|
|
516
|
+
.fn()
|
|
517
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
518
|
+
smartChunkMock.mockReturnValueOnce([
|
|
519
|
+
{ text: 'a', startLine: 1, endLine: 1 },
|
|
520
|
+
{ text: 'b', startLine: 2, endLine: 2 },
|
|
521
|
+
]);
|
|
522
|
+
const processSpy = vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
523
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
524
|
+
{ file: '/root/a.js', startLine: 2, endLine: 2, content: 'b', vector: [2], success: false },
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
await indexer.indexAll(false);
|
|
528
|
+
|
|
529
|
+
const hasBatchSkip = consoleWarn.mock.calls.some(
|
|
530
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update for a.js')
|
|
531
|
+
);
|
|
532
|
+
expect(hasBatchSkip).toBe(true);
|
|
533
|
+
expect(processSpy).toHaveBeenCalledWith(
|
|
534
|
+
expect.arrayContaining([
|
|
535
|
+
expect.objectContaining({ startLine: 1 }),
|
|
536
|
+
expect.objectContaining({ startLine: 2 }),
|
|
537
|
+
])
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('skips batch hash update logging when verbose is false', async () => {
|
|
542
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
543
|
+
const cache = createCache();
|
|
544
|
+
const config = {
|
|
545
|
+
searchDirectory: '/root',
|
|
546
|
+
excludePatterns: [],
|
|
547
|
+
fileExtensions: ['js'],
|
|
548
|
+
fileNames: [],
|
|
549
|
+
batchSize: 1,
|
|
550
|
+
maxFileSize: 1000,
|
|
551
|
+
callGraphEnabled: false,
|
|
552
|
+
verbose: false,
|
|
553
|
+
workerThreads: 0,
|
|
554
|
+
allowSingleThreadFallback: true,
|
|
555
|
+
};
|
|
556
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
557
|
+
|
|
558
|
+
cpuCount = 1;
|
|
559
|
+
mockFiles = ['/root/a.js'];
|
|
560
|
+
indexer.preFilterFiles = vi
|
|
561
|
+
.fn()
|
|
562
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
563
|
+
smartChunkMock.mockReturnValueOnce([
|
|
564
|
+
{ text: 'a', startLine: 1, endLine: 1 },
|
|
565
|
+
{ text: 'b', startLine: 2, endLine: 2 },
|
|
566
|
+
]);
|
|
567
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
568
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
569
|
+
{ file: '/root/a.js', startLine: 2, endLine: 2, content: 'b', vector: [2], success: false },
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
await indexer.indexAll(false);
|
|
573
|
+
|
|
574
|
+
const hasBatchSkip = consoleWarn.mock.calls.some(
|
|
575
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Skipped hash update for a.js')
|
|
576
|
+
);
|
|
577
|
+
expect(hasBatchSkip).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('logs ANN build failure in verbose mode', async () => {
|
|
581
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
582
|
+
const cache = createCache();
|
|
583
|
+
cache.ensureAnnIndex = vi.fn().mockRejectedValue(new Error('ann fail'));
|
|
584
|
+
cache.getVectorStore.mockReturnValue([]);
|
|
585
|
+
const config = {
|
|
586
|
+
searchDirectory: '/root',
|
|
587
|
+
excludePatterns: [],
|
|
588
|
+
fileExtensions: ['js'],
|
|
589
|
+
fileNames: [],
|
|
590
|
+
batchSize: 1,
|
|
591
|
+
maxFileSize: 1000,
|
|
592
|
+
callGraphEnabled: false,
|
|
593
|
+
verbose: true,
|
|
594
|
+
workerThreads: 0,
|
|
595
|
+
allowSingleThreadFallback: true,
|
|
596
|
+
};
|
|
597
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
598
|
+
|
|
599
|
+
cpuCount = 1;
|
|
600
|
+
mockFiles = ['/root/a.js'];
|
|
601
|
+
indexer.preFilterFiles = vi
|
|
602
|
+
.fn()
|
|
603
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
604
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
605
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
606
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
await indexer.indexAll(false);
|
|
610
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
611
|
+
|
|
612
|
+
const hasAnnError = consoleWarn.mock.calls.some(
|
|
613
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Background ANN build failed')
|
|
614
|
+
);
|
|
615
|
+
expect(hasAnnError).toBe(true);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('skips ANN build error logging when verbose is false', async () => {
|
|
619
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
620
|
+
const cache = createCache();
|
|
621
|
+
cache.ensureAnnIndex = vi.fn().mockRejectedValue(new Error('ann fail'));
|
|
622
|
+
cache.getVectorStore.mockReturnValue([]);
|
|
623
|
+
const config = {
|
|
624
|
+
searchDirectory: '/root',
|
|
625
|
+
excludePatterns: [],
|
|
626
|
+
fileExtensions: ['js'],
|
|
627
|
+
fileNames: [],
|
|
628
|
+
batchSize: 1,
|
|
629
|
+
maxFileSize: 1000,
|
|
630
|
+
callGraphEnabled: false,
|
|
631
|
+
verbose: false,
|
|
632
|
+
workerThreads: 0,
|
|
633
|
+
allowSingleThreadFallback: true,
|
|
634
|
+
};
|
|
635
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
636
|
+
|
|
637
|
+
cpuCount = 1;
|
|
638
|
+
mockFiles = ['/root/a.js'];
|
|
639
|
+
indexer.preFilterFiles = vi
|
|
640
|
+
.fn()
|
|
641
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
642
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
643
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
644
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
645
|
+
]);
|
|
646
|
+
|
|
647
|
+
await indexer.indexAll(false);
|
|
648
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
649
|
+
|
|
650
|
+
const hasAnnError = consoleWarn.mock.calls.some(
|
|
651
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Background ANN build failed')
|
|
652
|
+
);
|
|
653
|
+
expect(hasAnnError).toBe(false);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('uses adaptive batch sizes and increments chunk stats', async () => {
|
|
657
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
658
|
+
const cache = createCache();
|
|
659
|
+
const config = {
|
|
660
|
+
searchDirectory: '/root',
|
|
661
|
+
excludePatterns: [],
|
|
662
|
+
fileExtensions: ['js'],
|
|
663
|
+
fileNames: [],
|
|
664
|
+
batchSize: 5,
|
|
665
|
+
maxFileSize: 1000,
|
|
666
|
+
callGraphEnabled: false,
|
|
667
|
+
verbose: true,
|
|
668
|
+
};
|
|
669
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
670
|
+
|
|
671
|
+
mockFiles = new Array(10001).fill(0).map((_, i) => `/root/b${i}.js`);
|
|
672
|
+
indexer.preFilterFiles = vi
|
|
673
|
+
.fn()
|
|
674
|
+
.mockResolvedValue([{ file: '/root/b0.js', content: 'code', hash: 'h' }]);
|
|
675
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'x', startLine: 1, endLine: 1 }]);
|
|
676
|
+
const processSpy = vi
|
|
677
|
+
.spyOn(indexer, 'processChunksSingleThreaded')
|
|
678
|
+
.mockResolvedValue([
|
|
679
|
+
{ file: '/root/b0.js', startLine: 1, endLine: 1, content: 'x', vector: [1], success: true },
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
await indexer.indexAll(false);
|
|
683
|
+
|
|
684
|
+
const hasBatchSize = consoleInfo.mock.calls.some(
|
|
685
|
+
(call) => typeof call[0] === 'string' && call[0].includes('batch size: 500')
|
|
686
|
+
);
|
|
687
|
+
expect(hasBatchSize).toBe(true);
|
|
688
|
+
expect(processSpy.mock.calls[0][0]).toHaveLength(1);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('accepts allowed file names without matching extensions', async () => {
|
|
692
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
693
|
+
const cache = createCache();
|
|
694
|
+
const config = {
|
|
695
|
+
searchDirectory: '/root',
|
|
696
|
+
excludePatterns: [],
|
|
697
|
+
fileExtensions: ['js'],
|
|
698
|
+
fileNames: ['SPECIAL'],
|
|
699
|
+
verbose: false,
|
|
700
|
+
};
|
|
701
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
702
|
+
|
|
703
|
+
mockFiles = ['/root/SPECIAL'];
|
|
704
|
+
const files = await indexer.discoverFiles();
|
|
705
|
+
|
|
706
|
+
expect(files).toEqual(['/root/SPECIAL']);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('treats NODE_ENV=test as test environment', async () => {
|
|
710
|
+
const oldVitest = process.env.VITEST;
|
|
711
|
+
const oldNodeEnv = process.env.NODE_ENV;
|
|
712
|
+
delete process.env.VITEST;
|
|
713
|
+
process.env.NODE_ENV = 'test';
|
|
714
|
+
|
|
715
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
716
|
+
const cache = createCache();
|
|
717
|
+
const config = {
|
|
718
|
+
workerThreads: 2,
|
|
719
|
+
verbose: true,
|
|
720
|
+
embeddingModel: 'test-model',
|
|
721
|
+
excludePatterns: [],
|
|
722
|
+
};
|
|
723
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
workerMessageType = 'ready';
|
|
727
|
+
await indexer.initializeWorkers();
|
|
728
|
+
} finally {
|
|
729
|
+
if (oldVitest === undefined) {
|
|
730
|
+
delete process.env.VITEST;
|
|
731
|
+
} else {
|
|
732
|
+
process.env.VITEST = oldVitest;
|
|
733
|
+
}
|
|
734
|
+
process.env.NODE_ENV = oldNodeEnv;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('uses production timeouts when not in test env', async () => {
|
|
739
|
+
const oldVitest = process.env.VITEST;
|
|
740
|
+
const oldNodeEnv = process.env.NODE_ENV;
|
|
741
|
+
delete process.env.VITEST;
|
|
742
|
+
process.env.NODE_ENV = 'production';
|
|
743
|
+
|
|
744
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
745
|
+
const cache = createCache();
|
|
746
|
+
const config = {
|
|
747
|
+
workerThreads: 2,
|
|
748
|
+
verbose: false,
|
|
749
|
+
embeddingModel: 'test-model',
|
|
750
|
+
excludePatterns: [],
|
|
751
|
+
};
|
|
752
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
workerMessageType = 'ready';
|
|
756
|
+
await indexer.initializeWorkers();
|
|
757
|
+
|
|
758
|
+
const worker = {
|
|
759
|
+
postMessage: vi.fn(),
|
|
760
|
+
once: (event, handler) => {
|
|
761
|
+
if (event === 'exit') handler();
|
|
762
|
+
},
|
|
763
|
+
terminate: vi.fn().mockResolvedValue(undefined),
|
|
764
|
+
};
|
|
765
|
+
indexer.workers = [worker];
|
|
766
|
+
await indexer.terminateWorkers();
|
|
767
|
+
|
|
768
|
+
let handler;
|
|
769
|
+
const worker2 = {
|
|
770
|
+
on: (event, fn) => {
|
|
771
|
+
if (event === 'message') handler = fn;
|
|
772
|
+
},
|
|
773
|
+
once: () => {},
|
|
774
|
+
off: () => {},
|
|
775
|
+
postMessage: (msg) => {
|
|
776
|
+
handler({ type: 'results', results: [{ success: true }], batchId: msg.batchId });
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
indexer.workers = [worker2];
|
|
780
|
+
await indexer.processChunksWithWorkers([{ file: 'a.js', text: 'x' }]);
|
|
781
|
+
} finally {
|
|
782
|
+
if (oldVitest === undefined) {
|
|
783
|
+
delete process.env.VITEST;
|
|
784
|
+
} else {
|
|
785
|
+
process.env.VITEST = oldVitest;
|
|
786
|
+
}
|
|
787
|
+
process.env.NODE_ENV = oldNodeEnv;
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('sets memory timer when verbose is true', async () => {
|
|
792
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
793
|
+
const cache = createCache();
|
|
794
|
+
const config = {
|
|
795
|
+
searchDirectory: '/root',
|
|
796
|
+
excludePatterns: [],
|
|
797
|
+
fileExtensions: ['js'],
|
|
798
|
+
fileNames: [],
|
|
799
|
+
batchSize: 1,
|
|
800
|
+
maxFileSize: 1000,
|
|
801
|
+
callGraphEnabled: false,
|
|
802
|
+
verbose: true,
|
|
803
|
+
};
|
|
804
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
805
|
+
|
|
806
|
+
mockFiles = ['/root/a.js'];
|
|
807
|
+
indexer.preFilterFiles = vi
|
|
808
|
+
.fn()
|
|
809
|
+
.mockResolvedValue([{ file: '/root/a.js', content: 'code', hash: 'h' }]);
|
|
810
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
811
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([
|
|
812
|
+
{ file: '/root/a.js', startLine: 1, endLine: 1, content: 'a', vector: [1], success: true },
|
|
813
|
+
]);
|
|
814
|
+
|
|
815
|
+
vi.useFakeTimers();
|
|
816
|
+
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
|
817
|
+
|
|
818
|
+
await indexer.indexAll(false);
|
|
819
|
+
expect(setIntervalSpy).toHaveBeenCalled();
|
|
820
|
+
vi.useRealTimers();
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('skips large preset content without verbose log', async () => {
|
|
824
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
825
|
+
const cache = createCache();
|
|
826
|
+
const config = {
|
|
827
|
+
searchDirectory: '/root',
|
|
828
|
+
excludePatterns: [],
|
|
829
|
+
fileExtensions: ['js'],
|
|
830
|
+
fileNames: [],
|
|
831
|
+
batchSize: 1,
|
|
832
|
+
maxFileSize: 1,
|
|
833
|
+
callGraphEnabled: false,
|
|
834
|
+
verbose: false,
|
|
835
|
+
};
|
|
836
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
837
|
+
|
|
838
|
+
mockFiles = ['/root/large.js'];
|
|
839
|
+
indexer.preFilterFiles = vi
|
|
840
|
+
.fn()
|
|
841
|
+
.mockResolvedValue([{ file: '/root/large.js', content: 'xx', hash: 'h', force: true }]);
|
|
842
|
+
|
|
843
|
+
await indexer.indexAll(false);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('skips stat errors without verbose log', async () => {
|
|
847
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
848
|
+
const cache = createCache();
|
|
849
|
+
const config = {
|
|
850
|
+
searchDirectory: '/root',
|
|
851
|
+
excludePatterns: [],
|
|
852
|
+
fileExtensions: ['js'],
|
|
853
|
+
fileNames: [],
|
|
854
|
+
batchSize: 1,
|
|
855
|
+
maxFileSize: 1000,
|
|
856
|
+
callGraphEnabled: false,
|
|
857
|
+
verbose: false,
|
|
858
|
+
};
|
|
859
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
860
|
+
|
|
861
|
+
mockFiles = ['/root/stat.js'];
|
|
862
|
+
indexer.preFilterFiles = vi
|
|
863
|
+
.fn()
|
|
864
|
+
.mockResolvedValue([{ file: '/root/stat.js', hash: 'h', force: true }]);
|
|
865
|
+
fsMock.stat.mockRejectedValueOnce(new Error('stat fail'));
|
|
866
|
+
|
|
867
|
+
await indexer.indexAll(false);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('skips invalid stat results without verbose log', async () => {
|
|
871
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
872
|
+
const cache = createCache();
|
|
873
|
+
const config = {
|
|
874
|
+
searchDirectory: '/root',
|
|
875
|
+
excludePatterns: [],
|
|
876
|
+
fileExtensions: ['js'],
|
|
877
|
+
fileNames: [],
|
|
878
|
+
batchSize: 1,
|
|
879
|
+
maxFileSize: 1000,
|
|
880
|
+
callGraphEnabled: false,
|
|
881
|
+
verbose: false,
|
|
882
|
+
};
|
|
883
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
884
|
+
|
|
885
|
+
mockFiles = ['/root/invalid.js'];
|
|
886
|
+
indexer.preFilterFiles = vi
|
|
887
|
+
.fn()
|
|
888
|
+
.mockResolvedValue([{ file: '/root/invalid.js', hash: 'h', force: true }]);
|
|
889
|
+
fsMock.stat.mockResolvedValueOnce({});
|
|
890
|
+
|
|
891
|
+
await indexer.indexAll(false);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('skips oversized files without verbose log', async () => {
|
|
895
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
896
|
+
const cache = createCache();
|
|
897
|
+
const config = {
|
|
898
|
+
searchDirectory: '/root',
|
|
899
|
+
excludePatterns: [],
|
|
900
|
+
fileExtensions: ['js'],
|
|
901
|
+
fileNames: [],
|
|
902
|
+
batchSize: 1,
|
|
903
|
+
maxFileSize: 1,
|
|
904
|
+
callGraphEnabled: false,
|
|
905
|
+
verbose: false,
|
|
906
|
+
};
|
|
907
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
908
|
+
|
|
909
|
+
mockFiles = ['/root/big.js'];
|
|
910
|
+
indexer.preFilterFiles = vi
|
|
911
|
+
.fn()
|
|
912
|
+
.mockResolvedValue([{ file: '/root/big.js', hash: 'h', force: true }]);
|
|
913
|
+
fsMock.stat.mockResolvedValueOnce({ isDirectory: () => false, size: 10 });
|
|
914
|
+
|
|
915
|
+
await indexer.indexAll(false);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('skips read failures without verbose log', async () => {
|
|
919
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
920
|
+
const cache = createCache();
|
|
921
|
+
const config = {
|
|
922
|
+
searchDirectory: '/root',
|
|
923
|
+
excludePatterns: [],
|
|
924
|
+
fileExtensions: ['js'],
|
|
925
|
+
fileNames: [],
|
|
926
|
+
batchSize: 1,
|
|
927
|
+
maxFileSize: 1000,
|
|
928
|
+
callGraphEnabled: false,
|
|
929
|
+
verbose: false,
|
|
930
|
+
};
|
|
931
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
932
|
+
|
|
933
|
+
mockFiles = ['/root/read.js'];
|
|
934
|
+
indexer.preFilterFiles = vi
|
|
935
|
+
.fn()
|
|
936
|
+
.mockResolvedValue([{ file: '/root/read.js', hash: 'h', force: true }]);
|
|
937
|
+
fsMock.stat.mockResolvedValueOnce({ isDirectory: () => false, size: 1 });
|
|
938
|
+
fsMock.readFile.mockRejectedValueOnce(new Error('read fail'));
|
|
939
|
+
|
|
940
|
+
await indexer.indexAll(false);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('skips unchanged files without verbose log', async () => {
|
|
944
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
945
|
+
const cache = createCache();
|
|
946
|
+
const config = {
|
|
947
|
+
searchDirectory: '/root',
|
|
948
|
+
excludePatterns: [],
|
|
949
|
+
fileExtensions: ['js'],
|
|
950
|
+
fileNames: [],
|
|
951
|
+
batchSize: 1,
|
|
952
|
+
maxFileSize: 1000,
|
|
953
|
+
callGraphEnabled: false,
|
|
954
|
+
verbose: false,
|
|
955
|
+
};
|
|
956
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
957
|
+
|
|
958
|
+
mockFiles = ['/root/same.js'];
|
|
959
|
+
indexer.preFilterFiles = vi
|
|
960
|
+
.fn()
|
|
961
|
+
.mockResolvedValue([{ file: '/root/same.js', content: 'code', hash: 'h', force: false }]);
|
|
962
|
+
cache.getFileHash.mockReturnValueOnce('h');
|
|
963
|
+
|
|
964
|
+
await indexer.indexAll(false);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('covers non-verbose branches in the batch loop', async () => {
|
|
968
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
969
|
+
const cache = createCache();
|
|
970
|
+
const config = {
|
|
971
|
+
searchDirectory: '/root',
|
|
972
|
+
excludePatterns: [],
|
|
973
|
+
fileExtensions: ['js'],
|
|
974
|
+
fileNames: [],
|
|
975
|
+
batchSize: 10,
|
|
976
|
+
maxFileSize: 5,
|
|
977
|
+
callGraphEnabled: false,
|
|
978
|
+
verbose: false,
|
|
979
|
+
};
|
|
980
|
+
const indexer = new CodebaseIndexer(vi.fn(), cache, config);
|
|
981
|
+
|
|
982
|
+
cpuCount = 1;
|
|
983
|
+
mockFiles = [
|
|
984
|
+
'/root/large-content.js',
|
|
985
|
+
'/root/stat-fail.js',
|
|
986
|
+
'/root/invalid.js',
|
|
987
|
+
'/root/big.js',
|
|
988
|
+
'/root/read-fail.js',
|
|
989
|
+
'/root/unchanged.js',
|
|
990
|
+
'/root/ok.js',
|
|
991
|
+
];
|
|
992
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
993
|
+
{ file: '/root/large-content.js', content: 'xxxxxx', hash: 'h1', force: true },
|
|
994
|
+
{ file: '/root/stat-fail.js', hash: 'h2', force: true },
|
|
995
|
+
{ file: '/root/invalid.js', hash: 'h3', force: true },
|
|
996
|
+
{ file: '/root/big.js', hash: 'h4', force: true },
|
|
997
|
+
{ file: '/root/read-fail.js', hash: 'h5', force: true },
|
|
998
|
+
{ file: '/root/unchanged.js', content: 'same', hash: 'samehash', force: false },
|
|
999
|
+
{ file: '/root/ok.js', hash: 'h6', force: true },
|
|
1000
|
+
]);
|
|
1001
|
+
|
|
1002
|
+
fsMock.stat.mockImplementation(async (filePath) => {
|
|
1003
|
+
const file = String(filePath);
|
|
1004
|
+
if (file.endsWith('stat-fail.js')) {
|
|
1005
|
+
throw new Error('stat fail');
|
|
1006
|
+
}
|
|
1007
|
+
if (file.endsWith('invalid.js')) {
|
|
1008
|
+
return {};
|
|
1009
|
+
}
|
|
1010
|
+
if (file.endsWith('big.js')) {
|
|
1011
|
+
return { isDirectory: () => false, size: 10 };
|
|
1012
|
+
}
|
|
1013
|
+
return { isDirectory: () => false, size: 1 };
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
fsMock.readFile.mockImplementation(async (filePath) => {
|
|
1017
|
+
const file = String(filePath);
|
|
1018
|
+
if (file.endsWith('read-fail.js')) {
|
|
1019
|
+
throw new Error('read fail');
|
|
1020
|
+
}
|
|
1021
|
+
return 'ok';
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
cache.getFileHash.mockImplementation((file) =>
|
|
1025
|
+
String(file).endsWith('unchanged.js') ? 'samehash' : 'other'
|
|
1026
|
+
);
|
|
1027
|
+
smartChunkMock.mockReturnValueOnce([{ text: 'a', startLine: 1, endLine: 1 }]);
|
|
1028
|
+
vi.spyOn(indexer, 'processChunksSingleThreaded').mockResolvedValue([]);
|
|
1029
|
+
|
|
1030
|
+
await indexer.indexAll(false);
|
|
1031
|
+
});
|
|
1032
|
+
});
|