@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,220 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// Mocks
|
|
5
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
6
|
+
Server: class MockServer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.capabilities = {};
|
|
9
|
+
this.hybridSearch = null;
|
|
10
|
+
}
|
|
11
|
+
setRequestHandler() {}
|
|
12
|
+
connect() {
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
19
|
+
StdioServerTransport: class MockTransport {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('@xenova/transformers', () => ({
|
|
23
|
+
pipeline: vi.fn().mockResolvedValue({}),
|
|
24
|
+
env: {
|
|
25
|
+
backends: {
|
|
26
|
+
onnx: {
|
|
27
|
+
numThreads: 1,
|
|
28
|
+
wasm: { numThreads: 1 },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('../features/lifecycle.js', () => ({
|
|
35
|
+
stop: vi.fn(),
|
|
36
|
+
start: vi.fn(),
|
|
37
|
+
status: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock config to ensure verbose is true
|
|
41
|
+
vi.mock('../lib/config.js', () => ({
|
|
42
|
+
loadConfig: vi.fn().mockResolvedValue({
|
|
43
|
+
verbose: true,
|
|
44
|
+
searchDirectory: '/mock/search',
|
|
45
|
+
embeddingModel: 'mock-model',
|
|
46
|
+
cacheDirectory: '/mock/cache',
|
|
47
|
+
fileExtensions: ['js'],
|
|
48
|
+
excludePatterns: [],
|
|
49
|
+
}),
|
|
50
|
+
getGlobalCacheDir: () => '/mock/global',
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Mock cache
|
|
54
|
+
vi.mock('../lib/cache.js', () => ({
|
|
55
|
+
EmbeddingsCache: class MockCache {
|
|
56
|
+
load() {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
save() {
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
}
|
|
62
|
+
setVectorStore() {}
|
|
63
|
+
fileHashes = new Map();
|
|
64
|
+
getFileHashKeys() {
|
|
65
|
+
return Array.from(this.fileHashes.keys());
|
|
66
|
+
}
|
|
67
|
+
clearFileHashes() {
|
|
68
|
+
this.fileHashes.clear();
|
|
69
|
+
}
|
|
70
|
+
getFileHashCount() {
|
|
71
|
+
return this.fileHashes.size;
|
|
72
|
+
}
|
|
73
|
+
clearCallGraphData() {}
|
|
74
|
+
pruneCallGraphData() {}
|
|
75
|
+
getVectorStore() {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
ensureAnnIndex() {
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Mock features
|
|
85
|
+
vi.mock('../features/index-codebase.js', async () => {
|
|
86
|
+
return {
|
|
87
|
+
CodebaseIndexer: class MockIndexer {
|
|
88
|
+
indexAll() { return Promise.resolve({}); }
|
|
89
|
+
setupFileWatcher() {}
|
|
90
|
+
terminateWorkers() { return Promise.resolve(); }
|
|
91
|
+
watcher = { close: vi.fn() }
|
|
92
|
+
},
|
|
93
|
+
getToolDefinition: () => ({ name: 'mock_indexer' }),
|
|
94
|
+
handleToolCall: () => {}
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
vi.mock('../features/hybrid-search.js', () => ({
|
|
99
|
+
HybridSearch: class {},
|
|
100
|
+
getToolDefinition: () => ({ name: 'hs' }),
|
|
101
|
+
handleToolCall: () => {}
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
vi.mock('../features/clear-cache.js', () => ({
|
|
105
|
+
CacheClearer: class {},
|
|
106
|
+
getToolDefinition: () => ({ name: 'cc' }),
|
|
107
|
+
handleToolCall: () => {}
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
vi.mock('../features/find-similar-code.js', () => ({
|
|
111
|
+
FindSimilarCode: class {},
|
|
112
|
+
getToolDefinition: () => ({ name: 'fsc' }),
|
|
113
|
+
handleToolCall: () => {}
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
vi.mock('../features/ann-config.js', () => ({
|
|
117
|
+
AnnConfigTool: class {},
|
|
118
|
+
getToolDefinition: () => ({ name: 'ac' }),
|
|
119
|
+
handleToolCall: () => {}
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
vi.mock('../features/register.js', () => ({
|
|
123
|
+
register: vi.fn()
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Mock fs
|
|
127
|
+
vi.mock('fs/promises', async () => {
|
|
128
|
+
return {
|
|
129
|
+
default: {
|
|
130
|
+
access: vi.fn().mockResolvedValue(),
|
|
131
|
+
readFile: vi.fn().mockResolvedValue('{}'),
|
|
132
|
+
stat: vi.fn().mockResolvedValue({ isDirectory: () => false }),
|
|
133
|
+
constants: { F_OK: 0 }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
vi.mock('process', async () => {
|
|
139
|
+
const actual = await vi.importActual('process');
|
|
140
|
+
return {
|
|
141
|
+
...actual,
|
|
142
|
+
exit: vi.fn(),
|
|
143
|
+
memoryUsage: vi.fn()
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
describe('Index.js Memory Logging', () => {
|
|
149
|
+
const oldVitest = process.env.VITEST;
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
process.env.VITEST = 'true';
|
|
153
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
154
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
155
|
+
vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
156
|
+
|
|
157
|
+
// We can't mock process.exit globally easily if not using vitest environment options,
|
|
158
|
+
// but we can spy on it.
|
|
159
|
+
vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
vi.useRealTimers();
|
|
164
|
+
vi.restoreAllMocks();
|
|
165
|
+
if (oldVitest === undefined) {
|
|
166
|
+
delete process.env.VITEST;
|
|
167
|
+
} else {
|
|
168
|
+
process.env.VITEST = oldVitest;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should log memory usage periodically during startup', async () => {
|
|
173
|
+
// Mock memoryUsage
|
|
174
|
+
vi.spyOn(process, 'memoryUsage').mockReturnValue({
|
|
175
|
+
rss: 1024 * 1024 * 100,
|
|
176
|
+
heapUsed: 1024 * 1024 * 50,
|
|
177
|
+
heapTotal: 1024 * 1024 * 80,
|
|
178
|
+
external: 0,
|
|
179
|
+
arrayBuffers: 0
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// We must ensure arguments don't trigger immediate exit
|
|
183
|
+
process.argv = ['node', 'index.js']; // clean args
|
|
184
|
+
|
|
185
|
+
const fsPromises = await import('fs/promises');
|
|
186
|
+
let accessResolve;
|
|
187
|
+
const accessPromise = new Promise((resolve) => {
|
|
188
|
+
accessResolve = resolve;
|
|
189
|
+
});
|
|
190
|
+
fsPromises.default.access.mockReturnValueOnce(accessPromise);
|
|
191
|
+
|
|
192
|
+
// Import the module and call main
|
|
193
|
+
const { main } = await import('../index.js');
|
|
194
|
+
const importPromise = main();
|
|
195
|
+
|
|
196
|
+
// Wait for the first memory log to ensure initialize has reached the interval setup
|
|
197
|
+
await vi.waitFor(() => {
|
|
198
|
+
const calls = console.info.mock.calls.map(c => c[0]).filter(msg => msg && msg.includes('[Server] Memory (startup)'));
|
|
199
|
+
if (calls.length === 0) throw new Error('Not reached yet');
|
|
200
|
+
}, { timeout: 1000, interval: 10 });
|
|
201
|
+
|
|
202
|
+
// Advance time to trigger interval (15000ms)
|
|
203
|
+
await vi.advanceTimersByTimeAsync(16000);
|
|
204
|
+
accessResolve();
|
|
205
|
+
await importPromise;
|
|
206
|
+
|
|
207
|
+
// Check calls
|
|
208
|
+
// It should be called immediately on startup
|
|
209
|
+
// And then periodically
|
|
210
|
+
const calls = console.info.mock.calls.map(c => c[0]).filter(msg => msg && msg.includes('[Server] Memory'));
|
|
211
|
+
const startupCalls = calls.filter((msg) => msg.includes('Memory (startup)'));
|
|
212
|
+
|
|
213
|
+
// Expect at least 2 calls (startup + interval)
|
|
214
|
+
expect(calls.length).toBeGreaterThanOrEqual(2);
|
|
215
|
+
expect(startupCalls.length).toBeGreaterThanOrEqual(2);
|
|
216
|
+
|
|
217
|
+
// We can't guarantee distinct messages if we mocked memoryUsage to static values,
|
|
218
|
+
// but we can verify the COUNT of calls.
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { CodebaseIndexer, handleToolCall } from '../features/index-codebase.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
vi.mock('fs/promises');
|
|
7
|
+
vi.mock('chokidar', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
watch: vi.fn(() => ({
|
|
10
|
+
on: vi.fn().mockReturnThis(),
|
|
11
|
+
close: vi.fn().mockResolvedValue(true),
|
|
12
|
+
})),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock Worker with controlled event firing to avoid hangs
|
|
17
|
+
class MockWorker extends EventEmitter {
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.postMessage = vi.fn((msg) => {
|
|
21
|
+
if (msg.type === 'process') {
|
|
22
|
+
// Simulate processing results immediately
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
this.emit('message', {
|
|
25
|
+
type: 'results',
|
|
26
|
+
batchId: msg.batchId,
|
|
27
|
+
results: msg.chunks.map((c) => ({
|
|
28
|
+
...c,
|
|
29
|
+
success: true,
|
|
30
|
+
vector: [0.1],
|
|
31
|
+
})),
|
|
32
|
+
});
|
|
33
|
+
}, 0);
|
|
34
|
+
} else if (msg.type === 'shutdown') {
|
|
35
|
+
this.emit('exit', 0);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
this.terminate = vi.fn().mockResolvedValue(0);
|
|
39
|
+
this.threadId = Math.random();
|
|
40
|
+
}
|
|
41
|
+
off = this.removeListener;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('CodebaseIndexer Detailed Coverage', () => {
|
|
45
|
+
let indexer;
|
|
46
|
+
let mockEmbedder;
|
|
47
|
+
let mockCache;
|
|
48
|
+
let config;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
mockEmbedder = vi.fn().mockResolvedValue({ data: [1, 2, 3] });
|
|
53
|
+
mockCache = {
|
|
54
|
+
getVectorStore: vi.fn().mockReturnValue([]),
|
|
55
|
+
getFileHash: vi.fn(),
|
|
56
|
+
setFileHash: vi.fn(),
|
|
57
|
+
addToStore: vi.fn(),
|
|
58
|
+
removeFileFromStore: vi.fn(),
|
|
59
|
+
pruneCallGraphData: vi.fn().mockReturnValue(0),
|
|
60
|
+
save: vi.fn().mockResolvedValue(true),
|
|
61
|
+
rebuildCallGraph: vi.fn(),
|
|
62
|
+
ensureAnnIndex: vi.fn().mockResolvedValue({}),
|
|
63
|
+
fileCallData: new Map(),
|
|
64
|
+
getFileHashKeys: vi.fn().mockReturnValue([]),
|
|
65
|
+
};
|
|
66
|
+
config = {
|
|
67
|
+
searchDirectory: '/root',
|
|
68
|
+
embeddingModel: 'test',
|
|
69
|
+
fileExtensions: ['js'],
|
|
70
|
+
watchFiles: true,
|
|
71
|
+
verbose: true,
|
|
72
|
+
workerThreads: 2,
|
|
73
|
+
};
|
|
74
|
+
indexer = new CodebaseIndexer(mockEmbedder, mockCache, config);
|
|
75
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Watcher Gaps', () => {
|
|
79
|
+
it('covers setupFileWatcher patterns (lines 784-794)', async () => {
|
|
80
|
+
indexer.config.fileNames = ['config.json'];
|
|
81
|
+
await indexer.setupFileWatcher();
|
|
82
|
+
expect(indexer.watcher).toBeDefined();
|
|
83
|
+
await indexer.terminateWorkers(); // Cleanup if any
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Tool Call Gaps', () => {
|
|
88
|
+
it('covers handleToolCall statistics (lines 894-896)', async () => {
|
|
89
|
+
const result = {
|
|
90
|
+
skipped: false,
|
|
91
|
+
totalFiles: 1,
|
|
92
|
+
totalChunks: 5,
|
|
93
|
+
filesProcessed: 1,
|
|
94
|
+
chunksCreated: 5,
|
|
95
|
+
message: 'Done',
|
|
96
|
+
};
|
|
97
|
+
const mockIndexer = {
|
|
98
|
+
indexAll: vi.fn().mockResolvedValue(result),
|
|
99
|
+
cache: mockCache,
|
|
100
|
+
};
|
|
101
|
+
const request = { params: { arguments: { force: true } } };
|
|
102
|
+
const response = await handleToolCall(request, mockIndexer);
|
|
103
|
+
|
|
104
|
+
expect(response.content[0].text).toContain('Files processed this run: 1');
|
|
105
|
+
expect(response.content[0].text).toContain('Chunks created this run: 5');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Worker Lifecycle Gaps (Non-hanging)', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
vi.resetModules();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('covers worker creation and ready flow (lines 110-143)', async () => {
|
|
115
|
+
// Mock os
|
|
116
|
+
vi.doMock('os', () => ({
|
|
117
|
+
default: { cpus: () => [{}, {}, {}, {}] },
|
|
118
|
+
cpus: () => [{}, {}, {}, {}],
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// Mock worker_threads
|
|
122
|
+
vi.doMock('worker_threads', () => ({
|
|
123
|
+
Worker: vi.fn(function () {
|
|
124
|
+
const w = new MockWorker();
|
|
125
|
+
// Auto-emit ready on next tick
|
|
126
|
+
setTimeout(() => w.emit('message', { type: 'ready' }), 10);
|
|
127
|
+
return w;
|
|
128
|
+
}),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
132
|
+
const localIndexer = new CodebaseIndexer(mockEmbedder, mockCache, config);
|
|
133
|
+
|
|
134
|
+
await localIndexer.initializeWorkers();
|
|
135
|
+
expect(localIndexer.workers.length).toBe(2);
|
|
136
|
+
|
|
137
|
+
await localIndexer.terminateWorkers();
|
|
138
|
+
expect(localIndexer.workers.length).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('covers worker processing with recovery (lines 197-283)', async () => {
|
|
142
|
+
const worker = new MockWorker();
|
|
143
|
+
// Ready already
|
|
144
|
+
indexer.workers = [worker];
|
|
145
|
+
const chunks = [{ file: 'a.js', text: 'code' }];
|
|
146
|
+
|
|
147
|
+
const results = await indexer.processChunksWithWorkers(chunks);
|
|
148
|
+
expect(results.length).toBe(1);
|
|
149
|
+
expect(results[0].success).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('covers worker initialization failure (lines 151-154)', async () => {
|
|
153
|
+
vi.doMock('worker_threads', () => ({
|
|
154
|
+
Worker: vi.fn(function () {
|
|
155
|
+
const w = new MockWorker();
|
|
156
|
+
// Emit error instead of ready
|
|
157
|
+
setTimeout(() => w.emit('error', new Error('Init fail')), 10);
|
|
158
|
+
return w;
|
|
159
|
+
}),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const { CodebaseIndexer } = await import('../features/index-codebase.js');
|
|
163
|
+
const localIndexer = new CodebaseIndexer(mockEmbedder, mockCache, {
|
|
164
|
+
...config,
|
|
165
|
+
workerThreads: 1,
|
|
166
|
+
}); // Force 1 to hit guard? No, 2 to hit readyPromise catch
|
|
167
|
+
localIndexer.config.workerThreads = 2;
|
|
168
|
+
|
|
169
|
+
await localIndexer.initializeWorkers();
|
|
170
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
171
|
+
expect.stringContaining('Worker initialization failed')
|
|
172
|
+
);
|
|
173
|
+
expect(localIndexer.workers.length).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|