@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.
Files changed (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. 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
+ });