smart-coding-mcp 1.2.4 → 1.3.1
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/README.md +28 -168
- package/config.json +4 -3
- package/example.png +0 -0
- package/features/clear-cache.js +30 -7
- package/features/index-codebase.js +507 -37
- package/how-its-works.png +0 -0
- package/index.js +2 -2
- package/lib/cache.js +5 -0
- package/lib/config.js +29 -4
- package/lib/embedding-worker.js +67 -0
- package/lib/tokenizer.js +142 -0
- package/lib/utils.js +113 -25
- package/package.json +9 -3
- package/test/clear-cache.test.js +288 -0
- package/test/embedding-model.test.js +230 -0
- package/test/helpers.js +128 -0
- package/test/hybrid-search.test.js +243 -0
- package/test/index-codebase.test.js +246 -0
- package/test/integration.test.js +223 -0
- package/test/tokenizer.test.js +225 -0
- package/vitest.config.js +29 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HybridSearch feature
|
|
3
|
+
*
|
|
4
|
+
* Tests the search functionality including:
|
|
5
|
+
* - Semantic search with embeddings
|
|
6
|
+
* - Exact match boosting
|
|
7
|
+
* - Result formatting
|
|
8
|
+
* - Empty index handling
|
|
9
|
+
* - Score calculation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
createTestFixtures,
|
|
15
|
+
cleanupFixtures,
|
|
16
|
+
clearTestCache,
|
|
17
|
+
createMockRequest
|
|
18
|
+
} from './helpers.js';
|
|
19
|
+
import * as HybridSearchFeature from '../features/hybrid-search.js';
|
|
20
|
+
import { HybridSearch } from '../features/hybrid-search.js';
|
|
21
|
+
|
|
22
|
+
describe('HybridSearch', () => {
|
|
23
|
+
let fixtures;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
fixtures = await createTestFixtures({ workerThreads: 2 });
|
|
27
|
+
|
|
28
|
+
// Ensure we have indexed content
|
|
29
|
+
await clearTestCache(fixtures.config);
|
|
30
|
+
fixtures.cache.setVectorStore([]);
|
|
31
|
+
fixtures.cache.fileHashes = new Map();
|
|
32
|
+
await fixtures.indexer.indexAll(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await cleanupFixtures(fixtures);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Search Functionality', () => {
|
|
40
|
+
it('should find relevant code for semantic queries', async () => {
|
|
41
|
+
// Search for something that should exist in the codebase
|
|
42
|
+
const { results, message } = await fixtures.hybridSearch.search('embedding model', 5);
|
|
43
|
+
|
|
44
|
+
expect(message).toBeNull();
|
|
45
|
+
expect(results.length).toBeGreaterThan(0);
|
|
46
|
+
|
|
47
|
+
// Results should have required properties
|
|
48
|
+
for (const result of results) {
|
|
49
|
+
expect(result).toHaveProperty('file');
|
|
50
|
+
expect(result).toHaveProperty('content');
|
|
51
|
+
expect(result).toHaveProperty('score');
|
|
52
|
+
expect(result).toHaveProperty('startLine');
|
|
53
|
+
expect(result).toHaveProperty('endLine');
|
|
54
|
+
expect(result).toHaveProperty('vector');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return results sorted by score (highest first)', async () => {
|
|
59
|
+
const { results } = await fixtures.hybridSearch.search('function', 10);
|
|
60
|
+
|
|
61
|
+
expect(results.length).toBeGreaterThan(1);
|
|
62
|
+
|
|
63
|
+
// Verify descending order
|
|
64
|
+
for (let i = 1; i < results.length; i++) {
|
|
65
|
+
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should respect maxResults parameter', async () => {
|
|
70
|
+
const maxResults = 3;
|
|
71
|
+
const { results } = await fixtures.hybridSearch.search('const', maxResults);
|
|
72
|
+
|
|
73
|
+
expect(results.length).toBeLessThanOrEqual(maxResults);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should boost exact matches', async () => {
|
|
77
|
+
// Search for an exact term that exists
|
|
78
|
+
const { results: exactResults } = await fixtures.hybridSearch.search('embedder', 5);
|
|
79
|
+
|
|
80
|
+
// At least one result should contain the exact term
|
|
81
|
+
const hasExactMatch = exactResults.some(r =>
|
|
82
|
+
r.content.toLowerCase().includes('embedder')
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(hasExactMatch).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle natural language queries', async () => {
|
|
89
|
+
const { results } = await fixtures.hybridSearch.search('where is the configuration loaded', 5);
|
|
90
|
+
|
|
91
|
+
expect(results.length).toBeGreaterThan(0);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Empty Index Handling', () => {
|
|
96
|
+
it('should return helpful message when index is empty', async () => {
|
|
97
|
+
// Create a search instance with empty cache
|
|
98
|
+
const emptyCache = {
|
|
99
|
+
getVectorStore: () => [],
|
|
100
|
+
setVectorStore: () => {},
|
|
101
|
+
getFileHash: () => null,
|
|
102
|
+
setFileHash: () => {}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const emptySearch = new HybridSearch(fixtures.embedder, emptyCache, fixtures.config);
|
|
106
|
+
const { results, message } = await emptySearch.search('test', 5);
|
|
107
|
+
|
|
108
|
+
expect(results.length).toBe(0);
|
|
109
|
+
expect(message).toContain('No code has been indexed');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Result Formatting', () => {
|
|
114
|
+
it('should format results as markdown', async () => {
|
|
115
|
+
const { results } = await fixtures.hybridSearch.search('function', 3);
|
|
116
|
+
const formatted = fixtures.hybridSearch.formatResults(results);
|
|
117
|
+
|
|
118
|
+
// Should contain markdown elements
|
|
119
|
+
expect(formatted).toContain('## Result');
|
|
120
|
+
expect(formatted).toContain('**File:**');
|
|
121
|
+
expect(formatted).toContain('**Lines:**');
|
|
122
|
+
expect(formatted).toContain('```');
|
|
123
|
+
expect(formatted).toContain('Relevance:');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return no matches message for empty results', () => {
|
|
127
|
+
const formatted = fixtures.hybridSearch.formatResults([]);
|
|
128
|
+
|
|
129
|
+
expect(formatted).toContain('No matching code found');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should include relative file paths', async () => {
|
|
133
|
+
const { results } = await fixtures.hybridSearch.search('export', 1);
|
|
134
|
+
const formatted = fixtures.hybridSearch.formatResults(results);
|
|
135
|
+
|
|
136
|
+
// Should not contain absolute paths in the output
|
|
137
|
+
expect(formatted).not.toContain(fixtures.config.searchDirectory);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Score Calculation', () => {
|
|
142
|
+
it('should give higher scores to more relevant results', async () => {
|
|
143
|
+
// Search for a specific term
|
|
144
|
+
const { results } = await fixtures.hybridSearch.search('CodebaseIndexer', 5);
|
|
145
|
+
|
|
146
|
+
if (results.length > 0) {
|
|
147
|
+
// Top result should have high relevance
|
|
148
|
+
expect(results[0].score).toBeGreaterThan(0.3);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should apply semantic weight from config', async () => {
|
|
153
|
+
const { results } = await fixtures.hybridSearch.search('async function', 5);
|
|
154
|
+
|
|
155
|
+
// All results should have positive scores
|
|
156
|
+
for (const result of results) {
|
|
157
|
+
expect(result.score).toBeGreaterThan(0);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Hybrid Search Tool Handler', () => {
|
|
164
|
+
let fixtures;
|
|
165
|
+
|
|
166
|
+
beforeAll(async () => {
|
|
167
|
+
fixtures = await createTestFixtures({ workerThreads: 2 });
|
|
168
|
+
|
|
169
|
+
// Ensure indexed content
|
|
170
|
+
await fixtures.indexer.indexAll(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterAll(async () => {
|
|
174
|
+
await cleanupFixtures(fixtures);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Tool Definition', () => {
|
|
178
|
+
it('should have correct tool definition', () => {
|
|
179
|
+
const toolDef = HybridSearchFeature.getToolDefinition(fixtures.config);
|
|
180
|
+
|
|
181
|
+
expect(toolDef.name).toBe('a_semantic_search');
|
|
182
|
+
expect(toolDef.description).toContain('semantic');
|
|
183
|
+
expect(toolDef.description).toContain('hybrid');
|
|
184
|
+
expect(toolDef.inputSchema.properties.query).toBeDefined();
|
|
185
|
+
expect(toolDef.inputSchema.properties.maxResults).toBeDefined();
|
|
186
|
+
expect(toolDef.inputSchema.required).toContain('query');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should use config default for maxResults', () => {
|
|
190
|
+
const toolDef = HybridSearchFeature.getToolDefinition(fixtures.config);
|
|
191
|
+
|
|
192
|
+
expect(toolDef.inputSchema.properties.maxResults.default).toBe(fixtures.config.maxResults);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Tool Handler', () => {
|
|
197
|
+
it('should return search results for valid query', async () => {
|
|
198
|
+
const request = createMockRequest('a_semantic_search', {
|
|
199
|
+
query: 'function that handles indexing'
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = await HybridSearchFeature.handleToolCall(request, fixtures.hybridSearch);
|
|
203
|
+
|
|
204
|
+
expect(result.content[0].type).toBe('text');
|
|
205
|
+
expect(result.content[0].text).toContain('Result');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should use default maxResults when not provided', async () => {
|
|
209
|
+
const request = createMockRequest('a_semantic_search', {
|
|
210
|
+
query: 'import'
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await HybridSearchFeature.handleToolCall(request, fixtures.hybridSearch);
|
|
214
|
+
|
|
215
|
+
// Should return results (up to default max)
|
|
216
|
+
expect(result.content[0].text.length).toBeGreaterThan(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should respect custom maxResults', async () => {
|
|
220
|
+
const request = createMockRequest('a_semantic_search', {
|
|
221
|
+
query: 'const',
|
|
222
|
+
maxResults: 2
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await HybridSearchFeature.handleToolCall(request, fixtures.hybridSearch);
|
|
226
|
+
|
|
227
|
+
// Count result headers
|
|
228
|
+
const resultCount = (result.content[0].text.match(/## Result/g) || []).length;
|
|
229
|
+
expect(resultCount).toBeLessThanOrEqual(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle queries with no matches gracefully', async () => {
|
|
233
|
+
const request = createMockRequest('a_semantic_search', {
|
|
234
|
+
query: 'xyzzy_nonexistent_symbol_12345'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await HybridSearchFeature.handleToolCall(request, fixtures.hybridSearch);
|
|
238
|
+
|
|
239
|
+
// Should return something (either no matches message or low-score results)
|
|
240
|
+
expect(result.content[0].text.length).toBeGreaterThan(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CodebaseIndexer feature
|
|
3
|
+
*
|
|
4
|
+
* Tests the indexing functionality including:
|
|
5
|
+
* - File discovery and filtering
|
|
6
|
+
* - Chunk generation and embedding
|
|
7
|
+
* - Concurrent indexing protection
|
|
8
|
+
* - Force reindex behavior
|
|
9
|
+
* - Progress notifications
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
createTestFixtures,
|
|
15
|
+
cleanupFixtures,
|
|
16
|
+
clearTestCache,
|
|
17
|
+
createMockRequest,
|
|
18
|
+
measureTime
|
|
19
|
+
} from './helpers.js';
|
|
20
|
+
import * as IndexCodebaseFeature from '../features/index-codebase.js';
|
|
21
|
+
import { CodebaseIndexer } from '../features/index-codebase.js';
|
|
22
|
+
|
|
23
|
+
describe('CodebaseIndexer', () => {
|
|
24
|
+
let fixtures;
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
fixtures = await createTestFixtures({ workerThreads: 2 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await cleanupFixtures(fixtures);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
// Reset state
|
|
36
|
+
fixtures.indexer.isIndexing = false;
|
|
37
|
+
fixtures.indexer.terminateWorkers();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Basic Indexing', () => {
|
|
41
|
+
it('should index files and create embeddings', async () => {
|
|
42
|
+
// Clear cache first
|
|
43
|
+
await clearTestCache(fixtures.config);
|
|
44
|
+
fixtures.cache.setVectorStore([]);
|
|
45
|
+
fixtures.cache.fileHashes = new Map();
|
|
46
|
+
|
|
47
|
+
// Run indexing
|
|
48
|
+
const result = await fixtures.indexer.indexAll(true);
|
|
49
|
+
|
|
50
|
+
// Should have processed files
|
|
51
|
+
expect(result.skipped).toBe(false);
|
|
52
|
+
expect(result.filesProcessed).toBeGreaterThan(0);
|
|
53
|
+
expect(result.chunksCreated).toBeGreaterThan(0);
|
|
54
|
+
expect(result.totalFiles).toBeGreaterThan(0);
|
|
55
|
+
expect(result.totalChunks).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should skip unchanged files on subsequent indexing', async () => {
|
|
59
|
+
// First index
|
|
60
|
+
await fixtures.indexer.indexAll(true);
|
|
61
|
+
|
|
62
|
+
// Second index without force
|
|
63
|
+
const result = await fixtures.indexer.indexAll(false);
|
|
64
|
+
|
|
65
|
+
// Should skip processing (files unchanged)
|
|
66
|
+
expect(result.skipped).toBe(false);
|
|
67
|
+
expect(result.filesProcessed).toBe(0);
|
|
68
|
+
expect(result.message).toContain('up to date');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should reindex all files when force is true', async () => {
|
|
72
|
+
// First index
|
|
73
|
+
await fixtures.indexer.indexAll(true);
|
|
74
|
+
const firstChunks = fixtures.cache.getVectorStore().length;
|
|
75
|
+
|
|
76
|
+
// Force reindex
|
|
77
|
+
const result = await fixtures.indexer.indexAll(true);
|
|
78
|
+
|
|
79
|
+
// Should have processed all files again
|
|
80
|
+
expect(result.filesProcessed).toBeGreaterThan(0);
|
|
81
|
+
expect(result.chunksCreated).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Concurrent Indexing Protection', () => {
|
|
86
|
+
it('should prevent concurrent indexing', async () => {
|
|
87
|
+
// Clear for clean state
|
|
88
|
+
await clearTestCache(fixtures.config);
|
|
89
|
+
fixtures.cache.setVectorStore([]);
|
|
90
|
+
fixtures.cache.fileHashes = new Map();
|
|
91
|
+
|
|
92
|
+
// Start first indexing
|
|
93
|
+
const promise1 = fixtures.indexer.indexAll(true);
|
|
94
|
+
|
|
95
|
+
// Wait for it to start
|
|
96
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
97
|
+
expect(fixtures.indexer.isIndexing).toBe(true);
|
|
98
|
+
|
|
99
|
+
// Second call should be skipped
|
|
100
|
+
const result2 = await fixtures.indexer.indexAll(false);
|
|
101
|
+
|
|
102
|
+
expect(result2.skipped).toBe(true);
|
|
103
|
+
expect(result2.reason).toContain('already in progress');
|
|
104
|
+
|
|
105
|
+
await promise1;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should set and clear isIndexing flag correctly', async () => {
|
|
109
|
+
// Clear cache to ensure indexing actually runs
|
|
110
|
+
await clearTestCache(fixtures.config);
|
|
111
|
+
fixtures.cache.setVectorStore([]);
|
|
112
|
+
fixtures.cache.fileHashes = new Map();
|
|
113
|
+
|
|
114
|
+
expect(fixtures.indexer.isIndexing).toBe(false);
|
|
115
|
+
|
|
116
|
+
const promise = fixtures.indexer.indexAll(true);
|
|
117
|
+
|
|
118
|
+
// Should be set during indexing
|
|
119
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
120
|
+
expect(fixtures.indexer.isIndexing).toBe(true);
|
|
121
|
+
|
|
122
|
+
await promise;
|
|
123
|
+
|
|
124
|
+
// Should be cleared after indexing
|
|
125
|
+
expect(fixtures.indexer.isIndexing).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('File Discovery', () => {
|
|
130
|
+
it('should discover files matching configured extensions', async () => {
|
|
131
|
+
const files = await fixtures.indexer.discoverFiles();
|
|
132
|
+
|
|
133
|
+
expect(files.length).toBeGreaterThan(0);
|
|
134
|
+
|
|
135
|
+
// All files should have valid extensions
|
|
136
|
+
const extensions = fixtures.config.fileExtensions.map(ext => `.${ext}`);
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const ext = file.substring(file.lastIndexOf('.'));
|
|
139
|
+
expect(extensions).toContain(ext);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should exclude files in excluded directories', async () => {
|
|
144
|
+
const files = await fixtures.indexer.discoverFiles();
|
|
145
|
+
|
|
146
|
+
// No files from node_modules
|
|
147
|
+
const nodeModulesFiles = files.filter(f => f.includes('node_modules'));
|
|
148
|
+
expect(nodeModulesFiles.length).toBe(0);
|
|
149
|
+
|
|
150
|
+
// No files from .smart-coding-cache
|
|
151
|
+
const cacheFiles = files.filter(f => f.includes('.smart-coding-cache'));
|
|
152
|
+
expect(cacheFiles.length).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Worker Thread Management', () => {
|
|
157
|
+
it('should initialize workers when CPU count > 1', async () => {
|
|
158
|
+
await fixtures.indexer.initializeWorkers();
|
|
159
|
+
|
|
160
|
+
// Should have at least 1 worker on multi-core systems
|
|
161
|
+
expect(fixtures.indexer.workers.length).toBeGreaterThanOrEqual(0);
|
|
162
|
+
|
|
163
|
+
fixtures.indexer.terminateWorkers();
|
|
164
|
+
expect(fixtures.indexer.workers.length).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Index Codebase Tool Handler', () => {
|
|
170
|
+
let fixtures;
|
|
171
|
+
|
|
172
|
+
beforeAll(async () => {
|
|
173
|
+
fixtures = await createTestFixtures({ workerThreads: 2 });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
afterAll(async () => {
|
|
177
|
+
await cleanupFixtures(fixtures);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
beforeEach(async () => {
|
|
181
|
+
fixtures.indexer.isIndexing = false;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Tool Definition', () => {
|
|
185
|
+
it('should have correct tool definition', () => {
|
|
186
|
+
const toolDef = IndexCodebaseFeature.getToolDefinition();
|
|
187
|
+
|
|
188
|
+
expect(toolDef.name).toBe('b_index_codebase');
|
|
189
|
+
expect(toolDef.description).toContain('reindex');
|
|
190
|
+
expect(toolDef.inputSchema.properties.force).toBeDefined();
|
|
191
|
+
expect(toolDef.inputSchema.properties.force.type).toBe('boolean');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Tool Handler', () => {
|
|
196
|
+
it('should return success message on completed indexing', async () => {
|
|
197
|
+
const request = createMockRequest('b_index_codebase', { force: false });
|
|
198
|
+
const result = await IndexCodebaseFeature.handleToolCall(request, fixtures.indexer);
|
|
199
|
+
|
|
200
|
+
expect(result.content[0].text).toContain('reindexed successfully');
|
|
201
|
+
expect(result.content[0].text).toContain('Total files in index');
|
|
202
|
+
expect(result.content[0].text).toContain('Total code chunks');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return skipped message on concurrent calls', async () => {
|
|
206
|
+
// Start first indexing
|
|
207
|
+
await clearTestCache(fixtures.config);
|
|
208
|
+
fixtures.cache.setVectorStore([]);
|
|
209
|
+
fixtures.cache.fileHashes = new Map();
|
|
210
|
+
|
|
211
|
+
const promise1 = IndexCodebaseFeature.handleToolCall(
|
|
212
|
+
createMockRequest('b_index_codebase', { force: true }),
|
|
213
|
+
fixtures.indexer
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
217
|
+
|
|
218
|
+
// Second concurrent call
|
|
219
|
+
const result2 = await IndexCodebaseFeature.handleToolCall(
|
|
220
|
+
createMockRequest('b_index_codebase', { force: false }),
|
|
221
|
+
fixtures.indexer
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(result2.content[0].text).toContain('Indexing skipped');
|
|
225
|
+
expect(result2.content[0].text).toContain('already in progress');
|
|
226
|
+
|
|
227
|
+
await promise1;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle force parameter correctly', async () => {
|
|
231
|
+
// First index
|
|
232
|
+
await IndexCodebaseFeature.handleToolCall(
|
|
233
|
+
createMockRequest('b_index_codebase', { force: true }),
|
|
234
|
+
fixtures.indexer
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Non-force should skip unchanged
|
|
238
|
+
const result = await IndexCodebaseFeature.handleToolCall(
|
|
239
|
+
createMockRequest('b_index_codebase', { force: false }),
|
|
240
|
+
fixtures.indexer
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(result.content[0].text).toContain('up to date');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|