@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,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
+ });