@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,254 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
createTestFixtures,
|
|
5
|
+
cleanupFixtures,
|
|
6
|
+
clearTestCache,
|
|
7
|
+
} from './helpers.js';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
describe('CodebaseIndexer Edge Cases', () => {
|
|
12
|
+
let fixtures;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
fixtures = await createTestFixtures({ workerThreads: 1 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await cleanupFixtures(fixtures);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
fixtures.indexer.isIndexing = false;
|
|
24
|
+
await fixtures.indexer.terminateWorkers();
|
|
25
|
+
await clearTestCache(fixtures.config);
|
|
26
|
+
fixtures.cache.setVectorStore([]);
|
|
27
|
+
fixtures.cache.clearFileHashes();
|
|
28
|
+
fixtures.config.verbose = true;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should handle race conditions where file status changes during indexing', async () => {
|
|
32
|
+
// Setup a directory with specific files
|
|
33
|
+
const subDir = path.join(fixtures.config.searchDirectory, 'edge_cases');
|
|
34
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const fileInvalidStat = path.join(subDir, 'invalid_stat.js');
|
|
37
|
+
const fileIsDir = path.join(subDir, 'is_dir.js'); // Will become directory
|
|
38
|
+
const fileTooLarge = path.join(subDir, 'too_large.js');
|
|
39
|
+
const fileReadError = path.join(subDir, 'read_error.js');
|
|
40
|
+
const fileUnchanged = path.join(subDir, 'unchanged.js');
|
|
41
|
+
|
|
42
|
+
await fs.writeFile(fileInvalidStat, 'content');
|
|
43
|
+
await fs.writeFile(fileIsDir, 'content');
|
|
44
|
+
await fs.writeFile(fileTooLarge, 'content');
|
|
45
|
+
await fs.writeFile(fileReadError, 'content');
|
|
46
|
+
await fs.writeFile(fileUnchanged, 'content');
|
|
47
|
+
|
|
48
|
+
// Pre-calculate hash for unchanged file
|
|
49
|
+
const { hashContent } = await import('../lib/utils.js');
|
|
50
|
+
const hash = hashContent('content');
|
|
51
|
+
|
|
52
|
+
// We want to verify these specific paths in indexAll loop (lines 805-844)
|
|
53
|
+
// To do that, we need to mock fs calls.
|
|
54
|
+
// Since we are using real fs in other parts (like discovery), we should only mock for these specific files
|
|
55
|
+
// or use a smart mock that falls back to real fs.
|
|
56
|
+
|
|
57
|
+
const realStat = fs.stat;
|
|
58
|
+
const realReadFile = fs.readFile;
|
|
59
|
+
|
|
60
|
+
let statCallCount = 0;
|
|
61
|
+
|
|
62
|
+
// Spy on fs.stat
|
|
63
|
+
const statSpy = vi.spyOn(fs, 'stat').mockImplementation(async (filePath) => {
|
|
64
|
+
// Allow helper calls and basic setup to pass through
|
|
65
|
+
if (!filePath.includes('edge_cases')) {
|
|
66
|
+
return realStat.call(fs, filePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// During preFilterFiles (first pass), everything looks normal
|
|
70
|
+
// We can detect "second pass" (inside loop) roughly by context or count,
|
|
71
|
+
// but simpler is to check if we are already mocked "bad" state.
|
|
72
|
+
// However, preFilter calls stat, then readFile.
|
|
73
|
+
// Loop calls stat, then readFile.
|
|
74
|
+
|
|
75
|
+
// Let's rely on the fact that preFilter runs first.
|
|
76
|
+
// But preFilter runs in parallel.
|
|
77
|
+
// We can check the stack trace or just count calls? No, count is flaky.
|
|
78
|
+
|
|
79
|
+
// Better strategy: mocking these files specifically to be "normal" first, then "weird".
|
|
80
|
+
// But simpler: preFilter checks size. Loop checks size again.
|
|
81
|
+
|
|
82
|
+
// invalid_stat.js:
|
|
83
|
+
// preFilter: returns normal stat.
|
|
84
|
+
// Loop: returns null or non-function isDirectory? (Line 804 check: !stats || typeof stats.isDirectory ...)
|
|
85
|
+
|
|
86
|
+
// We can use a Map to track how many times a file has been stat-ed.
|
|
87
|
+
// 1st time (preFilter): good.
|
|
88
|
+
// 2nd time (loop): bad.
|
|
89
|
+
|
|
90
|
+
const p = path.normalize(filePath);
|
|
91
|
+
|
|
92
|
+
// invalid_stat.js
|
|
93
|
+
if (p.includes('invalid_stat.js')) {
|
|
94
|
+
// Return a valid stat object that has a "bad" isDirectory property on 2nd call?
|
|
95
|
+
// Actually line 804 checks: if (!stats || typeof stats.isDirectory !== 'function')
|
|
96
|
+
// So we can return { isDirectory: 'not a function' } or null?
|
|
97
|
+
// But preFilter needs it to be valid.
|
|
98
|
+
// preFilter checks: stats.isDirectory() (line 518).
|
|
99
|
+
|
|
100
|
+
// So we need stateful mock.
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return realStat.call(fs, filePath);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// We can't easily state-track inside the mock without variables from closure.
|
|
107
|
+
// Let's use a counter map.
|
|
108
|
+
const callCounts = new Map();
|
|
109
|
+
const getCount = (f) => {
|
|
110
|
+
const c = callCounts.get(f) || 0;
|
|
111
|
+
callCounts.set(f, c + 1);
|
|
112
|
+
return c;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
statSpy.mockImplementation(async (filePath) => {
|
|
116
|
+
if (!filePath.toString().includes('edge_cases')) return realStat.call(fs, filePath);
|
|
117
|
+
|
|
118
|
+
const f = path.normalize(filePath.toString());
|
|
119
|
+
const count = getCount(f);
|
|
120
|
+
|
|
121
|
+
// Files need to pass preFilter (count 0)
|
|
122
|
+
// Then fail in loop (count 1)
|
|
123
|
+
|
|
124
|
+
if (f.includes('invalid_stat.js')) {
|
|
125
|
+
if (count === 0) return realStat.call(fs, filePath); // preFilter pass
|
|
126
|
+
return { isDirectory: 'not-a-function', size: 100 }; // Loop fail (line 804)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (f.includes('is_dir.js')) {
|
|
130
|
+
if (count === 0) return realStat.call(fs, filePath); // preFilter pass
|
|
131
|
+
return { isDirectory: () => true, size: 100 }; // Loop fail (line 813)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (f.includes('too_large.js')) {
|
|
135
|
+
if (count === 0) return realStat.call(fs, filePath); // preFilter pass
|
|
136
|
+
return { isDirectory: () => false, size: 1024 * 1024 * 100 }; // Loop fail (line 817) - 100MB
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// read_error.js passes stat both times
|
|
140
|
+
// unchanged.js passes stat both times
|
|
141
|
+
|
|
142
|
+
return realStat.call(fs, filePath);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Mock readFile
|
|
146
|
+
const readFileSpy = vi.spyOn(fs, 'readFile').mockImplementation(async (filePath, options) => {
|
|
147
|
+
if (!filePath.toString().includes('edge_cases')) return realReadFile.call(fs, filePath, options);
|
|
148
|
+
|
|
149
|
+
const f = path.normalize(filePath.toString());
|
|
150
|
+
// preFilter calls readFile too!
|
|
151
|
+
// We need read_error.js to pass preFilter readFile, but fail loop readFile.
|
|
152
|
+
|
|
153
|
+
// Note: preFilter calls readFile (line 543)
|
|
154
|
+
// Loop calls readFile (line 827)
|
|
155
|
+
|
|
156
|
+
// Reset counts? No, distinct from stat.
|
|
157
|
+
// But we can use the same map key suffix or just assume strict ordering.
|
|
158
|
+
// Let's assume preFilter reads it first.
|
|
159
|
+
|
|
160
|
+
if (f.includes('read_error.js')) {
|
|
161
|
+
// Check if this is likely the second read (loop)
|
|
162
|
+
// We can just set a flag on the file object? No.
|
|
163
|
+
// Let's use a separate counter for reads.
|
|
164
|
+
// Actually, we can reuse the callCounts map logic or distinct one.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return realReadFile.call(fs, filePath, options);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const readCounts = new Map();
|
|
171
|
+
readFileSpy.mockImplementation(async (filePath, options) => {
|
|
172
|
+
if (!filePath.toString().includes('edge_cases')) return realReadFile.call(fs, filePath, options);
|
|
173
|
+
|
|
174
|
+
const f = path.normalize(filePath.toString());
|
|
175
|
+
const count = (readCounts.get(f) || 0);
|
|
176
|
+
readCounts.set(f, count + 1);
|
|
177
|
+
|
|
178
|
+
if (f.includes('read_error.js')) {
|
|
179
|
+
if (count === 0) return realReadFile.call(fs, filePath, options); // preFilter pass
|
|
180
|
+
throw new Error('Simulated read failure'); // Loop fail (line 828)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (f.includes('unchanged.js')) {
|
|
184
|
+
// Loop pass read, but then we want to simulate "liveHash === cache" (line 840)
|
|
185
|
+
// The file content is 'content'. Hash is known.
|
|
186
|
+
// The test simply needs to ensure the cache HAS this hash before loop check?
|
|
187
|
+
// But indexAll clears cache if force=true?
|
|
188
|
+
// If force=false, preFilter skips it.
|
|
189
|
+
// So we must run with force=true (to bypass preFilter skip),
|
|
190
|
+
// BUT force=true sets "force" flag in batch item (line 707).
|
|
191
|
+
// Inside loop: if (!force && liveHash && ...)
|
|
192
|
+
// So if force is true, Line 840 is skipped!
|
|
193
|
+
|
|
194
|
+
// Wait. Line 840: if (!force && liveHash && ...)
|
|
195
|
+
// If we run indexAll(true), force is true. Line 840 is skipped.
|
|
196
|
+
// If we run indexAll(false), preFilter skips unchanged files.
|
|
197
|
+
|
|
198
|
+
// How to hit Line 840 then?
|
|
199
|
+
// "liveHash" comes from re-reading the file.
|
|
200
|
+
// "presetHash" comes from preFilter.
|
|
201
|
+
|
|
202
|
+
// If preFilter says "changed" (hash mismatch), it returns { file, hash, force: false }.
|
|
203
|
+
// Then in loop, if we re-read file and hash NOW matches cache...
|
|
204
|
+
// That means cache was updated between preFilter and loop?
|
|
205
|
+
// OR preFilter thought it was changed, but loop found it matches cache?
|
|
206
|
+
|
|
207
|
+
// Example scenario:
|
|
208
|
+
// 1. preFilter sees file is changed (vs cache).
|
|
209
|
+
// 2. Loop runs.
|
|
210
|
+
// 3. Someone updates cache to match file (race condition?).
|
|
211
|
+
// 4. Line 840 check sees match and skips.
|
|
212
|
+
|
|
213
|
+
// To simulate this:
|
|
214
|
+
// - preFilter must see mismatch (e.g. cache has 'old', file has 'new').
|
|
215
|
+
// - Loop runs.
|
|
216
|
+
// - Before line 840, we sneakily update cache to have 'new'.
|
|
217
|
+
// - OR we mock readFile in loop to return content that matches 'old' (if cache has 'old')?
|
|
218
|
+
|
|
219
|
+
// Let's try:
|
|
220
|
+
// Cache has 'content'.
|
|
221
|
+
// preFilter Mock: returns { file, hash: 'diff', force: false } ?
|
|
222
|
+
// No, preFilter reads real file.
|
|
223
|
+
|
|
224
|
+
// Setup:
|
|
225
|
+
// Real file has 'content'.
|
|
226
|
+
// Cache has 'old'.
|
|
227
|
+
// preFilter reads 'content'. 'content' != 'old'. Passes.
|
|
228
|
+
// Loop starts.
|
|
229
|
+
// Loop reads 'content'. Hash is 'H(content)'.
|
|
230
|
+
// We want (cache.getFileHash(file) === 'H(content)').
|
|
231
|
+
// But cache has 'old'.
|
|
232
|
+
// So we need to update cache to 'H(content)' AFTER preFilter but BEFORE loop check.
|
|
233
|
+
|
|
234
|
+
// We can do this in the `fs.stat` mock for `unchanged.js` (which runs in loop before read).
|
|
235
|
+
if (f.includes('unchanged.js') && count > 0) { // inside loop stat
|
|
236
|
+
fixtures.cache.setFileHash(f, hash);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return realReadFile.call(fs, filePath, options);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Set initial cache for unchanged.js to satisfy pre-reqs
|
|
244
|
+
fixtures.cache.setFileHash(fileUnchanged, 'old_value_to_cause_mismatch_initially');
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await fixtures.indexer.indexAll(false); // force=false to enable line 840 check
|
|
248
|
+
} finally {
|
|
249
|
+
statSpy.mockRestore();
|
|
250
|
+
readFileSpy.mockRestore();
|
|
251
|
+
await fs.rm(subDir, { recursive: true, force: true });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CodebaseIndexer } from '../features/index-codebase.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
|
|
5
|
+
// Mock worker_threads
|
|
6
|
+
vi.mock('worker_threads', async () => {
|
|
7
|
+
const { EventEmitter } = await import('events');
|
|
8
|
+
class Worker extends EventEmitter {
|
|
9
|
+
constructor(path, options) {
|
|
10
|
+
super();
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.options = options;
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
this.emit('message', { type: 'ready' });
|
|
15
|
+
}, 10);
|
|
16
|
+
}
|
|
17
|
+
terminate() {
|
|
18
|
+
return Promise.resolve();
|
|
19
|
+
}
|
|
20
|
+
postMessage(msg) {
|
|
21
|
+
if (msg.type === 'process') {
|
|
22
|
+
// Simulate error response for specific batch
|
|
23
|
+
if (msg.batchId.includes('error-batch')) {
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
this.emit('message', {
|
|
26
|
+
type: 'error',
|
|
27
|
+
error: 'Simulated Processing Error',
|
|
28
|
+
batchId: msg.batchId,
|
|
29
|
+
});
|
|
30
|
+
}, 10);
|
|
31
|
+
} else {
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
this.emit('message', {
|
|
34
|
+
type: 'results',
|
|
35
|
+
results: [{ success: true, file: 'test.js' }],
|
|
36
|
+
batchId: msg.batchId,
|
|
37
|
+
});
|
|
38
|
+
}, 10);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { Worker };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
vi.mock('os', async () => {
|
|
47
|
+
const actual = await vi.importActual('os');
|
|
48
|
+
return {
|
|
49
|
+
...actual,
|
|
50
|
+
default: { ...actual, cpus: () => [{}, {}, {}, {}] },
|
|
51
|
+
cpus: () => [{}, {}, {}, {}],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('CodebaseIndexer Error Handling', () => {
|
|
56
|
+
let indexer;
|
|
57
|
+
let config;
|
|
58
|
+
let cache;
|
|
59
|
+
let embedder;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
config = {
|
|
63
|
+
workerThreads: 2,
|
|
64
|
+
verbose: true,
|
|
65
|
+
embeddingModel: 'test-model',
|
|
66
|
+
};
|
|
67
|
+
cache = { save: vi.fn(), getVectorStore: () => [] };
|
|
68
|
+
embedder = vi.fn().mockResolvedValue({ data: [] });
|
|
69
|
+
indexer = new CodebaseIndexer(embedder, cache, config);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await indexer.terminateWorkers();
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle worker error message during processing (Line 253)', async () => {
|
|
78
|
+
await indexer.initializeWorkers();
|
|
79
|
+
|
|
80
|
+
// Force a batch ID that triggers the error in our mock
|
|
81
|
+
// We can't easily force the batchId inside processChunksWithWorkers,
|
|
82
|
+
// but we can rely on the mock behavior if we spy/control Date.now or similar.
|
|
83
|
+
// Actually, the mock above checks for "error-batch" in batchId.
|
|
84
|
+
// But the real code generates `batch-${i}-${Date.now()}`.
|
|
85
|
+
// So we can't easily make it include "error-batch".
|
|
86
|
+
|
|
87
|
+
// Alternative: Replace the worker instance with a custom one that emits error
|
|
88
|
+
// regardless of batchId, but wait, the handler checks batchId match.
|
|
89
|
+
|
|
90
|
+
// Let's modify the indexer.workers[0] directly before calling processChunksWithWorkers
|
|
91
|
+
const worker = indexer.workers[0];
|
|
92
|
+
const originalPostMessage = worker.postMessage;
|
|
93
|
+
|
|
94
|
+
worker.postMessage = (msg) => {
|
|
95
|
+
if (msg.type === 'process') {
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
worker.emit('message', {
|
|
98
|
+
type: 'error',
|
|
99
|
+
error: 'Forced Error',
|
|
100
|
+
batchId: msg.batchId, // Echo back the correct batchId
|
|
101
|
+
});
|
|
102
|
+
}, 10);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const chunks = [{ text: 'content', file: 'f1.js' }];
|
|
107
|
+
|
|
108
|
+
// We expect it to log error and resolve with empty array (and then retry single threaded)
|
|
109
|
+
// Since we mock embedder, the single threaded retry should succeed.
|
|
110
|
+
|
|
111
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
|
|
113
|
+
await indexer.processChunksWithWorkers(chunks);
|
|
114
|
+
|
|
115
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
116
|
+
expect.stringContaining('Worker 0 error: Forced Error')
|
|
117
|
+
);
|
|
118
|
+
consoleSpy.mockRestore();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle terminate error (Line 168)', async () => {
|
|
122
|
+
await indexer.initializeWorkers();
|
|
123
|
+
|
|
124
|
+
// Mock terminate to fail
|
|
125
|
+
indexer.workers[0].terminate = vi.fn().mockRejectedValue(new Error('Terminate fail'));
|
|
126
|
+
|
|
127
|
+
// Should not throw
|
|
128
|
+
await indexer.terminateWorkers();
|
|
129
|
+
|
|
130
|
+
expect(indexer.workers.length).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CodebaseIndexer } from '../features/index-codebase.js';
|
|
3
|
+
import * as utils from '../lib/utils.js';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('fs/promises');
|
|
8
|
+
|
|
9
|
+
// Mock helpers
|
|
10
|
+
vi.mock('../lib/utils.js', async () => {
|
|
11
|
+
const actual = await vi.importActual('../lib/utils.js');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
hashContent: vi.fn(),
|
|
15
|
+
smartChunk: vi.fn().mockReturnValue([]),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock os.cpus (single threaded for simplicity in these tests)
|
|
20
|
+
vi.mock('os', async () => ({
|
|
21
|
+
default: {
|
|
22
|
+
cpus: () => [{}],
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Mock Chokidar
|
|
27
|
+
const mockWatcher = {
|
|
28
|
+
on: vi.fn().mockReturnThis(),
|
|
29
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
};
|
|
31
|
+
vi.mock('chokidar', () => ({
|
|
32
|
+
default: {
|
|
33
|
+
watch: vi.fn(() => mockWatcher),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('CodebaseIndexer Gap Coverage', () => {
|
|
38
|
+
let indexer;
|
|
39
|
+
let mockEmbedder;
|
|
40
|
+
let mockCache;
|
|
41
|
+
let mockConfig;
|
|
42
|
+
let mockServer;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.restoreAllMocks();
|
|
46
|
+
|
|
47
|
+
// Default mocks
|
|
48
|
+
mockEmbedder = vi.fn().mockResolvedValue({ data: [0.1] });
|
|
49
|
+
|
|
50
|
+
mockCache = {
|
|
51
|
+
getFileHash: vi.fn(),
|
|
52
|
+
setFileHash: vi.fn(),
|
|
53
|
+
removeFileFromStore: vi.fn(),
|
|
54
|
+
addToStore: vi.fn(),
|
|
55
|
+
deleteFileHash: vi.fn(),
|
|
56
|
+
save: vi.fn(),
|
|
57
|
+
clearCallGraphData: vi.fn(),
|
|
58
|
+
getVectorStore: vi.fn().mockReturnValue([]),
|
|
59
|
+
setVectorStore: vi.fn(),
|
|
60
|
+
ensureAnnIndex: vi.fn().mockResolvedValue(null),
|
|
61
|
+
pruneCallGraphData: vi.fn(),
|
|
62
|
+
fileCallData: new Map(),
|
|
63
|
+
fileHashes: new Map(),
|
|
64
|
+
rebuildCallGraph: vi.fn(),
|
|
65
|
+
setFileCallData: vi.fn(),
|
|
66
|
+
getFileHashKeys: vi.fn().mockReturnValue([]),
|
|
67
|
+
getFileCallDataKeys: vi.fn().mockImplementation(() => [...mockCache.fileCallData.keys()]),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
mockConfig = {
|
|
71
|
+
searchDirectory: '/test',
|
|
72
|
+
fileExtensions: ['js'],
|
|
73
|
+
fileNames: [],
|
|
74
|
+
excludePatterns: [],
|
|
75
|
+
maxFileSize: 1024, // Small max size for testing
|
|
76
|
+
batchSize: 10,
|
|
77
|
+
verbose: true, // IMPORTANT: Must be true for these tests
|
|
78
|
+
callGraphEnabled: false,
|
|
79
|
+
watchFiles: true,
|
|
80
|
+
workerThreads: 1,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
mockServer = {
|
|
84
|
+
hybridSearch: {
|
|
85
|
+
clearFileModTime: vi.fn(),
|
|
86
|
+
},
|
|
87
|
+
sendNotification: vi.fn(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
indexer = new CodebaseIndexer(mockEmbedder, mockCache, mockConfig, mockServer);
|
|
91
|
+
|
|
92
|
+
// Silence console.warn/log but track calls
|
|
93
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
94
|
+
const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Coverage for lines 825-837: Content provided but too large, or stat error
|
|
98
|
+
it('logs verbose message and skips when content provided is too large', async () => {
|
|
99
|
+
const largeContent = 'x'.repeat(2048); // > 1024
|
|
100
|
+
|
|
101
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/large.js']);
|
|
102
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
103
|
+
{ file: '/test/large.js', content: largeContent, hash: 'abc', force: false }
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
await indexer.indexAll();
|
|
107
|
+
|
|
108
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
109
|
+
expect.stringContaining('Skipped large.js (too large:')
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('logs verbose message when fs.stat fails (if content not provided)', async () => {
|
|
114
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/error.js']);
|
|
115
|
+
// content is undefined so it tries fs.stat
|
|
116
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
117
|
+
{ file: '/test/error.js', content: undefined, hash: undefined, force: false }
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
vi.spyOn(fs, 'stat').mockRejectedValue(new Error('Stat fail'));
|
|
121
|
+
|
|
122
|
+
await indexer.indexAll();
|
|
123
|
+
|
|
124
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
125
|
+
expect.stringContaining('Failed to stat error.js: Stat fail')
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Coverage for line 846: Invalid stat result
|
|
130
|
+
it('logs verbose message when stat result is invalid', async () => {
|
|
131
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/weird.js']);
|
|
132
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
133
|
+
{ file: '/test/weird.js', content: undefined, hash: undefined, force: false }
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Return mock object that is not a directory function or null
|
|
137
|
+
vi.spyOn(fs, 'stat').mockResolvedValue(null);
|
|
138
|
+
|
|
139
|
+
await indexer.indexAll();
|
|
140
|
+
|
|
141
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
142
|
+
expect.stringContaining('Invalid stat result for weird.js')
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Coverage for lines 859-870: File too large via stat, or read error
|
|
147
|
+
it('logs verbose message when file is too large via stat', async () => {
|
|
148
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/large_stat.js']);
|
|
149
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
150
|
+
{ file: '/test/large_stat.js', content: undefined, hash: undefined, force: false }
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
154
|
+
isDirectory: () => false,
|
|
155
|
+
size: 2048, // > 1024
|
|
156
|
+
mtimeMs: 123
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await indexer.indexAll();
|
|
160
|
+
|
|
161
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
162
|
+
expect.stringContaining('Skipped large_stat.js (too large:')
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('logs verbose message when fs.readFile fails', async () => {
|
|
167
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/read_fail.js']);
|
|
168
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
169
|
+
{ file: '/test/read_fail.js', content: undefined, hash: undefined, force: false }
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
173
|
+
isDirectory: () => false,
|
|
174
|
+
size: 100,
|
|
175
|
+
mtimeMs: 123
|
|
176
|
+
});
|
|
177
|
+
vi.spyOn(fs, 'readFile').mockRejectedValue(new Error('Read error'));
|
|
178
|
+
|
|
179
|
+
await indexer.indexAll();
|
|
180
|
+
|
|
181
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
182
|
+
expect.stringContaining('Failed to read read_fail.js: Read error')
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Coverage for line 882: Unchanged file logging
|
|
187
|
+
it('logs verbose message when file is unchanged (hash match)', async () => {
|
|
188
|
+
indexer.discoverFiles = vi.fn().mockResolvedValue(['/test/same.js']);
|
|
189
|
+
indexer.preFilterFiles = vi.fn().mockResolvedValue([
|
|
190
|
+
{ file: '/test/same.js', content: undefined, hash: undefined, force: false }
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
vi.spyOn(fs, 'stat').mockResolvedValue({
|
|
194
|
+
isDirectory: () => false,
|
|
195
|
+
size: 100,
|
|
196
|
+
mtimeMs: 123
|
|
197
|
+
});
|
|
198
|
+
vi.spyOn(fs, 'readFile').mockResolvedValue('content');
|
|
199
|
+
|
|
200
|
+
vi.spyOn(utils, 'hashContent').mockReturnValue('the-hash');
|
|
201
|
+
mockCache.getFileHash.mockReturnValue('the-hash');
|
|
202
|
+
|
|
203
|
+
await indexer.indexAll();
|
|
204
|
+
|
|
205
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
206
|
+
expect.stringContaining('Skipped same.js (unchanged)')
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Coverage for lines 1106, 1126, 1146: Watcher events during indexing
|
|
211
|
+
it('logs verbose messages when queuing watch events during indexing', async () => {
|
|
212
|
+
// 1. Setup watcher
|
|
213
|
+
await indexer.setupFileWatcher();
|
|
214
|
+
|
|
215
|
+
// Get the handlers
|
|
216
|
+
const addHandler = mockWatcher.on.mock.calls.find(c => c[0] === 'add')[1];
|
|
217
|
+
const changeHandler = mockWatcher.on.mock.calls.find(c => c[0] === 'change')[1];
|
|
218
|
+
const unlinkHandler = mockWatcher.on.mock.calls.find(c => c[0] === 'unlink')[1];
|
|
219
|
+
|
|
220
|
+
// 2. Set indexing state
|
|
221
|
+
indexer.isIndexing = true;
|
|
222
|
+
|
|
223
|
+
// 3. Trigger events
|
|
224
|
+
await addHandler('new.js');
|
|
225
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
226
|
+
expect.stringContaining('Queued add event during indexing')
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await changeHandler('changed.js');
|
|
230
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining('Queued change event during indexing')
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
await unlinkHandler('deleted.js');
|
|
235
|
+
expect(console.info).toHaveBeenCalledWith(
|
|
236
|
+
expect.stringContaining('Queued delete event during indexing')
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|