@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,158 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('@xenova/transformers', () => ({
|
|
4
|
+
pipeline: vi.fn(),
|
|
5
|
+
env: {
|
|
6
|
+
backends: {
|
|
7
|
+
onnx: {
|
|
8
|
+
wasm: { numThreads: null },
|
|
9
|
+
numThreads: null,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('worker_threads', () => ({
|
|
15
|
+
parentPort: {
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
postMessage: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
workerData: {
|
|
20
|
+
embeddingModel: 'test-model',
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { pipeline } from '@xenova/transformers';
|
|
25
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
26
|
+
|
|
27
|
+
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
28
|
+
|
|
29
|
+
describe('embedding-worker', () => {
|
|
30
|
+
let exitSpy;
|
|
31
|
+
let messageHandler;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.resetModules();
|
|
35
|
+
messageHandler = null;
|
|
36
|
+
parentPort.on.mockReset();
|
|
37
|
+
parentPort.on.mockImplementation((event, handler) => {
|
|
38
|
+
if (event === 'message') messageHandler = handler;
|
|
39
|
+
});
|
|
40
|
+
parentPort.postMessage.mockReset();
|
|
41
|
+
workerData.embeddingModel = 'test-model';
|
|
42
|
+
pipeline.mockReset();
|
|
43
|
+
pipeline.mockImplementation(() => Promise.resolve({}));
|
|
44
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
exitSpy.mockRestore();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('processes chunks and posts results', async () => {
|
|
52
|
+
pipeline.mockResolvedValue(async () => ({
|
|
53
|
+
data: Float32Array.from([1, 2]),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
await import('../lib/embedding-worker.js');
|
|
57
|
+
await tick();
|
|
58
|
+
|
|
59
|
+
expect(parentPort.postMessage).toHaveBeenCalledWith({ type: 'ready' });
|
|
60
|
+
|
|
61
|
+
await messageHandler({
|
|
62
|
+
type: 'process',
|
|
63
|
+
chunks: [{ file: 'a.js', startLine: 1, endLine: 2, text: 'code' }],
|
|
64
|
+
batchId: 'batch-1',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const resultsCall = parentPort.postMessage.mock.calls.find(
|
|
68
|
+
(call) => call[0]?.type === 'results'
|
|
69
|
+
);
|
|
70
|
+
expect(resultsCall).toBeDefined();
|
|
71
|
+
const [payload, transferList] = resultsCall;
|
|
72
|
+
expect(payload.batchId).toBe('batch-1');
|
|
73
|
+
expect(payload.done).toBe(true);
|
|
74
|
+
expect(payload.results).toHaveLength(1);
|
|
75
|
+
const result = payload.results[0];
|
|
76
|
+
expect(result.vector).toBeInstanceOf(Float32Array);
|
|
77
|
+
expect(Array.from(result.vector)).toEqual([1, 2]);
|
|
78
|
+
expect(transferList).toEqual([result.vector.buffer]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('captures embedding errors per chunk', async () => {
|
|
82
|
+
pipeline.mockResolvedValue(async () => {
|
|
83
|
+
throw new Error('embed fail');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await import('../lib/embedding-worker.js');
|
|
87
|
+
await tick();
|
|
88
|
+
|
|
89
|
+
await messageHandler({
|
|
90
|
+
type: 'process',
|
|
91
|
+
chunks: [{ file: 'b.js', startLine: 3, endLine: 4, text: 'bad' }],
|
|
92
|
+
batchId: 'batch-2',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const message = parentPort.postMessage.mock.calls.find((call) => call[0].type === 'results')[0];
|
|
96
|
+
expect(message.results[0].success).toBe(false);
|
|
97
|
+
expect(message.results[0].error).toBe('embed fail');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('reports initialization failures', async () => {
|
|
101
|
+
pipeline.mockRejectedValue(new Error('init fail'));
|
|
102
|
+
|
|
103
|
+
await import('../lib/embedding-worker.js');
|
|
104
|
+
await tick();
|
|
105
|
+
|
|
106
|
+
expect(parentPort.postMessage).toHaveBeenCalledWith({
|
|
107
|
+
type: 'error',
|
|
108
|
+
error: 'init fail',
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reports process errors when initialization fails', async () => {
|
|
113
|
+
pipeline.mockRejectedValue(new Error('init fail'));
|
|
114
|
+
|
|
115
|
+
await import('../lib/embedding-worker.js');
|
|
116
|
+
await tick();
|
|
117
|
+
|
|
118
|
+
await messageHandler({
|
|
119
|
+
type: 'process',
|
|
120
|
+
chunks: [{ file: 'c.js', startLine: 1, endLine: 2, text: 'x' }],
|
|
121
|
+
batchId: 'batch-3',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(parentPort.postMessage).toHaveBeenCalledWith({
|
|
125
|
+
type: 'error',
|
|
126
|
+
error: 'init fail',
|
|
127
|
+
batchId: 'batch-3',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('shuts down on shutdown messages', async () => {
|
|
132
|
+
pipeline.mockResolvedValue(async () => ({
|
|
133
|
+
data: Float32Array.from([1, 2]),
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
await import('../lib/embedding-worker.js');
|
|
137
|
+
await tick();
|
|
138
|
+
|
|
139
|
+
await messageHandler({ type: 'shutdown' });
|
|
140
|
+
|
|
141
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('ignores unknown message types', async () => {
|
|
145
|
+
await import('../lib/embedding-worker.js');
|
|
146
|
+
await tick();
|
|
147
|
+
|
|
148
|
+
await messageHandler({ type: 'unknown' });
|
|
149
|
+
|
|
150
|
+
// Should throw error for unknown message type
|
|
151
|
+
expect(parentPort.postMessage).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(parentPort.postMessage).toHaveBeenCalledWith({ type: 'ready' });
|
|
153
|
+
expect(parentPort.postMessage).toHaveBeenCalledWith({
|
|
154
|
+
type: 'error',
|
|
155
|
+
error: 'Unknown message type: unknown'
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as annConfig from '../features/ann-config.js';
|
|
3
|
+
import * as findSimilar from '../features/find-similar-code.js';
|
|
4
|
+
|
|
5
|
+
describe('Features Coverage Maximizer', () => {
|
|
6
|
+
describe('ann-config.js', () => {
|
|
7
|
+
it('covers tool definition and handleToolCall actions', async () => {
|
|
8
|
+
expect(annConfig.getToolDefinition()).toBeDefined();
|
|
9
|
+
|
|
10
|
+
const mockCache = {
|
|
11
|
+
getAnnStats: () => ({
|
|
12
|
+
enabled: true,
|
|
13
|
+
indexLoaded: true,
|
|
14
|
+
dirty: false,
|
|
15
|
+
vectorCount: 10,
|
|
16
|
+
minChunksForAnn: 5000,
|
|
17
|
+
config: {
|
|
18
|
+
metric: 'l2',
|
|
19
|
+
dim: 128,
|
|
20
|
+
count: 10,
|
|
21
|
+
m: 16,
|
|
22
|
+
efConstruction: 200,
|
|
23
|
+
efSearch: 50,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
setEfSearch: vi.fn().mockReturnValue({ success: true }),
|
|
27
|
+
invalidateAnnIndex: vi.fn(),
|
|
28
|
+
ensureAnnIndex: vi.fn().mockResolvedValue({}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const tool = new annConfig.AnnConfigTool(mockCache, {});
|
|
32
|
+
|
|
33
|
+
// Action: stats
|
|
34
|
+
const r1 = await annConfig.handleToolCall(
|
|
35
|
+
{ params: { arguments: { action: 'stats' } } },
|
|
36
|
+
tool
|
|
37
|
+
);
|
|
38
|
+
expect(r1.content[0].text).toContain('ANN Index Statistics');
|
|
39
|
+
|
|
40
|
+
// Action: set_ef_search
|
|
41
|
+
const r2 = await annConfig.handleToolCall(
|
|
42
|
+
{ params: { arguments: { action: 'set_ef_search', efSearch: 100 } } },
|
|
43
|
+
tool
|
|
44
|
+
);
|
|
45
|
+
expect(r2.content[0].text).toContain('true');
|
|
46
|
+
|
|
47
|
+
// Action: rebuild
|
|
48
|
+
const r3 = await annConfig.handleToolCall(
|
|
49
|
+
{ params: { arguments: { action: 'rebuild' } } },
|
|
50
|
+
tool
|
|
51
|
+
);
|
|
52
|
+
expect(r3.content[0].text).toContain('true');
|
|
53
|
+
|
|
54
|
+
// Error path: Unknown action
|
|
55
|
+
const r4 = await tool.execute({ action: 'unknown' });
|
|
56
|
+
expect(r4.success).toBe(false);
|
|
57
|
+
expect(tool.formatResults(r4)).toContain('Error');
|
|
58
|
+
|
|
59
|
+
// Missing parameter for set_ef_search (line 27)
|
|
60
|
+
const r5 = await tool.execute({ action: 'set_ef_search' });
|
|
61
|
+
expect(r5.success).toBe(false);
|
|
62
|
+
|
|
63
|
+
// No active ANN index output (line 68)
|
|
64
|
+
const r6 = tool.formatResults({
|
|
65
|
+
enabled: true,
|
|
66
|
+
indexLoaded: false,
|
|
67
|
+
config: null,
|
|
68
|
+
});
|
|
69
|
+
expect(r6).toContain('No active ANN index');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('find-similar-code.js', () => {
|
|
74
|
+
it('covers tool definition and handleToolCall search', async () => {
|
|
75
|
+
expect(findSimilar.getToolDefinition({})).toBeDefined();
|
|
76
|
+
|
|
77
|
+
const mockCache = {
|
|
78
|
+
getVectorStore: () => [
|
|
79
|
+
{
|
|
80
|
+
file: 'a.js',
|
|
81
|
+
content: 'test code line',
|
|
82
|
+
vector: [1, 0],
|
|
83
|
+
startLine: 1,
|
|
84
|
+
endLine: 1,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
queryAnn: vi.fn().mockResolvedValue([0]),
|
|
88
|
+
getChunkVector: (c) => c.vector,
|
|
89
|
+
getChunkContent: (c) => c.content,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const mockEmbedder = vi.fn().mockResolvedValue({ data: new Float32Array([1, 0]) });
|
|
93
|
+
const tool = new findSimilar.FindSimilarCode(mockEmbedder, mockCache, {
|
|
94
|
+
annEnabled: true,
|
|
95
|
+
searchDirectory: '/root',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const request = {
|
|
99
|
+
params: {
|
|
100
|
+
arguments: {
|
|
101
|
+
code: 'different search',
|
|
102
|
+
maxResults: 1,
|
|
103
|
+
minSimilarity: 0.1,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const result = await findSimilar.handleToolCall(request, tool);
|
|
108
|
+
if (!result.content[0].text.includes('Similar Code')) {
|
|
109
|
+
console.info('DEBUG [features]: Result text:', result.content[0].text);
|
|
110
|
+
}
|
|
111
|
+
expect(result.content[0].text).toContain('Similar Code');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('handles search with no results', async () => {
|
|
115
|
+
const mockCache = {
|
|
116
|
+
getVectorStore: () => [],
|
|
117
|
+
queryAnn: vi.fn().mockResolvedValue([]),
|
|
118
|
+
getChunkVector: (c) => c.vector,
|
|
119
|
+
getChunkContent: (c) => c.content,
|
|
120
|
+
};
|
|
121
|
+
const mockEmbedder = vi.fn().mockResolvedValue({ data: [0.1] });
|
|
122
|
+
const tool = new findSimilar.FindSimilarCode(mockEmbedder, mockCache, {
|
|
123
|
+
searchDirectory: '/root',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await findSimilar.handleToolCall(
|
|
127
|
+
{ params: { arguments: { code: 'x' } } },
|
|
128
|
+
tool
|
|
129
|
+
);
|
|
130
|
+
expect(result.content[0].text).toContain('No code has been indexed yet');
|
|
131
|
+
|
|
132
|
+
// Force "No similar code found" message (line 96)
|
|
133
|
+
tool.config.annEnabled = false;
|
|
134
|
+
mockCache.getVectorStore = () => [{ file: 'a.js', content: 'y', vector: [0, 1] }]; // No match
|
|
135
|
+
const r3 = await tool.execute({ code: 'z', minSimilarity: 0.9 });
|
|
136
|
+
await expect(tool.formatResults(r3.results)).resolves.toContain('No similar code patterns found');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { EmbeddingsCache } from '../lib/cache.js';
|
|
4
|
+
import * as callGraph from '../lib/call-graph.js';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import { Worker } from 'worker_threads';
|
|
7
|
+
import EventEmitter from 'events';
|
|
8
|
+
|
|
9
|
+
// Mock worker_threads
|
|
10
|
+
vi.mock('worker_threads', () => {
|
|
11
|
+
return {
|
|
12
|
+
Worker: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Mock fs/promises
|
|
17
|
+
vi.mock('fs/promises', () => {
|
|
18
|
+
return {
|
|
19
|
+
default: {
|
|
20
|
+
stat: vi.fn().mockResolvedValue({ size: 100 }),
|
|
21
|
+
readFile: vi.fn().mockResolvedValue('{}'),
|
|
22
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
rm: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Final Coverage Boost', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('cache.js - Worker Edge Cases', () => {
|
|
35
|
+
const config = {
|
|
36
|
+
enableCache: true,
|
|
37
|
+
cacheDirectory: '/cache',
|
|
38
|
+
fileExtensions: ['js'],
|
|
39
|
+
embeddingModel: 'test-model',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// Default fs behavior
|
|
44
|
+
fs.stat.mockResolvedValue({ size: 100 });
|
|
45
|
+
fs.readFile.mockResolvedValue('{}');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle worker double-settling guard', { timeout: 1000 }, async () => {
|
|
49
|
+
// Setup: only cache file triggers worker
|
|
50
|
+
fs.stat.mockImplementation(async (path) => {
|
|
51
|
+
if (path && path.includes('embeddings.json')) return { size: 6 * 1024 * 1024 };
|
|
52
|
+
return { size: 100 };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Provide valid meta to avoid early returns (though not strictly needed for this test)
|
|
56
|
+
fs.readFile.mockImplementation(async (path) => {
|
|
57
|
+
if (path.includes('meta.json')) return JSON.stringify({ version: 1, embeddingModel: 'test-model' });
|
|
58
|
+
return '{}';
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const mockWorker = new EventEmitter();
|
|
62
|
+
mockWorker.postMessage = vi.fn();
|
|
63
|
+
mockWorker.terminate = vi.fn();
|
|
64
|
+
mockWorker.removeAllListeners = vi.fn();
|
|
65
|
+
Worker.mockImplementation(function() { return mockWorker; });
|
|
66
|
+
|
|
67
|
+
const cache = new EmbeddingsCache(config);
|
|
68
|
+
|
|
69
|
+
// Wait for the worker to attach the 'message' listener
|
|
70
|
+
const workerListenerReady = new Promise((resolve) => {
|
|
71
|
+
mockWorker.on('newListener', (event) => {
|
|
72
|
+
if (event === 'message') resolve();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const loadPromise = cache.load();
|
|
77
|
+
|
|
78
|
+
await workerListenerReady;
|
|
79
|
+
|
|
80
|
+
// Trigger success message
|
|
81
|
+
mockWorker.emit('message', { ok: true, data: [] });
|
|
82
|
+
|
|
83
|
+
// Immediately trigger exit - acts as second "settle" attempt
|
|
84
|
+
mockWorker.emit('exit', 0);
|
|
85
|
+
|
|
86
|
+
await loadPromise;
|
|
87
|
+
|
|
88
|
+
// If it didn't throw, we're good.
|
|
89
|
+
expect(mockWorker.removeAllListeners).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle worker error event', async () => {
|
|
93
|
+
let embeddingsStatCalls = 0;
|
|
94
|
+
fs.stat.mockImplementation(async (path) => {
|
|
95
|
+
if (path && path.includes('embeddings.json')) {
|
|
96
|
+
embeddingsStatCalls++;
|
|
97
|
+
// Only trigger worker (large size) on first attempt.
|
|
98
|
+
// On retry (triggered by load() seeing null result), return small size to use fs.readFile.
|
|
99
|
+
if (embeddingsStatCalls === 1) return { size: 6 * 1024 * 1024 };
|
|
100
|
+
return { size: 100 };
|
|
101
|
+
}
|
|
102
|
+
return { size: 100 };
|
|
103
|
+
});
|
|
104
|
+
fs.readFile.mockImplementation(async (path) => {
|
|
105
|
+
if (path.includes('meta.json')) return JSON.stringify({ version: 1, embeddingModel: 'test-model' });
|
|
106
|
+
return '{}';
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mockWorker = new EventEmitter();
|
|
110
|
+
mockWorker.postMessage = vi.fn();
|
|
111
|
+
mockWorker.terminate = vi.fn();
|
|
112
|
+
mockWorker.removeAllListeners = vi.fn();
|
|
113
|
+
Worker.mockImplementation(function() { return mockWorker; });
|
|
114
|
+
|
|
115
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
116
|
+
|
|
117
|
+
const cache = new EmbeddingsCache(config);
|
|
118
|
+
|
|
119
|
+
const workerListenerReady = new Promise((resolve) => {
|
|
120
|
+
mockWorker.on('newListener', (event) => {
|
|
121
|
+
if (event === 'error') resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const loadPromise = cache.load();
|
|
126
|
+
await workerListenerReady;
|
|
127
|
+
|
|
128
|
+
const error = new Error('Worker exploded');
|
|
129
|
+
mockWorker.emit('error', error);
|
|
130
|
+
|
|
131
|
+
await loadPromise;
|
|
132
|
+
|
|
133
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Worker exploded'));
|
|
134
|
+
consoleSpy.mockRestore();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle worker exit with non-zero code', async () => {
|
|
138
|
+
let embeddingsStatCalls = 0;
|
|
139
|
+
fs.stat.mockImplementation(async (path) => {
|
|
140
|
+
if (path && path.includes('embeddings.json')) {
|
|
141
|
+
embeddingsStatCalls++;
|
|
142
|
+
if (embeddingsStatCalls === 1) return { size: 6 * 1024 * 1024 };
|
|
143
|
+
return { size: 100 };
|
|
144
|
+
}
|
|
145
|
+
return { size: 100 };
|
|
146
|
+
});
|
|
147
|
+
fs.readFile.mockImplementation(async (path) => {
|
|
148
|
+
if (path.includes('meta.json')) return JSON.stringify({ version: 1, embeddingModel: 'test-model' });
|
|
149
|
+
return '{}';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const mockWorker = new EventEmitter();
|
|
153
|
+
mockWorker.postMessage = vi.fn();
|
|
154
|
+
mockWorker.terminate = vi.fn();
|
|
155
|
+
mockWorker.removeAllListeners = vi.fn();
|
|
156
|
+
Worker.mockImplementation(function() { return mockWorker; });
|
|
157
|
+
|
|
158
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
159
|
+
|
|
160
|
+
const cache = new EmbeddingsCache(config);
|
|
161
|
+
|
|
162
|
+
const workerListenerReady = new Promise((resolve) => {
|
|
163
|
+
mockWorker.on('newListener', (event) => {
|
|
164
|
+
if (event === 'exit') resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const loadPromise = cache.load();
|
|
169
|
+
await workerListenerReady;
|
|
170
|
+
|
|
171
|
+
mockWorker.emit('exit', 1);
|
|
172
|
+
|
|
173
|
+
await loadPromise;
|
|
174
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('exited with code 1'));
|
|
175
|
+
consoleSpy.mockRestore();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('cache.js - Verbose Call Graph Loading', () => {
|
|
180
|
+
it('should log when loading call graph in verbose mode', async () => {
|
|
181
|
+
const config = {
|
|
182
|
+
enableCache: true,
|
|
183
|
+
cacheDirectory: '/cache',
|
|
184
|
+
fileExtensions: ['js'],
|
|
185
|
+
embeddingModel: 'test-model',
|
|
186
|
+
verbose: true
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
fs.mkdir.mockResolvedValue(undefined);
|
|
190
|
+
// readFile needs to return null for meta/cache/hash to skip main logic
|
|
191
|
+
// but return valid JSON for call-graph
|
|
192
|
+
fs.readFile.mockImplementation(async (path) => {
|
|
193
|
+
if (path.endsWith('call-graph.json')) {
|
|
194
|
+
return JSON.stringify({ 'file.js': { definitions: [], calls: [] } });
|
|
195
|
+
}
|
|
196
|
+
return null; // triggers "Missing cache metadata" early return, which is after call-graph load?
|
|
197
|
+
// Wait, call-graph load is inside load().
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// We need main cache load to succeed partially or reach the call-graph part.
|
|
201
|
+
// Looking at cache.js:163, it reads meta, cache, hash.
|
|
202
|
+
// If meta missing, it returns. We need meta to exist.
|
|
203
|
+
|
|
204
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
205
|
+
if (filePath.endsWith('meta.json')) {
|
|
206
|
+
return JSON.stringify({ version: 1, embeddingModel: 'test-model' });
|
|
207
|
+
}
|
|
208
|
+
if (filePath.endsWith('embeddings.json')) return '[]';
|
|
209
|
+
if (filePath.endsWith('file-hashes.json')) return '{}';
|
|
210
|
+
if (filePath.endsWith('call-graph.json')) {
|
|
211
|
+
return JSON.stringify({ 'file.js': { definitions: [], calls: [] } });
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Mock fs.stat for readJsonFile to avoid worker
|
|
217
|
+
fs.stat.mockResolvedValue({ size: 100 });
|
|
218
|
+
|
|
219
|
+
const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
220
|
+
|
|
221
|
+
const cache = new EmbeddingsCache(config);
|
|
222
|
+
await cache.load();
|
|
223
|
+
|
|
224
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[Cache] Loaded call-graph data'));
|
|
225
|
+
consoleSpy.mockRestore();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('call-graph.js - Coverage Gaps', () => {
|
|
230
|
+
it('should ignore single-character definitions', () => {
|
|
231
|
+
const content = `
|
|
232
|
+
function a() {}
|
|
233
|
+
function b() {}
|
|
234
|
+
class C {}
|
|
235
|
+
`;
|
|
236
|
+
const defs = callGraph.extractDefinitions(content, 'test.js');
|
|
237
|
+
expect(defs).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should handle merging multiple definitions and calls in buildCallGraph', () => {
|
|
241
|
+
const fileCallData = new Map();
|
|
242
|
+
fileCallData.set('file1.js', { definitions: ['CommonFunc'], calls: ['SharedTarget'] });
|
|
243
|
+
fileCallData.set('file2.js', { definitions: ['CommonFunc'], calls: ['SharedTarget'] });
|
|
244
|
+
// logic at lines 253, 262: checks if map.has(key)
|
|
245
|
+
|
|
246
|
+
const graph = callGraph.buildCallGraph(fileCallData);
|
|
247
|
+
|
|
248
|
+
expect(graph.defines.get('CommonFunc')).toHaveLength(2);
|
|
249
|
+
expect(graph.defines.get('CommonFunc')).toContain('file1.js');
|
|
250
|
+
expect(graph.defines.get('CommonFunc')).toContain('file2.js');
|
|
251
|
+
|
|
252
|
+
expect(graph.calledBy.get('SharedTarget')).toHaveLength(2);
|
|
253
|
+
expect(graph.calledBy.get('SharedTarget')).toContain('file1.js');
|
|
254
|
+
expect(graph.calledBy.get('SharedTarget')).toContain('file2.js');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should ignore short symbols in extractSymbolsFromContent', () => {
|
|
258
|
+
const content = `
|
|
259
|
+
function a() {}
|
|
260
|
+
class B {}
|
|
261
|
+
let c = 1;
|
|
262
|
+
function ValidName() {}
|
|
263
|
+
`;
|
|
264
|
+
const symbols = callGraph.extractSymbolsFromContent(content);
|
|
265
|
+
expect(symbols).not.toContain('a');
|
|
266
|
+
expect(symbols).not.toContain('B');
|
|
267
|
+
expect(symbols).not.toContain('c');
|
|
268
|
+
expect(symbols).toContain('ValidName');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|