@softerist/heuristic-mcp 2.0.0 → 2.1.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.
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractCallData, extractDefinitions, extractCalls, buildCallGraph, getRelatedFiles } from '../lib/call-graph.js';
3
+
4
+ describe('Call Graph Extractor', () => {
5
+ describe('extractDefinitions', () => {
6
+ it('should extract JavaScript function declarations', () => {
7
+ const content = `
8
+ function foo() {}
9
+ async function bar() {}
10
+ const baz = () => {};
11
+ let qux = async () => {};
12
+ `;
13
+ const defs = extractDefinitions(content, 'test.js');
14
+ expect(defs).toContain('foo');
15
+ expect(defs).toContain('bar');
16
+ expect(defs).toContain('baz');
17
+ expect(defs).toContain('qux');
18
+ });
19
+
20
+ it('should extract JavaScript class declarations', () => {
21
+ const content = `
22
+ class MyClass {
23
+ myMethod() {}
24
+ }
25
+ `;
26
+ const defs = extractDefinitions(content, 'test.js');
27
+ expect(defs).toContain('MyClass');
28
+ expect(defs).toContain('myMethod');
29
+ });
30
+
31
+ it('should extract Python definitions', () => {
32
+ const content = `
33
+ def my_function():
34
+ pass
35
+
36
+ class MyClass:
37
+ def method(self):
38
+ pass
39
+ `;
40
+ const defs = extractDefinitions(content, 'test.py');
41
+ expect(defs).toContain('my_function');
42
+ expect(defs).toContain('MyClass');
43
+ expect(defs).toContain('method');
44
+ });
45
+
46
+ it('should extract Go function declarations', () => {
47
+ const content = `
48
+ func main() {}
49
+ func (s *Server) Start() {}
50
+ `;
51
+ const defs = extractDefinitions(content, 'test.go');
52
+ expect(defs).toContain('main');
53
+ expect(defs).toContain('Start');
54
+ });
55
+ });
56
+
57
+ describe('extractCalls', () => {
58
+ it('should extract function calls', () => {
59
+ const content = `
60
+ function main() {
61
+ foo();
62
+ bar.baz();
63
+ await asyncFunc();
64
+ }
65
+ `;
66
+ const calls = extractCalls(content, 'test.js');
67
+ expect(calls).toContain('foo');
68
+ expect(calls).toContain('baz'); // bar.baz() -> extracts 'baz'
69
+ expect(calls).toContain('asyncFunc');
70
+ });
71
+
72
+ it('should exclude built-in keywords', () => {
73
+ const content = `
74
+ if (condition) {}
75
+ for (let i = 0; i < 10; i++) {}
76
+ while (true) {}
77
+ `;
78
+ const calls = extractCalls(content, 'test.js');
79
+ expect(calls).not.toContain('if');
80
+ expect(calls).not.toContain('for');
81
+ expect(calls).not.toContain('while');
82
+ });
83
+
84
+ it('should not extract calls from strings', () => {
85
+ const content = `
86
+ const str = "someFunction()";
87
+ const template = \`anotherFunction()\`;
88
+ `;
89
+ const calls = extractCalls(content, 'test.js');
90
+ expect(calls).not.toContain('someFunction');
91
+ expect(calls).not.toContain('anotherFunction');
92
+ });
93
+ });
94
+
95
+ describe('extractCallData', () => {
96
+ it('should separate definitions from external calls', () => {
97
+ const content = `
98
+ function localFunc() {
99
+ externalFunc();
100
+ localFunc(); // self-reference
101
+ }
102
+ `;
103
+ const result = extractCallData(content, 'test.js');
104
+ expect(result.definitions).toContain('localFunc');
105
+ expect(result.calls).toContain('externalFunc');
106
+ expect(result.calls).not.toContain('localFunc'); // Filtered as self-reference
107
+ });
108
+ });
109
+
110
+ describe('buildCallGraph', () => {
111
+ it('should build graph from file data', () => {
112
+ const fileData = new Map([
113
+ ['/path/a.js', { definitions: ['funcA'], calls: ['funcB'] }],
114
+ ['/path/b.js', { definitions: ['funcB'], calls: ['funcC'] }],
115
+ ['/path/c.js', { definitions: ['funcC'], calls: [] }]
116
+ ]);
117
+
118
+ const graph = buildCallGraph(fileData);
119
+
120
+ expect(graph.defines.get('funcA')).toContain('/path/a.js');
121
+ expect(graph.defines.get('funcB')).toContain('/path/b.js');
122
+ expect(graph.calledBy.get('funcB')).toContain('/path/a.js');
123
+ expect(graph.calledBy.get('funcC')).toContain('/path/b.js');
124
+ });
125
+ });
126
+
127
+ describe('getRelatedFiles', () => {
128
+ it('should find callers and callees', () => {
129
+ const fileData = new Map([
130
+ ['/path/a.js', { definitions: ['funcA'], calls: ['funcB'] }],
131
+ ['/path/b.js', { definitions: ['funcB'], calls: [] }]
132
+ ]);
133
+
134
+ const graph = buildCallGraph(fileData);
135
+ const related = getRelatedFiles(graph, ['funcB'], 1);
136
+
137
+ // funcB is defined in b.js and called by a.js
138
+ expect(related.has('/path/a.js')).toBe(true);
139
+ expect(related.has('/path/b.js')).toBe(true);
140
+ });
141
+ });
142
+ });
@@ -25,7 +25,7 @@ describe('CacheClearer', () => {
25
25
  let fixtures;
26
26
 
27
27
  beforeAll(async () => {
28
- fixtures = await createTestFixtures({ workerThreads: 2 });
28
+ fixtures = await createTestFixtures({ workerThreads: 1 });
29
29
  });
30
30
 
31
31
  afterAll(async () => {
@@ -90,9 +90,6 @@ describe('CacheClearer', () => {
90
90
  fixtures.cache.fileHashes = new Map();
91
91
 
92
92
  const indexPromise = fixtures.indexer.indexAll(true);
93
-
94
- // Wait for indexing to start
95
- await new Promise(resolve => setTimeout(resolve, 100));
96
93
  expect(fixtures.indexer.isIndexing).toBe(true);
97
94
 
98
95
  // Try to clear - should fail
@@ -207,7 +204,7 @@ describe('Clear Cache Tool Handler', () => {
207
204
  let fixtures;
208
205
 
209
206
  beforeAll(async () => {
210
- fixtures = await createTestFixtures({ workerThreads: 2 });
207
+ fixtures = await createTestFixtures({ workerThreads: 1 });
211
208
  });
212
209
 
213
210
  afterAll(async () => {
@@ -250,7 +247,7 @@ describe('Clear Cache Tool Handler', () => {
250
247
  fixtures.cache.fileHashes = new Map();
251
248
 
252
249
  const indexPromise = fixtures.indexer.indexAll(true);
253
- await new Promise(resolve => setTimeout(resolve, 100));
250
+ expect(fixtures.indexer.isIndexing).toBe(true);
254
251
 
255
252
  const request = createMockRequest('c_clear_cache', {});
256
253
  const result = await ClearCacheFeature.handleToolCall(request, fixtures.cacheClearer);
package/test/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Test helper utilities for Smart Coding MCP tests
2
+ * Test helper utilities for Heuristic MCP tests
3
3
  * Provides shared setup, teardown, and mock utilities
4
4
  */
5
5
 
@@ -15,6 +15,8 @@ import path from 'path';
15
15
  // Cached embedder instance (shared across tests for speed)
16
16
  let sharedEmbedder = null;
17
17
 
18
+ const DEFAULT_MOCK_DIMENSIONS = 64;
19
+
18
20
  /**
19
21
  * Get or initialize the shared embedder instance
20
22
  * Loading the model once and reusing saves significant time
@@ -28,6 +30,57 @@ export async function getEmbedder(config) {
28
30
  return sharedEmbedder;
29
31
  }
30
32
 
33
+ function isVitest() {
34
+ return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
35
+ }
36
+
37
+ function hashToken(token) {
38
+ let hash = 2166136261;
39
+ for (let i = 0; i < token.length; i++) {
40
+ hash ^= token.charCodeAt(i);
41
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
42
+ }
43
+ return hash >>> 0;
44
+ }
45
+
46
+ function normalizeVector(vector) {
47
+ let sumSquares = 0;
48
+ for (let i = 0; i < vector.length; i++) {
49
+ sumSquares += vector[i] * vector[i];
50
+ }
51
+ if (sumSquares === 0) {
52
+ vector[0] = 1;
53
+ return vector;
54
+ }
55
+ const norm = Math.sqrt(sumSquares);
56
+ for (let i = 0; i < vector.length; i++) {
57
+ vector[i] /= norm;
58
+ }
59
+ return vector;
60
+ }
61
+
62
+ function createMockEmbedder({ dimensions = DEFAULT_MOCK_DIMENSIONS } = {}) {
63
+ return async (text, options = {}) => {
64
+ const vector = new Float32Array(dimensions);
65
+ const tokens = String(text ?? "")
66
+ .toLowerCase()
67
+ .split(/[^a-z0-9_]+/g)
68
+ .filter(token => token.length > 1);
69
+
70
+ for (const token of tokens) {
71
+ const index = hashToken(token) % dimensions;
72
+ const weight = 1 + Math.min(4, Math.floor(token.length / 4));
73
+ vector[index] += weight;
74
+ }
75
+
76
+ if (options.normalize) {
77
+ normalizeVector(vector);
78
+ }
79
+
80
+ return { data: vector };
81
+ };
82
+ }
83
+
31
84
  /**
32
85
  * Create test fixtures with initialized components
33
86
  * @param {Object} options - Options for fixture creation
@@ -35,20 +88,24 @@ export async function getEmbedder(config) {
35
88
  */
36
89
  export async function createTestFixtures(options = {}) {
37
90
  const config = await loadConfig();
38
-
91
+
39
92
  // Override config for testing if needed
40
93
  if (options.verbose !== undefined) config.verbose = options.verbose;
41
94
  if (options.workerThreads !== undefined) config.workerThreads = options.workerThreads;
42
-
43
- const embedder = await getEmbedder(config);
44
-
95
+ if (isVitest()) config.workerThreads = 1;
96
+
97
+ const useRealEmbedder = options.useRealEmbedder === true;
98
+ const embedder = useRealEmbedder
99
+ ? await getEmbedder(config)
100
+ : createMockEmbedder({ dimensions: options.embeddingDimensions });
101
+
45
102
  const cache = new EmbeddingsCache(config);
46
103
  await cache.load();
47
-
104
+
48
105
  const indexer = new CodebaseIndexer(embedder, cache, config, null);
49
106
  const cacheClearer = new CacheClearer(embedder, cache, config, indexer);
50
107
  const hybridSearch = new HybridSearch(embedder, cache, config);
51
-
108
+
52
109
  return {
53
110
  config,
54
111
  embedder,
@@ -23,7 +23,7 @@ describe('HybridSearch', () => {
23
23
  let fixtures;
24
24
 
25
25
  beforeAll(async () => {
26
- fixtures = await createTestFixtures({ workerThreads: 2 });
26
+ fixtures = await createTestFixtures({ workerThreads: 1 });
27
27
 
28
28
  // Ensure we have indexed content
29
29
  await clearTestCache(fixtures.config);
@@ -164,7 +164,7 @@ describe('Hybrid Search Tool Handler', () => {
164
164
  let fixtures;
165
165
 
166
166
  beforeAll(async () => {
167
- fixtures = await createTestFixtures({ workerThreads: 2 });
167
+ fixtures = await createTestFixtures({ workerThreads: 1 });
168
168
 
169
169
  // Ensure indexed content
170
170
  await fixtures.indexer.indexAll(false);
@@ -24,7 +24,7 @@ describe('CodebaseIndexer', () => {
24
24
  let fixtures;
25
25
 
26
26
  beforeAll(async () => {
27
- fixtures = await createTestFixtures({ workerThreads: 2 });
27
+ fixtures = await createTestFixtures({ workerThreads: 1 });
28
28
  });
29
29
 
30
30
  afterAll(async () => {
@@ -91,9 +91,6 @@ describe('CodebaseIndexer', () => {
91
91
 
92
92
  // Start first indexing
93
93
  const promise1 = fixtures.indexer.indexAll(true);
94
-
95
- // Wait for it to start
96
- await new Promise(resolve => setTimeout(resolve, 100));
97
94
  expect(fixtures.indexer.isIndexing).toBe(true);
98
95
 
99
96
  // Second call should be skipped
@@ -114,9 +111,6 @@ describe('CodebaseIndexer', () => {
114
111
  expect(fixtures.indexer.isIndexing).toBe(false);
115
112
 
116
113
  const promise = fixtures.indexer.indexAll(true);
117
-
118
- // Should be set during indexing
119
- await new Promise(resolve => setTimeout(resolve, 100));
120
114
  expect(fixtures.indexer.isIndexing).toBe(true);
121
115
 
122
116
  await promise;
@@ -170,7 +164,7 @@ describe('Index Codebase Tool Handler', () => {
170
164
  let fixtures;
171
165
 
172
166
  beforeAll(async () => {
173
- fixtures = await createTestFixtures({ workerThreads: 2 });
167
+ fixtures = await createTestFixtures({ workerThreads: 1 });
174
168
  });
175
169
 
176
170
  afterAll(async () => {
@@ -212,8 +206,7 @@ describe('Index Codebase Tool Handler', () => {
212
206
  createMockRequest('b_index_codebase', { force: true }),
213
207
  fixtures.indexer
214
208
  );
215
-
216
- await new Promise(resolve => setTimeout(resolve, 100));
209
+ expect(fixtures.indexer.isIndexing).toBe(true);
217
210
 
218
211
  // Second concurrent call
219
212
  const result2 = await IndexCodebaseFeature.handleToolCall(
@@ -22,7 +22,7 @@ describe('Concurrent Indexing', () => {
22
22
  let fixtures;
23
23
 
24
24
  beforeAll(async () => {
25
- fixtures = await createTestFixtures({ workerThreads: 2 });
25
+ fixtures = await createTestFixtures({ workerThreads: 1, useRealEmbedder: true });
26
26
  });
27
27
 
28
28
  afterAll(async () => {
@@ -110,7 +110,7 @@ describe('Clear Cache Operations', () => {
110
110
  let fixtures;
111
111
 
112
112
  beforeAll(async () => {
113
- fixtures = await createTestFixtures({ workerThreads: 2 });
113
+ fixtures = await createTestFixtures({ workerThreads: 1, useRealEmbedder: true });
114
114
  });
115
115
 
116
116
  afterAll(async () => {
@@ -197,7 +197,7 @@ describe('Tool Handler Response Quality', () => {
197
197
  let fixtures;
198
198
 
199
199
  beforeAll(async () => {
200
- fixtures = await createTestFixtures({ workerThreads: 2 });
200
+ fixtures = await createTestFixtures({ workerThreads: 1, useRealEmbedder: true });
201
201
  });
202
202
 
203
203
  afterAll(async () => {