codeguard-testgen 1.0.6 → 1.0.8
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 +262 -5
- package/dist/codebaseIndexer.d.ts.map +1 -1
- package/dist/codebaseIndexer.js +2 -13
- package/dist/codebaseIndexer.js.map +1 -1
- package/dist/fuzzyMatcher.d.ts +53 -0
- package/dist/fuzzyMatcher.d.ts.map +1 -0
- package/dist/fuzzyMatcher.js +349 -0
- package/dist/fuzzyMatcher.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2240 -525
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/promptService.d.ts +0 -65
- package/dist/promptService.d.ts.map +0 -1
- package/dist/promptService.js +0 -173
- package/dist/promptService.js.map +0 -1
- package/dist/prompts.d.ts +0 -12
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -470
- package/dist/prompts.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.generateTests = generateTests;
|
|
|
20
20
|
exports.generateTestsForFolder = generateTestsForFolder;
|
|
21
21
|
exports.generateTestsForFunction = generateTestsForFunction;
|
|
22
22
|
exports.generateTestsForFunctions = generateTestsForFunctions;
|
|
23
|
+
exports.validateAndFixCompleteTestFile = validateAndFixCompleteTestFile;
|
|
23
24
|
exports.executeTool = executeTool;
|
|
24
25
|
exports.analyzeFileAST = analyzeFileAST;
|
|
25
26
|
exports.getFunctionAST = getFunctionAST;
|
|
@@ -27,6 +28,8 @@ exports.getImportsAST = getImportsAST;
|
|
|
27
28
|
exports.getTypeDefinitions = getTypeDefinitions;
|
|
28
29
|
exports.getClassMethods = getClassMethods;
|
|
29
30
|
exports.replaceFunctionTests = replaceFunctionTests;
|
|
31
|
+
exports.searchReplaceBlock = searchReplaceBlock;
|
|
32
|
+
exports.insertAtPosition = insertAtPosition;
|
|
30
33
|
exports.deleteLines = deleteLines;
|
|
31
34
|
exports.insertLines = insertLines;
|
|
32
35
|
exports.replaceLines = replaceLines;
|
|
@@ -43,6 +46,8 @@ const codebaseIndexer_1 = require("./codebaseIndexer");
|
|
|
43
46
|
Object.defineProperty(exports, "CodebaseIndexer", { enumerable: true, get: function () { return codebaseIndexer_1.CodebaseIndexer; } });
|
|
44
47
|
// Configuration loader
|
|
45
48
|
const config_1 = require("./config");
|
|
49
|
+
// Fuzzy matcher for search-replace operations
|
|
50
|
+
const fuzzyMatcher_1 = require("./fuzzyMatcher");
|
|
46
51
|
// Configuration - will be loaded from codeguard.json
|
|
47
52
|
let CONFIG;
|
|
48
53
|
// Global indexer instance (optional - only initialized if user chooses to index)
|
|
@@ -80,7 +85,7 @@ function getAIProviders() {
|
|
|
80
85
|
const TOOLS = [
|
|
81
86
|
{
|
|
82
87
|
name: 'read_file',
|
|
83
|
-
description: '
|
|
88
|
+
description: '⚠️ WARNING: Reads ENTIRE file content. For source files, prefer analyze_file_ast + get_function_ast instead to avoid token waste. Only use this for small config files, test files, or when you absolutely need full file context. Limited to 5000 lines max.',
|
|
84
89
|
input_schema: {
|
|
85
90
|
type: 'object',
|
|
86
91
|
properties: {
|
|
@@ -92,6 +97,28 @@ const TOOLS = [
|
|
|
92
97
|
required: ['file_path']
|
|
93
98
|
}
|
|
94
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: 'read_file_lines',
|
|
102
|
+
description: 'Read a specific portion of a file by line numbers. Useful for examining syntax errors, bracket mismatches, or specific sections that need fixing. Returns lines with line numbers for easy reference.',
|
|
103
|
+
input_schema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
file_path: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'The relative path to the file from the repository root'
|
|
109
|
+
},
|
|
110
|
+
start_line: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'The starting line number (1-indexed)'
|
|
113
|
+
},
|
|
114
|
+
end_line: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'The ending line number (1-indexed)'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ['file_path', 'start_line', 'end_line']
|
|
120
|
+
}
|
|
121
|
+
},
|
|
95
122
|
{
|
|
96
123
|
name: 'analyze_file_ast',
|
|
97
124
|
description: 'Parse file using AST and extract detailed information about all functions, classes, types, and exports',
|
|
@@ -100,7 +127,7 @@ const TOOLS = [
|
|
|
100
127
|
properties: {
|
|
101
128
|
file_path: {
|
|
102
129
|
type: 'string',
|
|
103
|
-
description: 'The relative path to the source file'
|
|
130
|
+
description: 'The relative path to the source file.'
|
|
104
131
|
}
|
|
105
132
|
},
|
|
106
133
|
required: ['file_path']
|
|
@@ -108,7 +135,7 @@ const TOOLS = [
|
|
|
108
135
|
},
|
|
109
136
|
{
|
|
110
137
|
name: 'get_function_ast',
|
|
111
|
-
description: 'Get detailed AST analysis of a specific function including parameters, return type,
|
|
138
|
+
description: 'Get detailed AST analysis of a specific function including full code with JSDoc comments, parameters, return type, async flag, and lists of called functions/methods. Use this instead of read_file for analyzing specific functions.',
|
|
112
139
|
input_schema: {
|
|
113
140
|
type: 'object',
|
|
114
141
|
properties: {
|
|
@@ -118,7 +145,7 @@ const TOOLS = [
|
|
|
118
145
|
},
|
|
119
146
|
function_name: {
|
|
120
147
|
type: 'string',
|
|
121
|
-
description: 'The name of the function to analyze'
|
|
148
|
+
description: 'The name of the function to analyze, this tool must be used for functions information'
|
|
122
149
|
}
|
|
123
150
|
},
|
|
124
151
|
required: ['file_path', 'function_name']
|
|
@@ -152,6 +179,20 @@ const TOOLS = [
|
|
|
152
179
|
required: ['file_path']
|
|
153
180
|
}
|
|
154
181
|
},
|
|
182
|
+
{
|
|
183
|
+
name: 'get_file_preamble',
|
|
184
|
+
description: 'Extract top-level code (imports, jest.mocks, setup blocks, module-level variables) from a file. Captures complete multi-line statements. Perfect for understanding large test files without reading entire content.',
|
|
185
|
+
input_schema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
file_path: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'The relative path to the file'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ['file_path']
|
|
194
|
+
}
|
|
195
|
+
},
|
|
155
196
|
{
|
|
156
197
|
name: 'resolve_import_path',
|
|
157
198
|
description: 'Resolve a relative import path to an absolute path',
|
|
@@ -189,56 +230,8 @@ const TOOLS = [
|
|
|
189
230
|
}
|
|
190
231
|
},
|
|
191
232
|
{
|
|
192
|
-
name: '
|
|
193
|
-
description: 'Write
|
|
194
|
-
input_schema: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
file_path: {
|
|
198
|
-
type: 'string',
|
|
199
|
-
description: 'The path where the test file should be written'
|
|
200
|
-
},
|
|
201
|
-
content: {
|
|
202
|
-
type: 'string',
|
|
203
|
-
description: 'The complete content of the test file'
|
|
204
|
-
},
|
|
205
|
-
source_file: {
|
|
206
|
-
type: 'string',
|
|
207
|
-
description: 'The path to the source file being tested (used to validate all functions are tested)'
|
|
208
|
-
},
|
|
209
|
-
function_mode: {
|
|
210
|
-
type: 'boolean',
|
|
211
|
-
description: 'Whether the tests are being generated function wise.'
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
required: ['file_path', 'content']
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
name: 'edit_test_file',
|
|
219
|
-
description: 'Edit an existing test file by replacing specific sections. If this fails, use write_test_file to overwrite the entire file instead.',
|
|
220
|
-
input_schema: {
|
|
221
|
-
type: 'object',
|
|
222
|
-
properties: {
|
|
223
|
-
file_path: {
|
|
224
|
-
type: 'string',
|
|
225
|
-
description: 'The path to the test file'
|
|
226
|
-
},
|
|
227
|
-
old_content: {
|
|
228
|
-
type: 'string',
|
|
229
|
-
description: 'The content to be replaced (whitespace-normalized matching). Can be empty to overwrite entire file.'
|
|
230
|
-
},
|
|
231
|
-
new_content: {
|
|
232
|
-
type: 'string',
|
|
233
|
-
description: 'The new content to insert'
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
required: ['file_path', 'old_content', 'new_content']
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'replace_function_tests',
|
|
241
|
-
description: 'Replace or add tests for a specific function in an existing test file. This tool PRESERVES all other existing tests! Use this when test file exists and you want to update only specific function tests without affecting other tests.',
|
|
233
|
+
name: 'upsert_function_tests',
|
|
234
|
+
description: 'Write or update tests for a specific function. If test file doesn\'t exist, creates it. If file exists, replaces only the specific function\'s describe block while PRESERVING all other tests. If the function does not exist, the tool will write it to the test file. This is the PRIMARY tool for writing tests!',
|
|
242
235
|
input_schema: {
|
|
243
236
|
type: 'object',
|
|
244
237
|
properties: {
|
|
@@ -248,7 +241,7 @@ const TOOLS = [
|
|
|
248
241
|
},
|
|
249
242
|
function_name: {
|
|
250
243
|
type: 'string',
|
|
251
|
-
description: 'The name of the function whose tests should be
|
|
244
|
+
description: 'The name of the function whose tests should be written/updated'
|
|
252
245
|
},
|
|
253
246
|
new_test_content: {
|
|
254
247
|
type: 'string',
|
|
@@ -260,13 +253,18 @@ const TOOLS = [
|
|
|
260
253
|
},
|
|
261
254
|
{
|
|
262
255
|
name: 'run_tests',
|
|
263
|
-
description: 'Run Jest tests for a specific test file',
|
|
256
|
+
description: 'Run Jest tests for a specific test file. In function-wise mode, only runs tests for specified functions to avoid interference from other failing tests.',
|
|
264
257
|
input_schema: {
|
|
265
258
|
type: 'object',
|
|
266
259
|
properties: {
|
|
267
260
|
test_file_path: {
|
|
268
261
|
type: 'string',
|
|
269
262
|
description: 'The path to the test file to run'
|
|
263
|
+
},
|
|
264
|
+
function_names: {
|
|
265
|
+
type: 'array',
|
|
266
|
+
items: { type: 'string' },
|
|
267
|
+
description: 'Optional: Array of function names to test. When provided, only runs tests matching these function names using Jest -t flag. Use this in function-wise mode to isolate specific function tests.'
|
|
270
268
|
}
|
|
271
269
|
},
|
|
272
270
|
required: ['test_file_path']
|
|
@@ -346,8 +344,8 @@ const TOOLS = [
|
|
|
346
344
|
}
|
|
347
345
|
},
|
|
348
346
|
{
|
|
349
|
-
name: '
|
|
350
|
-
description: '
|
|
347
|
+
name: 'search_replace_block',
|
|
348
|
+
description: '🚀 RECOMMENDED: Search for a code block and replace it with new content. Uses intelligent fuzzy matching to handle whitespace/indentation differences. 10x more reliable than line-based editing! Include 3-5 lines of context before/after the target to make the search unique.',
|
|
351
349
|
input_schema: {
|
|
352
350
|
type: 'object',
|
|
353
351
|
properties: {
|
|
@@ -355,66 +353,50 @@ const TOOLS = [
|
|
|
355
353
|
type: 'string',
|
|
356
354
|
description: 'The path to the file to edit'
|
|
357
355
|
},
|
|
358
|
-
|
|
359
|
-
type: 'number',
|
|
360
|
-
description: 'The starting line number to delete (1-indexed, inclusive)'
|
|
361
|
-
},
|
|
362
|
-
end_line: {
|
|
363
|
-
type: 'number',
|
|
364
|
-
description: 'The ending line number to delete (1-indexed, inclusive)'
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
required: ['file_path', 'start_line', 'end_line']
|
|
368
|
-
}
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: 'insert_lines',
|
|
372
|
-
description: 'Insert new lines at a specific position in a file. Useful for adding missing imports, mocks, or test cases. Use this after reading the file to know exact line numbers.',
|
|
373
|
-
input_schema: {
|
|
374
|
-
type: 'object',
|
|
375
|
-
properties: {
|
|
376
|
-
file_path: {
|
|
356
|
+
search: {
|
|
377
357
|
type: 'string',
|
|
378
|
-
description: 'The
|
|
358
|
+
description: 'The code block to search for. IMPORTANT: Include enough surrounding context (3-5 lines before/after the part you want to change) to make it unique. The tool handles whitespace/indentation differences automatically.'
|
|
379
359
|
},
|
|
380
|
-
|
|
381
|
-
type: '
|
|
382
|
-
description: 'The
|
|
360
|
+
replace: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: 'The new code to replace the search block with. Can be different length than search block.'
|
|
383
363
|
},
|
|
384
|
-
|
|
364
|
+
match_mode: {
|
|
385
365
|
type: 'string',
|
|
386
|
-
|
|
366
|
+
enum: ['exact', 'normalized', 'fuzzy'],
|
|
367
|
+
description: 'Matching strategy. Default: "normalized" (recommended - handles whitespace/indentation). Use "exact" only for very precise matches, "fuzzy" for typo tolerance.'
|
|
387
368
|
}
|
|
388
369
|
},
|
|
389
|
-
required: ['file_path', '
|
|
370
|
+
required: ['file_path', 'search', 'replace']
|
|
390
371
|
}
|
|
391
372
|
},
|
|
392
373
|
{
|
|
393
|
-
name: '
|
|
394
|
-
description: '
|
|
374
|
+
name: 'insert_at_position',
|
|
375
|
+
description: 'Insert content at a specific position in the file (beginning, end, after imports, etc). Simpler than search_replace_block for adding new content.',
|
|
395
376
|
input_schema: {
|
|
396
377
|
type: 'object',
|
|
397
378
|
properties: {
|
|
398
379
|
file_path: {
|
|
399
380
|
type: 'string',
|
|
400
|
-
description: 'The path to the file
|
|
381
|
+
description: 'The path to the file'
|
|
401
382
|
},
|
|
402
|
-
|
|
403
|
-
type: '
|
|
404
|
-
description: 'The
|
|
383
|
+
content: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'The content to insert'
|
|
405
386
|
},
|
|
406
|
-
|
|
407
|
-
type: '
|
|
408
|
-
|
|
387
|
+
position: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
enum: ['beginning', 'end', 'after_imports', 'before_first_describe'],
|
|
390
|
+
description: 'Where to insert: beginning (top of file), end (bottom), after_imports (after last import), before_first_describe (before first test block)'
|
|
409
391
|
},
|
|
410
|
-
|
|
392
|
+
after_marker: {
|
|
411
393
|
type: 'string',
|
|
412
|
-
description: '
|
|
394
|
+
description: 'Optional: Instead of position, insert after this specific text marker (uses fuzzy search)'
|
|
413
395
|
}
|
|
414
396
|
},
|
|
415
|
-
required: ['file_path', '
|
|
397
|
+
required: ['file_path', 'content']
|
|
416
398
|
}
|
|
417
|
-
}
|
|
399
|
+
},
|
|
418
400
|
];
|
|
419
401
|
exports.TOOLS = TOOLS;
|
|
420
402
|
// AST Parsing utilities
|
|
@@ -635,62 +617,157 @@ function getTypeAnnotation(typeNode) {
|
|
|
635
617
|
return 'unknown';
|
|
636
618
|
}
|
|
637
619
|
function getFunctionAST(filePath, functionName) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
path.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
path.
|
|
620
|
+
// Helper to search for function in a file
|
|
621
|
+
const searchInFile = (targetPath) => {
|
|
622
|
+
try {
|
|
623
|
+
const content = fsSync.readFileSync(targetPath, 'utf-8');
|
|
624
|
+
const ast = parseFileToAST(targetPath, content);
|
|
625
|
+
const lines = content.split('\n');
|
|
626
|
+
let functionInfo = null;
|
|
627
|
+
traverse(ast, {
|
|
628
|
+
FunctionDeclaration(path) {
|
|
629
|
+
if (path.node.id?.name === functionName) {
|
|
630
|
+
functionInfo = extractFunctionDetails(path, lines);
|
|
631
|
+
path.stop();
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
VariableDeclarator(path) {
|
|
635
|
+
if (path.node.id.name === functionName &&
|
|
636
|
+
(path.node.init?.type === 'ArrowFunctionExpression' ||
|
|
637
|
+
path.node.init?.type === 'FunctionExpression')) {
|
|
638
|
+
functionInfo = extractFunctionDetails(path, lines, true);
|
|
639
|
+
path.stop();
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
ClassMethod(path) {
|
|
643
|
+
if (path.node.key.name === functionName) {
|
|
644
|
+
functionInfo = extractFunctionDetails(path, lines);
|
|
645
|
+
path.stop();
|
|
646
|
+
}
|
|
662
647
|
}
|
|
648
|
+
});
|
|
649
|
+
// console.log(` ✅ Found functionInfo: ${JSON.stringify(functionInfo)}`);
|
|
650
|
+
// console.log(` ✅ Found targetPath: ${JSON.stringify(targetPath)}`);
|
|
651
|
+
if (functionInfo) {
|
|
652
|
+
return { success: true, ...functionInfo, filePath: targetPath };
|
|
663
653
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
// Try the provided file path first
|
|
661
|
+
try {
|
|
662
|
+
const result = searchInFile(filePath);
|
|
663
|
+
if (result) {
|
|
664
|
+
return result;
|
|
667
665
|
}
|
|
668
|
-
return Object.assign({ success: true }, functionInfo);
|
|
669
666
|
}
|
|
670
667
|
catch (error) {
|
|
671
|
-
|
|
668
|
+
// File not found or other error - continue to fallback
|
|
669
|
+
}
|
|
670
|
+
// Fallback: If we have an indexer, search for the function in the index
|
|
671
|
+
if (globalIndexer) {
|
|
672
|
+
console.log(` 🔍 Function not found in ${filePath}, searching index...`);
|
|
673
|
+
// Search through all indexed files
|
|
674
|
+
const indexData = globalIndexer.index;
|
|
675
|
+
if (indexData && indexData.files) {
|
|
676
|
+
for (const [indexedFilePath, fileData] of Object.entries(indexData.files)) {
|
|
677
|
+
const analysis = fileData.analysis;
|
|
678
|
+
// Check if this file contains the function
|
|
679
|
+
const hasFunction = analysis.functions?.some((fn) => fn.name === functionName);
|
|
680
|
+
const hasMethod = analysis.classes?.some((cls) => cls.methods?.some((method) => method.name === functionName));
|
|
681
|
+
if (hasFunction || hasMethod) {
|
|
682
|
+
console.log(` ✅ Found ${functionName} in ${indexedFilePath}`);
|
|
683
|
+
const result = searchInFile(indexedFilePath);
|
|
684
|
+
if (result) {
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
error: `Function ${functionName} not found in ${filePath} or any indexed files`
|
|
693
|
+
};
|
|
672
694
|
}
|
|
695
|
+
// No indexer available and not found in provided path
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
error: `Function ${functionName} not found in ${filePath}. Tip: Enable indexing for better function lookup across files.`
|
|
699
|
+
};
|
|
673
700
|
}
|
|
674
701
|
function extractFunctionDetails(path, lines, isVariable = false) {
|
|
675
702
|
const node = isVariable ? path.node.init : path.node;
|
|
676
703
|
const startLine = node.loc.start.line - 1;
|
|
677
704
|
const endLine = node.loc.end.line - 1;
|
|
678
|
-
|
|
679
|
-
|
|
705
|
+
// Look backwards for JSDoc/comments (up to 20 lines)
|
|
706
|
+
let commentStartLine = startLine;
|
|
707
|
+
let lookbackLimit = Math.max(0, startLine - 20);
|
|
708
|
+
while (commentStartLine > lookbackLimit) {
|
|
709
|
+
const prevLine = lines[commentStartLine - 1]?.trim() || '';
|
|
710
|
+
if (prevLine.startsWith('*') ||
|
|
711
|
+
prevLine.startsWith('//') ||
|
|
712
|
+
prevLine.startsWith('/**') ||
|
|
713
|
+
prevLine.endsWith('*/') ||
|
|
714
|
+
prevLine === '') {
|
|
715
|
+
commentStartLine--;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const code = lines.slice(commentStartLine, endLine + 1).join('\n');
|
|
722
|
+
// Extract called functions from the function body
|
|
723
|
+
const calledFunctions = [];
|
|
724
|
+
const calledMethods = [];
|
|
725
|
+
try {
|
|
726
|
+
// Use path.traverse() which has the proper scope/parentPath
|
|
727
|
+
path.traverse({
|
|
728
|
+
CallExpression(callPath) {
|
|
729
|
+
const callee = callPath.node.callee;
|
|
730
|
+
// Direct function calls: functionName()
|
|
731
|
+
if (callee.type === 'Identifier') {
|
|
732
|
+
const funcName = callee.name;
|
|
733
|
+
if (!calledFunctions.includes(funcName)) {
|
|
734
|
+
calledFunctions.push(funcName);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// Method calls: object.method()
|
|
738
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
739
|
+
const objectName = callee.object.type === 'Identifier' ? callee.object.name :
|
|
740
|
+
callee.object.type === 'ThisExpression' ? 'this' : 'unknown';
|
|
741
|
+
const methodCall = `${objectName}.${callee.property.name}`;
|
|
742
|
+
if (!calledMethods.includes(methodCall)) {
|
|
743
|
+
calledMethods.push(methodCall);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
// If traversal fails, just continue without called functions
|
|
751
|
+
console.warn('Could not extract called functions:', error);
|
|
752
|
+
}
|
|
753
|
+
// console.log(` ✅ Found code: ${code}`);
|
|
680
754
|
return {
|
|
681
755
|
name: isVariable ? path.node.id.name : node.id?.name,
|
|
682
756
|
code,
|
|
683
|
-
startLine:
|
|
757
|
+
startLine: commentStartLine + 1,
|
|
684
758
|
endLine: endLine + 1,
|
|
685
759
|
params: node.params.map((p) => extractParamInfo(p)),
|
|
686
760
|
returnType: node.returnType ? getTypeAnnotation(node.returnType) : null,
|
|
687
761
|
async: node.async,
|
|
762
|
+
calledFunctions: calledFunctions.length > 0 ? calledFunctions : undefined,
|
|
763
|
+
calledMethods: calledMethods.length > 0 ? calledMethods : undefined,
|
|
688
764
|
complexity: 1 // Simplified complexity
|
|
689
765
|
};
|
|
690
766
|
}
|
|
691
767
|
// Removed estimateComplexity to avoid scope/parentPath traversal issues
|
|
692
768
|
// Complexity is now hardcoded to 1 in extractFunctionDetails
|
|
693
769
|
function getImportsAST(filePath) {
|
|
770
|
+
// Try to read the file
|
|
694
771
|
try {
|
|
695
772
|
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
696
773
|
const ast = parseFileToAST(filePath, content);
|
|
@@ -738,9 +815,55 @@ function getImportsAST(filePath) {
|
|
|
738
815
|
}
|
|
739
816
|
}
|
|
740
817
|
});
|
|
741
|
-
return { success: true, imports };
|
|
818
|
+
return { success: true, imports, filePath };
|
|
742
819
|
}
|
|
743
820
|
catch (error) {
|
|
821
|
+
// File not found - try to find similar file in index
|
|
822
|
+
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
823
|
+
console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
824
|
+
const indexData = globalIndexer.index;
|
|
825
|
+
if (indexData && indexData.files) {
|
|
826
|
+
const searchName = path.basename(filePath);
|
|
827
|
+
const searchDir = path.dirname(filePath);
|
|
828
|
+
const matches = [];
|
|
829
|
+
// Find files with matching basename
|
|
830
|
+
for (const indexedFilePath of Object.keys(indexData.files)) {
|
|
831
|
+
if (path.basename(indexedFilePath) === searchName) {
|
|
832
|
+
matches.push(indexedFilePath);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (matches.length > 0) {
|
|
836
|
+
let suggestion = matches[0];
|
|
837
|
+
// If multiple matches, try to find the best one by matching directory structure
|
|
838
|
+
if (matches.length > 1) {
|
|
839
|
+
const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
|
|
840
|
+
// Score each match by how many path components match
|
|
841
|
+
const scored = matches.map(m => {
|
|
842
|
+
const matchParts = path.dirname(m).split(path.sep);
|
|
843
|
+
let score = 0;
|
|
844
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
845
|
+
const searchPart = searchParts[searchParts.length - 1 - i];
|
|
846
|
+
const matchPart = matchParts[matchParts.length - 1 - i];
|
|
847
|
+
if (searchPart === matchPart) {
|
|
848
|
+
score += (i + 1); // Weight recent parts higher
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return { path: m, score };
|
|
852
|
+
});
|
|
853
|
+
// Sort by score (highest first)
|
|
854
|
+
scored.sort((a, b) => b.score - a.score);
|
|
855
|
+
suggestion = scored[0].path;
|
|
856
|
+
}
|
|
857
|
+
console.log(` 💡 Did you mean: ${suggestion}?`);
|
|
858
|
+
return {
|
|
859
|
+
success: false,
|
|
860
|
+
error: `File not found: ${filePath}`,
|
|
861
|
+
suggestion: suggestion,
|
|
862
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
744
867
|
return { success: false, error: error.message };
|
|
745
868
|
}
|
|
746
869
|
}
|
|
@@ -802,93 +925,689 @@ function getTypeDefinitions(filePath) {
|
|
|
802
925
|
});
|
|
803
926
|
}
|
|
804
927
|
});
|
|
805
|
-
return { success: true, types };
|
|
928
|
+
return { success: true, types, filePath };
|
|
806
929
|
}
|
|
807
930
|
catch (error) {
|
|
931
|
+
// File not found - try to find similar file in index
|
|
932
|
+
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
933
|
+
console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
934
|
+
const indexData = globalIndexer.index;
|
|
935
|
+
if (indexData && indexData.files) {
|
|
936
|
+
const searchName = path.basename(filePath);
|
|
937
|
+
const searchDir = path.dirname(filePath);
|
|
938
|
+
const matches = [];
|
|
939
|
+
// Find files with matching basename
|
|
940
|
+
for (const indexedFilePath of Object.keys(indexData.files)) {
|
|
941
|
+
if (path.basename(indexedFilePath) === searchName) {
|
|
942
|
+
matches.push(indexedFilePath);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (matches.length > 0) {
|
|
946
|
+
let suggestion = matches[0];
|
|
947
|
+
// If multiple matches, try to find the best one by matching directory structure
|
|
948
|
+
if (matches.length > 1) {
|
|
949
|
+
const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
|
|
950
|
+
// Score each match by how many path components match
|
|
951
|
+
const scored = matches.map(m => {
|
|
952
|
+
const matchParts = path.dirname(m).split(path.sep);
|
|
953
|
+
let score = 0;
|
|
954
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
955
|
+
const searchPart = searchParts[searchParts.length - 1 - i];
|
|
956
|
+
const matchPart = matchParts[matchParts.length - 1 - i];
|
|
957
|
+
if (searchPart === matchPart) {
|
|
958
|
+
score += (i + 1); // Weight recent parts higher
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return { path: m, score };
|
|
962
|
+
});
|
|
963
|
+
// Sort by score (highest first)
|
|
964
|
+
scored.sort((a, b) => b.score - a.score);
|
|
965
|
+
suggestion = scored[0].path;
|
|
966
|
+
}
|
|
967
|
+
console.log(` 💡 Did you mean: ${suggestion}?`);
|
|
968
|
+
return {
|
|
969
|
+
success: false,
|
|
970
|
+
error: `File not found: ${filePath}`,
|
|
971
|
+
suggestion: suggestion,
|
|
972
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
808
977
|
return { success: false, error: error.message };
|
|
809
978
|
}
|
|
810
979
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
980
|
+
function getFilePreamble(filePath) {
|
|
981
|
+
// Helper to extract preamble from a file
|
|
982
|
+
const extractPreamble = (targetPath) => {
|
|
983
|
+
try {
|
|
984
|
+
const content = fsSync.readFileSync(targetPath, 'utf-8');
|
|
985
|
+
const ast = parseFileToAST(targetPath, content);
|
|
986
|
+
const lines = content.split('\n');
|
|
987
|
+
const imports = [];
|
|
988
|
+
const mocks = [];
|
|
989
|
+
const setupBlocks = [];
|
|
990
|
+
const topLevelVariables = [];
|
|
991
|
+
// Process only top-level statements in Program.body
|
|
992
|
+
if (ast.program && ast.program.body) {
|
|
993
|
+
for (const statement of ast.program.body) {
|
|
994
|
+
const startLine = statement.loc?.start.line || 0;
|
|
995
|
+
const endLine = statement.loc?.end.line || 0;
|
|
996
|
+
const code = lines.slice(startLine - 1, endLine).join('\n');
|
|
997
|
+
// Import declarations
|
|
998
|
+
if (statement.type === 'ImportDeclaration') {
|
|
999
|
+
imports.push({
|
|
1000
|
+
code,
|
|
1001
|
+
startLine,
|
|
1002
|
+
endLine,
|
|
1003
|
+
source: statement.source?.value || ''
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
// Top-level variable declarations
|
|
1007
|
+
else if (statement.type === 'VariableDeclaration') {
|
|
1008
|
+
statement.declarations.forEach((decl) => {
|
|
1009
|
+
topLevelVariables.push({
|
|
1010
|
+
code,
|
|
1011
|
+
startLine,
|
|
1012
|
+
endLine,
|
|
1013
|
+
name: decl.id?.name || 'unknown',
|
|
1014
|
+
kind: statement.kind
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
// Expression statements (could be jest.mock or setup blocks)
|
|
1019
|
+
else if (statement.type === 'ExpressionStatement' && statement.expression?.type === 'CallExpression') {
|
|
1020
|
+
const callExpr = statement.expression;
|
|
1021
|
+
const callee = callExpr.callee;
|
|
1022
|
+
// Check for jest.mock()
|
|
1023
|
+
if (callee.type === 'MemberExpression' &&
|
|
1024
|
+
callee.object?.name === 'jest' &&
|
|
1025
|
+
callee.property?.name === 'mock') {
|
|
1026
|
+
const moduleName = callExpr.arguments[0]?.value || 'unknown';
|
|
1027
|
+
let isVirtual = false;
|
|
1028
|
+
// Check for virtual flag in second argument
|
|
1029
|
+
if (callExpr.arguments[1]?.type === 'ObjectExpression') {
|
|
1030
|
+
const props = callExpr.arguments[1].properties || [];
|
|
1031
|
+
isVirtual = props.some((prop) => prop.key?.name === 'virtual' && prop.value?.value === true);
|
|
1032
|
+
}
|
|
1033
|
+
mocks.push({
|
|
1034
|
+
code,
|
|
1035
|
+
startLine,
|
|
1036
|
+
endLine,
|
|
1037
|
+
moduleName,
|
|
1038
|
+
isVirtual
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
// Check for setup blocks (beforeAll, beforeEach, etc.)
|
|
1042
|
+
else if (callee.type === 'Identifier' &&
|
|
1043
|
+
['beforeAll', 'beforeEach', 'afterAll', 'afterEach'].includes(callee.name)) {
|
|
1044
|
+
setupBlocks.push({
|
|
1045
|
+
code,
|
|
1046
|
+
startLine,
|
|
1047
|
+
endLine,
|
|
1048
|
+
type: callee.name
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// Handle require statements
|
|
1053
|
+
else if (statement.type === 'VariableDeclaration') {
|
|
1054
|
+
statement.declarations.forEach((decl) => {
|
|
1055
|
+
if (decl.init?.type === 'CallExpression' &&
|
|
1056
|
+
decl.init.callee?.name === 'require' &&
|
|
1057
|
+
decl.init.arguments[0]?.type === 'StringLiteral') {
|
|
1058
|
+
imports.push({
|
|
1059
|
+
code,
|
|
1060
|
+
startLine,
|
|
1061
|
+
endLine,
|
|
1062
|
+
source: decl.init.arguments[0].value,
|
|
1063
|
+
type: 'require'
|
|
835
1064
|
});
|
|
836
1065
|
}
|
|
837
1066
|
});
|
|
838
1067
|
}
|
|
839
|
-
classInfo = {
|
|
840
|
-
name: className,
|
|
841
|
-
methods,
|
|
842
|
-
superClass: path.node.superClass?.name
|
|
843
|
-
};
|
|
844
|
-
path.stop();
|
|
845
1068
|
}
|
|
846
1069
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1070
|
+
// Build full code with line markers
|
|
1071
|
+
const allItems = [
|
|
1072
|
+
...imports.map(i => ({ ...i, category: 'import' })),
|
|
1073
|
+
...mocks.map(m => ({ ...m, category: 'mock' })),
|
|
1074
|
+
...setupBlocks.map(s => ({ ...s, category: 'setup' })),
|
|
1075
|
+
...topLevelVariables.map(v => ({ ...v, category: 'variable' }))
|
|
1076
|
+
].sort((a, b) => a.startLine - b.startLine);
|
|
1077
|
+
const fullCode = allItems.map(item => `// Lines ${item.startLine}-${item.endLine} (${item.category})\n${item.code}`).join('\n\n');
|
|
1078
|
+
console.log(` ✅ Found file at ${targetPath}`);
|
|
1079
|
+
// console.log(` fullCode: ${fullCode}`);
|
|
1080
|
+
// console.log(` summary: ${JSON.stringify({
|
|
1081
|
+
// importCount: imports.length,
|
|
1082
|
+
// mockCount: mocks.length,
|
|
1083
|
+
// setupBlockCount: setupBlocks.length,
|
|
1084
|
+
// variableCount: topLevelVariables.length,
|
|
1085
|
+
// totalLines: allItems.length > 0 ? allItems[allItems.length - 1].endLine : 0
|
|
1086
|
+
// })}`);
|
|
1087
|
+
// console.log(` imports: ${JSON.stringify({imports,
|
|
1088
|
+
// mocks,
|
|
1089
|
+
// setupBlocks,
|
|
1090
|
+
// topLevelVariables})}`);
|
|
1091
|
+
return {
|
|
1092
|
+
success: true,
|
|
1093
|
+
filePath: targetPath,
|
|
1094
|
+
preamble: {
|
|
1095
|
+
imports,
|
|
1096
|
+
mocks,
|
|
1097
|
+
setupBlocks,
|
|
1098
|
+
topLevelVariables
|
|
1099
|
+
},
|
|
1100
|
+
fullCode,
|
|
1101
|
+
summary: {
|
|
1102
|
+
importCount: imports.length,
|
|
1103
|
+
mockCount: mocks.length,
|
|
1104
|
+
setupBlockCount: setupBlocks.length,
|
|
1105
|
+
variableCount: topLevelVariables.length,
|
|
1106
|
+
totalLines: allItems.length > 0 ? allItems[allItems.length - 1].endLine : 0
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
return null;
|
|
850
1112
|
}
|
|
851
|
-
|
|
1113
|
+
};
|
|
1114
|
+
// Try the provided file path first
|
|
1115
|
+
const result = extractPreamble(filePath);
|
|
1116
|
+
if (result) {
|
|
1117
|
+
return result;
|
|
852
1118
|
}
|
|
853
|
-
|
|
854
|
-
|
|
1119
|
+
// Fallback: If we have an indexer, search for file with matching path
|
|
1120
|
+
if (globalIndexer) {
|
|
1121
|
+
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1122
|
+
const indexData = globalIndexer.index;
|
|
1123
|
+
if (indexData && indexData.files) {
|
|
1124
|
+
const searchName = path.basename(filePath);
|
|
1125
|
+
const searchDir = path.dirname(filePath);
|
|
1126
|
+
const matches = [];
|
|
1127
|
+
// Find files with matching basename
|
|
1128
|
+
for (const indexedFilePath of Object.keys(indexData.files)) {
|
|
1129
|
+
if (path.basename(indexedFilePath) === searchName) {
|
|
1130
|
+
matches.push(indexedFilePath);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (matches.length > 0) {
|
|
1134
|
+
let correctPath = matches[0];
|
|
1135
|
+
// If multiple matches, try to find the best one by matching directory structure
|
|
1136
|
+
if (matches.length > 1) {
|
|
1137
|
+
const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
|
|
1138
|
+
// Score each match by how many path components match
|
|
1139
|
+
const scored = matches.map(m => {
|
|
1140
|
+
const matchParts = path.dirname(m).split(path.sep);
|
|
1141
|
+
let score = 0;
|
|
1142
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
1143
|
+
const searchPart = searchParts[searchParts.length - 1 - i];
|
|
1144
|
+
const matchPart = matchParts[matchParts.length - 1 - i];
|
|
1145
|
+
if (searchPart === matchPart) {
|
|
1146
|
+
score += (i + 1); // Weight recent parts higher
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return { path: m, score };
|
|
1150
|
+
});
|
|
1151
|
+
// Sort by score (highest first)
|
|
1152
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1153
|
+
correctPath = scored[0].path;
|
|
1154
|
+
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
console.log(` ✅ Found file at ${correctPath}`);
|
|
1158
|
+
}
|
|
1159
|
+
const retryResult = extractPreamble(correctPath);
|
|
1160
|
+
if (retryResult) {
|
|
1161
|
+
return retryResult;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
success: false,
|
|
1166
|
+
error: `File not found: ${filePath}`,
|
|
1167
|
+
suggestion: matches.length > 0 ? matches[0] : undefined,
|
|
1168
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
855
1171
|
}
|
|
1172
|
+
// No indexer available
|
|
1173
|
+
return {
|
|
1174
|
+
success: false,
|
|
1175
|
+
error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
|
|
1176
|
+
};
|
|
856
1177
|
}
|
|
857
|
-
|
|
858
|
-
|
|
1178
|
+
function getClassMethods(filePath, className) {
|
|
1179
|
+
// Helper to search for class in a file
|
|
1180
|
+
const searchInFile = (targetPath) => {
|
|
1181
|
+
try {
|
|
1182
|
+
const content = fsSync.readFileSync(targetPath, 'utf-8');
|
|
1183
|
+
const ast = parseFileToAST(targetPath, content);
|
|
1184
|
+
const lines = content.split('\n');
|
|
1185
|
+
let classInfo = null;
|
|
1186
|
+
traverse(ast, {
|
|
1187
|
+
ClassDeclaration(path) {
|
|
1188
|
+
if (path.node.id?.name === className) {
|
|
1189
|
+
const methods = [];
|
|
1190
|
+
if (path.node.body && path.node.body.body) {
|
|
1191
|
+
path.node.body.body.forEach(member => {
|
|
1192
|
+
if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
|
|
1193
|
+
const startLine = member.loc.start.line - 1;
|
|
1194
|
+
const endLine = member.loc.end.line - 1;
|
|
1195
|
+
methods.push({
|
|
1196
|
+
name: member.key.name,
|
|
1197
|
+
kind: member.kind,
|
|
1198
|
+
static: member.static,
|
|
1199
|
+
async: member.async,
|
|
1200
|
+
params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
|
|
1201
|
+
returnType: member.returnType ? getTypeAnnotation(member.returnType) : null,
|
|
1202
|
+
code: lines.slice(startLine, endLine + 1).join('\n'),
|
|
1203
|
+
line: startLine + 1
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
classInfo = {
|
|
1209
|
+
name: className,
|
|
1210
|
+
methods,
|
|
1211
|
+
superClass: path.node.superClass?.name,
|
|
1212
|
+
filePath: targetPath
|
|
1213
|
+
};
|
|
1214
|
+
path.stop();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
if (classInfo) {
|
|
1219
|
+
return { success: true, ...classInfo };
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
// Try the provided file path first
|
|
859
1228
|
try {
|
|
860
|
-
const
|
|
861
|
-
|
|
1229
|
+
const result = searchInFile(filePath);
|
|
1230
|
+
if (result) {
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
862
1233
|
}
|
|
863
1234
|
catch (error) {
|
|
864
|
-
|
|
1235
|
+
// File not found or other error - continue to fallback
|
|
865
1236
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1237
|
+
// Fallback: If we have an indexer, search for the class in the index
|
|
1238
|
+
if (globalIndexer) {
|
|
1239
|
+
console.log(` 🔍 Class not found in ${filePath}, searching index...`);
|
|
1240
|
+
// Search through all indexed files
|
|
1241
|
+
const indexData = globalIndexer.index;
|
|
1242
|
+
if (indexData && indexData.files) {
|
|
1243
|
+
for (const [indexedFilePath, fileData] of Object.entries(indexData.files)) {
|
|
1244
|
+
const analysis = fileData.analysis;
|
|
1245
|
+
// Check if this file contains the class
|
|
1246
|
+
const hasClass = analysis.classes?.some((cls) => cls.name === className);
|
|
1247
|
+
if (hasClass) {
|
|
1248
|
+
console.log(` ✅ Found ${className} in ${indexedFilePath}`);
|
|
1249
|
+
const result = searchInFile(indexedFilePath);
|
|
1250
|
+
if (result) {
|
|
1251
|
+
return result;
|
|
1252
|
+
}
|
|
877
1253
|
}
|
|
878
1254
|
}
|
|
879
|
-
if (fsSync.existsSync(resolved)) {
|
|
880
|
-
return { success: true, resolvedPath: resolved };
|
|
881
|
-
}
|
|
882
1255
|
}
|
|
883
1256
|
return {
|
|
884
|
-
success:
|
|
885
|
-
|
|
886
|
-
|
|
1257
|
+
success: false,
|
|
1258
|
+
error: `Class ${className} not found in ${filePath} or any indexed files`
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
// No indexer available and not found in provided path
|
|
1262
|
+
return {
|
|
1263
|
+
success: false,
|
|
1264
|
+
error: `Class ${className} not found in ${filePath}. Tip: Enable indexing for better class lookup across files.`
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
// Other tool implementations
|
|
1268
|
+
async function readFile(filePath) {
|
|
1269
|
+
const MAX_LINES = 2000;
|
|
1270
|
+
// Helper to read and validate file
|
|
1271
|
+
const tryReadFile = async (targetPath) => {
|
|
1272
|
+
try {
|
|
1273
|
+
const content = await fs.readFile(targetPath, 'utf-8');
|
|
1274
|
+
const lines = content.split('\n');
|
|
1275
|
+
if (lines.length > MAX_LINES) {
|
|
1276
|
+
// Check if this is a test file
|
|
1277
|
+
const isTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(targetPath);
|
|
1278
|
+
// File is too large - check if we have cached analysis
|
|
1279
|
+
if (globalIndexer) {
|
|
1280
|
+
const cached = globalIndexer.getFileAnalysis(targetPath);
|
|
1281
|
+
if (cached) {
|
|
1282
|
+
console.log(` 📦 File too large (${lines.length} lines), returning cached analysis instead`);
|
|
1283
|
+
// For test files, also include preamble automatically
|
|
1284
|
+
let preamble = undefined;
|
|
1285
|
+
if (isTestFile) {
|
|
1286
|
+
const preambleResult = getFilePreamble(targetPath);
|
|
1287
|
+
if (preambleResult.success) {
|
|
1288
|
+
preamble = preambleResult;
|
|
1289
|
+
console.log(` 📋 Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
success: true,
|
|
1294
|
+
usedCache: true,
|
|
1295
|
+
fileSize: lines.length,
|
|
1296
|
+
filePath: targetPath,
|
|
1297
|
+
analysis: cached,
|
|
1298
|
+
preamble: preamble,
|
|
1299
|
+
message: isTestFile
|
|
1300
|
+
? `Test file has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/mocks/setup). Use get_function_ast with proper file path for specific test functions.`
|
|
1301
|
+
: `File has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/setup). Use read_file_lines to read specific section of file or get_function_ast with proper file path for specific functions.`
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
// No cache available
|
|
1306
|
+
return {
|
|
1307
|
+
success: false,
|
|
1308
|
+
error: `File too large (${lines.length} lines, max ${MAX_LINES}). Use get_file_preamble for imports/mocks, analyze_file_ast for structure, or get_function_ast for specific functions or read_file_lines to read specific section of file.`,
|
|
1309
|
+
fileSize: lines.length,
|
|
1310
|
+
filePath: targetPath,
|
|
1311
|
+
suggestion: 'For large files: 1) get_file_preamble for imports/mocks/setup, 2) analyze_file_ast for all functions, 3) get_function_ast for specific functions'
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
return { success: true, content, filePath: targetPath, fileSize: lines.length };
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
return null; // Signal to try fallback
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
// Try the provided file path first
|
|
1321
|
+
const result = await tryReadFile(filePath);
|
|
1322
|
+
if (result) {
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
// Fallback: If we have an indexer, search for file with matching path
|
|
1326
|
+
if (globalIndexer) {
|
|
1327
|
+
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1328
|
+
const indexData = globalIndexer.index;
|
|
1329
|
+
if (indexData && indexData.files) {
|
|
1330
|
+
const searchName = path.basename(filePath);
|
|
1331
|
+
const searchDir = path.dirname(filePath);
|
|
1332
|
+
const matches = [];
|
|
1333
|
+
// Find files with matching basename
|
|
1334
|
+
for (const indexedFilePath of Object.keys(indexData.files)) {
|
|
1335
|
+
if (path.basename(indexedFilePath) === searchName) {
|
|
1336
|
+
matches.push(indexedFilePath);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (matches.length > 0) {
|
|
1340
|
+
let correctPath = matches[0];
|
|
1341
|
+
// If multiple matches, try to find the best one by matching directory structure
|
|
1342
|
+
if (matches.length > 1) {
|
|
1343
|
+
const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
|
|
1344
|
+
// Score each match by how many path components match
|
|
1345
|
+
const scored = matches.map(m => {
|
|
1346
|
+
const matchParts = path.dirname(m).split(path.sep);
|
|
1347
|
+
let score = 0;
|
|
1348
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
1349
|
+
const searchPart = searchParts[searchParts.length - 1 - i];
|
|
1350
|
+
const matchPart = matchParts[matchParts.length - 1 - i];
|
|
1351
|
+
if (searchPart === matchPart) {
|
|
1352
|
+
score += (i + 1); // Weight recent parts higher
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return { path: m, score };
|
|
1356
|
+
});
|
|
1357
|
+
// Sort by score (highest first)
|
|
1358
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1359
|
+
correctPath = scored[0].path;
|
|
1360
|
+
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
console.log(` ✅ Found file at ${correctPath}`);
|
|
1364
|
+
}
|
|
1365
|
+
// Try to read from correct path
|
|
1366
|
+
const retryResult = await tryReadFile(correctPath);
|
|
1367
|
+
if (retryResult) {
|
|
1368
|
+
return retryResult;
|
|
1369
|
+
}
|
|
1370
|
+
// If still failed, at least return the correct path
|
|
1371
|
+
return {
|
|
1372
|
+
success: false,
|
|
1373
|
+
error: `Found file at ${correctPath} but failed to read it`,
|
|
1374
|
+
suggestion: correctPath,
|
|
1375
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return {
|
|
1380
|
+
success: false,
|
|
1381
|
+
error: `File not found: ${filePath}. No matching files found in index.`
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
// No indexer available
|
|
1385
|
+
return {
|
|
1386
|
+
success: false,
|
|
1387
|
+
error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Read specific lines from a file
|
|
1392
|
+
* Useful for examining syntax errors, bracket mismatches, or specific sections
|
|
1393
|
+
*/
|
|
1394
|
+
async function readFileLines(filePath, startLine, endLine) {
|
|
1395
|
+
// Validate line numbers
|
|
1396
|
+
if (startLine < 1) {
|
|
1397
|
+
return {
|
|
1398
|
+
success: false,
|
|
1399
|
+
error: 'Start line must be >= 1'
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (endLine < startLine) {
|
|
1403
|
+
return {
|
|
1404
|
+
success: false,
|
|
1405
|
+
error: 'End line must be >= start line'
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
if (endLine - startLine > 2000) {
|
|
1409
|
+
return {
|
|
1410
|
+
succes: false,
|
|
1411
|
+
error: 'You are note allowed to read more than 2000 lines at once. End line - start line must be less than 2000'
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
// Helper to read lines from a file
|
|
1415
|
+
const tryReadFileLines = async (targetPath) => {
|
|
1416
|
+
try {
|
|
1417
|
+
const content = await fs.readFile(targetPath, 'utf-8');
|
|
1418
|
+
const allLines = content.split('\n');
|
|
1419
|
+
// Check if requested range is valid
|
|
1420
|
+
if (startLine > allLines.length) {
|
|
1421
|
+
return {
|
|
1422
|
+
success: false,
|
|
1423
|
+
error: `Start line ${startLine} exceeds file length (${allLines.length} lines)`,
|
|
1424
|
+
fileLength: allLines.length,
|
|
1425
|
+
filePath: targetPath
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
// Adjust end line if it exceeds file length
|
|
1429
|
+
const actualEndLine = Math.min(endLine, allLines.length);
|
|
1430
|
+
// Extract requested lines (convert to 0-indexed)
|
|
1431
|
+
const requestedLines = allLines.slice(startLine - 1, actualEndLine);
|
|
1432
|
+
// Format lines with line numbers for easy reference
|
|
1433
|
+
const formattedLines = requestedLines.map((line, index) => {
|
|
1434
|
+
const lineNumber = startLine + index;
|
|
1435
|
+
return `${lineNumber.toString().padStart(6, ' ')}|${line}`;
|
|
1436
|
+
}).join('\n');
|
|
1437
|
+
return {
|
|
1438
|
+
success: true,
|
|
1439
|
+
filePath: targetPath,
|
|
1440
|
+
startLine,
|
|
1441
|
+
endLine: actualEndLine,
|
|
1442
|
+
lineCount: requestedLines.length,
|
|
1443
|
+
content: formattedLines,
|
|
1444
|
+
rawLines: requestedLines
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
return null; // Signal to try fallback
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
// Try the provided file path first
|
|
1452
|
+
const result = await tryReadFileLines(filePath);
|
|
1453
|
+
if (result) {
|
|
1454
|
+
return result;
|
|
1455
|
+
}
|
|
1456
|
+
// Fallback: If we have an indexer, search for file with matching path
|
|
1457
|
+
if (globalIndexer) {
|
|
1458
|
+
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1459
|
+
const indexData = globalIndexer.index;
|
|
1460
|
+
if (indexData && indexData.files) {
|
|
1461
|
+
const searchName = path.basename(filePath);
|
|
1462
|
+
const searchDir = path.dirname(filePath);
|
|
1463
|
+
const matches = [];
|
|
1464
|
+
// Find files with matching basename
|
|
1465
|
+
for (const indexedFilePath of Object.keys(indexData.files)) {
|
|
1466
|
+
if (path.basename(indexedFilePath) === searchName) {
|
|
1467
|
+
matches.push(indexedFilePath);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (matches.length > 0) {
|
|
1471
|
+
let correctPath = matches[0];
|
|
1472
|
+
// If multiple matches, try to find the best one by matching directory structure
|
|
1473
|
+
if (matches.length > 1) {
|
|
1474
|
+
const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
|
|
1475
|
+
// Score each match by how many path components match
|
|
1476
|
+
const scored = matches.map(m => {
|
|
1477
|
+
const matchParts = path.dirname(m).split(path.sep);
|
|
1478
|
+
let score = 0;
|
|
1479
|
+
for (let i = 0; i < searchParts.length; i++) {
|
|
1480
|
+
const searchPart = searchParts[searchParts.length - 1 - i];
|
|
1481
|
+
const matchPart = matchParts[matchParts.length - 1 - i];
|
|
1482
|
+
if (searchPart === matchPart) {
|
|
1483
|
+
score += (i + 1); // Weight recent parts higher
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return { path: m, score };
|
|
1487
|
+
});
|
|
1488
|
+
// Sort by score (highest first)
|
|
1489
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1490
|
+
correctPath = scored[0].path;
|
|
1491
|
+
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
console.log(` ✅ Found file at ${correctPath}`);
|
|
1495
|
+
}
|
|
1496
|
+
// Try to read from correct path
|
|
1497
|
+
const retryResult = await tryReadFileLines(correctPath);
|
|
1498
|
+
if (retryResult) {
|
|
1499
|
+
return retryResult;
|
|
1500
|
+
}
|
|
1501
|
+
// If still failed, return error with suggestion
|
|
1502
|
+
return {
|
|
1503
|
+
success: false,
|
|
1504
|
+
error: `Found file at ${correctPath} but failed to read it`,
|
|
1505
|
+
suggestion: correctPath,
|
|
1506
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
success: false,
|
|
1512
|
+
error: `File not found: ${filePath}. No matching files found in index.`
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
// No indexer available
|
|
1516
|
+
return {
|
|
1517
|
+
success: false,
|
|
1518
|
+
error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function resolveImportPath(fromFile, importPath) {
|
|
1522
|
+
try {
|
|
1523
|
+
if (importPath.startsWith('.')) {
|
|
1524
|
+
const dir = path.dirname(fromFile);
|
|
1525
|
+
const resolved = path.resolve(dir, importPath);
|
|
1526
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
|
|
1527
|
+
for (const ext of extensions) {
|
|
1528
|
+
const withExt = resolved + ext;
|
|
1529
|
+
if (fsSync.existsSync(withExt)) {
|
|
1530
|
+
// console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
|
|
1531
|
+
// success: true,
|
|
1532
|
+
// resolvedPath: withExt
|
|
1533
|
+
// })})`)
|
|
1534
|
+
return { success: true, resolvedPath: withExt };
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (fsSync.existsSync(resolved)) {
|
|
1538
|
+
// console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
|
|
1539
|
+
// success: true,
|
|
1540
|
+
// resolvedPath: resolved
|
|
1541
|
+
// })})`)
|
|
1542
|
+
return { success: true, resolvedPath: resolved };
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
|
|
1546
|
+
// success: true,
|
|
1547
|
+
// resolvedPath: importPath,
|
|
1548
|
+
// isExternal: true
|
|
1549
|
+
// })})`)
|
|
1550
|
+
return {
|
|
1551
|
+
success: true,
|
|
1552
|
+
resolvedPath: importPath,
|
|
1553
|
+
isExternal: true
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
return { success: false, error: error.message };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Validate test file syntax using AST parsing
|
|
1562
|
+
* Provides detailed error information for AI to retry
|
|
1563
|
+
*/
|
|
1564
|
+
async function validateTestFileSyntax(filePath) {
|
|
1565
|
+
try {
|
|
1566
|
+
// Read the file content
|
|
1567
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
1568
|
+
// Try to parse with Babel - this will throw if syntax is invalid
|
|
1569
|
+
babelParser.parse(content, {
|
|
1570
|
+
sourceType: 'module',
|
|
1571
|
+
plugins: [
|
|
1572
|
+
'typescript',
|
|
1573
|
+
'jsx',
|
|
1574
|
+
'decorators-legacy'
|
|
1575
|
+
]
|
|
1576
|
+
});
|
|
1577
|
+
// If parsing succeeded, syntax is valid
|
|
1578
|
+
return { valid: true };
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
// Extract error details
|
|
1582
|
+
const line = error.loc?.line || error.lineNumber || 0;
|
|
1583
|
+
const column = error.loc?.column || error.column || 0;
|
|
1584
|
+
let errorMessage = error.message || 'Unknown syntax error';
|
|
1585
|
+
// Clean up error message (remove file path prefix if present)
|
|
1586
|
+
errorMessage = errorMessage.replace(/^.*?: /, '');
|
|
1587
|
+
// Generate helpful suggestion based on error type
|
|
1588
|
+
let suggestion = '';
|
|
1589
|
+
if (errorMessage.includes('Unexpected token') || errorMessage.includes('unexpected')) {
|
|
1590
|
+
suggestion = 'Check for missing/extra brackets, parentheses, or commas near this location.';
|
|
1591
|
+
}
|
|
1592
|
+
else if (errorMessage.includes('Unterminated')) {
|
|
1593
|
+
suggestion = 'You have an unclosed string, comment, or template literal.';
|
|
1594
|
+
}
|
|
1595
|
+
else if (errorMessage.includes('Expected')) {
|
|
1596
|
+
suggestion = 'Missing required syntax element. Check if you closed all blocks properly.';
|
|
1597
|
+
}
|
|
1598
|
+
else if (errorMessage.includes('duplicate') || errorMessage.includes('already')) {
|
|
1599
|
+
suggestion = 'Duplicate declaration detected. Check for repeated variable/function names.';
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
suggestion = 'Review the code structure around this line for syntax issues.';
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
valid: false,
|
|
1606
|
+
error: errorMessage,
|
|
1607
|
+
location: { line, column },
|
|
1608
|
+
suggestion
|
|
887
1609
|
};
|
|
888
1610
|
}
|
|
889
|
-
catch (error) {
|
|
890
|
-
return { success: false, error: error.message };
|
|
891
|
-
}
|
|
892
1611
|
}
|
|
893
1612
|
async function writeTestFile(filePath, content, sourceFilePath, functionMode = false) {
|
|
894
1613
|
try {
|
|
@@ -936,18 +1655,12 @@ async function writeTestFile(filePath, content, sourceFilePath, functionMode = f
|
|
|
936
1655
|
}
|
|
937
1656
|
}
|
|
938
1657
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
if (testCount < Math.max(4, expectedFunctionCount * 2)) {
|
|
946
|
-
return {
|
|
947
|
-
success: false,
|
|
948
|
-
error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
|
|
949
|
-
};
|
|
950
|
-
}
|
|
1658
|
+
// if (testCount < Math.max(4, expectedFunctionCount * 2)) {
|
|
1659
|
+
// return {
|
|
1660
|
+
// success: false,
|
|
1661
|
+
// error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
|
|
1662
|
+
// };
|
|
1663
|
+
// }
|
|
951
1664
|
}
|
|
952
1665
|
const dir = path.dirname(filePath);
|
|
953
1666
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -1006,7 +1719,7 @@ async function editTestFile(filePath, oldContent, newContent) {
|
|
|
1006
1719
|
const preview = content.substring(0, 500);
|
|
1007
1720
|
return {
|
|
1008
1721
|
success: false,
|
|
1009
|
-
error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use
|
|
1722
|
+
error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use search_replace_block with more context to find the correct location.`,
|
|
1010
1723
|
currentContent: preview
|
|
1011
1724
|
};
|
|
1012
1725
|
}
|
|
@@ -1090,6 +1803,19 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1090
1803
|
updatedLines.push(...afterBlock);
|
|
1091
1804
|
const updatedContent = updatedLines.join('\n');
|
|
1092
1805
|
await fs.writeFile(testFilePath, updatedContent, 'utf-8');
|
|
1806
|
+
// Validate syntax after modification
|
|
1807
|
+
const validation = await validateTestFileSyntax(testFilePath);
|
|
1808
|
+
if (!validation.valid) {
|
|
1809
|
+
// Rollback the change by restoring original content
|
|
1810
|
+
await fs.writeFile(testFilePath, content, 'utf-8');
|
|
1811
|
+
return {
|
|
1812
|
+
success: false,
|
|
1813
|
+
error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
|
|
1814
|
+
syntaxError: true,
|
|
1815
|
+
location: validation.location,
|
|
1816
|
+
suggestion: validation.suggestion
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1093
1819
|
return {
|
|
1094
1820
|
success: true,
|
|
1095
1821
|
message: `Replaced existing tests for function '${functionName}' (lines ${describeStartLine + 1}-${describeEndLine + 1})`,
|
|
@@ -1133,10 +1859,23 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1133
1859
|
// Validate bracket pairing
|
|
1134
1860
|
const openCount = (updatedContent.match(/{/g) || []).length;
|
|
1135
1861
|
const closeCount = (updatedContent.match(/}/g) || []).length;
|
|
1136
|
-
if (openCount !== closeCount) {
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1862
|
+
// if (openCount !== closeCount) {
|
|
1863
|
+
// console.warn(`⚠️ Warning: Bracket mismatch detected (${openCount} opening, ${closeCount} closing). The test file structure may be malformed.`);
|
|
1864
|
+
// }
|
|
1139
1865
|
await fs.writeFile(testFilePath, updatedContent, 'utf-8');
|
|
1866
|
+
// Validate syntax after modification
|
|
1867
|
+
const validation = await validateTestFileSyntax(testFilePath);
|
|
1868
|
+
if (!validation.valid) {
|
|
1869
|
+
// Rollback the change by restoring original content
|
|
1870
|
+
await fs.writeFile(testFilePath, content, 'utf-8');
|
|
1871
|
+
return {
|
|
1872
|
+
success: false,
|
|
1873
|
+
error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
|
|
1874
|
+
syntaxError: true,
|
|
1875
|
+
location: validation.location,
|
|
1876
|
+
suggestion: validation.suggestion
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1140
1879
|
return {
|
|
1141
1880
|
success: true,
|
|
1142
1881
|
message: `Added tests for function '${functionName}' at root level (after line ${lastRootLevelClosing + 1})`,
|
|
@@ -1150,19 +1889,33 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1150
1889
|
return { success: false, error: error.message };
|
|
1151
1890
|
}
|
|
1152
1891
|
}
|
|
1153
|
-
function runTests(testFilePath) {
|
|
1892
|
+
function runTests(testFilePath, functionNames) {
|
|
1154
1893
|
try {
|
|
1155
|
-
|
|
1894
|
+
// Build Jest command with optional function name filter
|
|
1895
|
+
let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
|
|
1896
|
+
// If function names provided, use Jest -t flag to run only those tests
|
|
1897
|
+
if (functionNames && functionNames.length > 0) {
|
|
1898
|
+
// Create regex pattern to match any of the function names
|
|
1899
|
+
// Escape special regex characters in function names
|
|
1900
|
+
const escapedNames = functionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
1901
|
+
const pattern = escapedNames.join('|');
|
|
1902
|
+
command += ` -t "${pattern}"`;
|
|
1903
|
+
}
|
|
1904
|
+
const output = (0, child_process_1.execSync)(command, {
|
|
1156
1905
|
encoding: 'utf-8',
|
|
1157
1906
|
stdio: 'pipe'
|
|
1158
1907
|
});
|
|
1908
|
+
console.log(` Test run output: ${output}`);
|
|
1159
1909
|
return {
|
|
1160
1910
|
success: true,
|
|
1161
1911
|
output,
|
|
1162
|
-
passed: true
|
|
1912
|
+
passed: true,
|
|
1913
|
+
command // Return command for debugging
|
|
1163
1914
|
};
|
|
1164
1915
|
}
|
|
1165
1916
|
catch (error) {
|
|
1917
|
+
console.log(` Test run error: ${error.message}`);
|
|
1918
|
+
console.log(`output sent to ai: ${error.stdout + error.stderr}`);
|
|
1166
1919
|
return {
|
|
1167
1920
|
success: false,
|
|
1168
1921
|
output: error.stdout + error.stderr,
|
|
@@ -1263,6 +2016,13 @@ function calculateRelativePath(fromFile, toFile) {
|
|
|
1263
2016
|
}
|
|
1264
2017
|
// Convert backslashes to forward slashes (Windows compatibility)
|
|
1265
2018
|
relativePath = relativePath.replace(/\\/g, '/');
|
|
2019
|
+
// console.log(`calculate_relative_path: JSON.stringify(${JSON.stringify({
|
|
2020
|
+
// success: true,
|
|
2021
|
+
// from: fromFile,
|
|
2022
|
+
// to: toFile,
|
|
2023
|
+
// relativePath,
|
|
2024
|
+
// importStatement: `import { ... } from '${relativePath}';`
|
|
2025
|
+
// })})`)
|
|
1266
2026
|
return {
|
|
1267
2027
|
success: true,
|
|
1268
2028
|
from: fromFile,
|
|
@@ -1379,43 +2139,276 @@ async function replaceLines(filePath, startLine, endLine, newContent) {
|
|
|
1379
2139
|
return { success: false, error: error.message };
|
|
1380
2140
|
}
|
|
1381
2141
|
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Search and replace a code block using fuzzy matching
|
|
2144
|
+
* Much more reliable than line-based editing!
|
|
2145
|
+
*/
|
|
2146
|
+
async function searchReplaceBlock(filePath, search, replace, matchMode = 'normalized') {
|
|
2147
|
+
try {
|
|
2148
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
2149
|
+
// Try exact match first if in normalized/fuzzy mode (faster)
|
|
2150
|
+
if (matchMode !== 'exact' && content.includes(search)) {
|
|
2151
|
+
const newContent = content.replace(search, replace);
|
|
2152
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
2153
|
+
// Validate syntax if this is a test file
|
|
2154
|
+
if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
|
|
2155
|
+
const validation = await validateTestFileSyntax(filePath);
|
|
2156
|
+
if (!validation.valid) {
|
|
2157
|
+
// Rollback the change by restoring original content
|
|
2158
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
2159
|
+
return {
|
|
2160
|
+
success: false,
|
|
2161
|
+
error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
|
|
2162
|
+
syntaxError: true,
|
|
2163
|
+
location: validation.location,
|
|
2164
|
+
suggestion: validation.suggestion
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
success: true,
|
|
2170
|
+
message: 'Replaced block (exact match)',
|
|
2171
|
+
matchMode: 'exact',
|
|
2172
|
+
confidence: 1.0,
|
|
2173
|
+
linesChanged: newContent.split('\n').length - content.split('\n').length
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
// Bailout for very large files/searches to prevent hanging
|
|
2177
|
+
const contentSize = content.length;
|
|
2178
|
+
const searchSize = search.length;
|
|
2179
|
+
const isLargeOperation = contentSize > 200000 || searchSize > 2000;
|
|
2180
|
+
// Use smart search (force normalized for large files to skip expensive fuzzy)
|
|
2181
|
+
const searchMode = isLargeOperation ? 'normalized' : matchMode;
|
|
2182
|
+
const result = (0, fuzzyMatcher_1.smartSearch)(content, search, searchMode);
|
|
2183
|
+
if (!result || !result.found) {
|
|
2184
|
+
// Skip expensive similarity search for large files
|
|
2185
|
+
const suggestions = isLargeOperation ? [] : (0, fuzzyMatcher_1.findSimilarBlocks)(content, search, 3);
|
|
2186
|
+
const errorMessage = isLargeOperation
|
|
2187
|
+
? 'Search block not found. File is large - try including more unique context or use exact match mode.'
|
|
2188
|
+
: (0, fuzzyMatcher_1.getSearchFailureMessage)(content, search);
|
|
2189
|
+
return {
|
|
2190
|
+
success: false,
|
|
2191
|
+
error: errorMessage,
|
|
2192
|
+
searchPreview: search.substring(0, 200) + (search.length > 200 ? '...' : ''),
|
|
2193
|
+
suggestions: suggestions.map(s => ({
|
|
2194
|
+
text: s.text.substring(0, 150) + (s.text.length > 150 ? '...' : ''),
|
|
2195
|
+
similarity: Math.round(s.similarity * 100) + '%',
|
|
2196
|
+
lines: `${s.startLine}-${s.endLine}`
|
|
2197
|
+
})),
|
|
2198
|
+
hint: suggestions.length > 0
|
|
2199
|
+
? `Found ${suggestions.length} similar block(s). Check if your search text has typos or try including more context lines.`
|
|
2200
|
+
: 'No similar blocks found. Make sure your search text matches the file content and includes enough context (3-5 lines around the target).'
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
// Perform replacement
|
|
2204
|
+
const newContent = content.substring(0, result.startIndex) +
|
|
2205
|
+
replace +
|
|
2206
|
+
content.substring(result.endIndex);
|
|
2207
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
2208
|
+
// Validate syntax if this is a test file
|
|
2209
|
+
if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
|
|
2210
|
+
const validation = await validateTestFileSyntax(filePath);
|
|
2211
|
+
if (!validation.valid) {
|
|
2212
|
+
// Rollback the change by restoring original content
|
|
2213
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
2214
|
+
return {
|
|
2215
|
+
success: false,
|
|
2216
|
+
error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
|
|
2217
|
+
syntaxError: true,
|
|
2218
|
+
location: validation.location,
|
|
2219
|
+
suggestion: validation.suggestion
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
// Get lines for context
|
|
2224
|
+
const getLineNumber = (charIndex) => {
|
|
2225
|
+
return content.substring(0, charIndex).split('\n').length;
|
|
2226
|
+
};
|
|
2227
|
+
const startLine = getLineNumber(result.startIndex);
|
|
2228
|
+
const endLine = getLineNumber(result.endIndex);
|
|
2229
|
+
const result1 = { success: true,
|
|
2230
|
+
message: `Replaced block successfully using ${result.matchType} matching`,
|
|
2231
|
+
matchMode: result.matchType,
|
|
2232
|
+
confidence: result.confidence,
|
|
2233
|
+
linesChanged: newContent.split('\n').length - content.split('\n').length,
|
|
2234
|
+
linesReplaced: `${startLine}-${endLine}`,
|
|
2235
|
+
replacedText: result.originalText.substring(0, 200) + (result.originalText.length > 200 ? '...' : '') };
|
|
2236
|
+
// console.log(`SearchReplaceBlock: JSON.stringify(${JSON.stringify(result1)})`)
|
|
2237
|
+
return {
|
|
2238
|
+
success: true,
|
|
2239
|
+
message: `Replaced block successfully using ${result.matchType} matching`,
|
|
2240
|
+
matchMode: result.matchType,
|
|
2241
|
+
confidence: result.confidence,
|
|
2242
|
+
linesChanged: newContent.split('\n').length - content.split('\n').length,
|
|
2243
|
+
linesReplaced: `${startLine}-${endLine}`,
|
|
2244
|
+
replacedText: result.originalText.substring(0, 200) + (result.originalText.length > 200 ? '...' : '')
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
catch (error) {
|
|
2248
|
+
return { success: false, error: error.message };
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Insert content at a specific position in the file
|
|
2253
|
+
* Simpler than search_replace_block for adding new content
|
|
2254
|
+
*/
|
|
2255
|
+
async function insertAtPosition(filePath, content, position, afterMarker) {
|
|
2256
|
+
try {
|
|
2257
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
2258
|
+
const lines = fileContent.split('\n');
|
|
2259
|
+
let insertIndex = 0;
|
|
2260
|
+
let insertionPoint = '';
|
|
2261
|
+
if (afterMarker) {
|
|
2262
|
+
// Use smart search to find marker
|
|
2263
|
+
const result = (0, fuzzyMatcher_1.smartSearch)(fileContent, afterMarker, 'normalized');
|
|
2264
|
+
if (!result || !result.found) {
|
|
2265
|
+
return {
|
|
2266
|
+
success: false,
|
|
2267
|
+
error: `Marker not found: "${afterMarker.substring(0, 100)}..."`,
|
|
2268
|
+
hint: 'Use search_replace_block or specify a position instead'
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
// Find line number of marker end
|
|
2272
|
+
let charCount = 0;
|
|
2273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2274
|
+
charCount += lines[i].length + 1; // +1 for newline
|
|
2275
|
+
if (charCount >= result.endIndex) {
|
|
2276
|
+
insertIndex = i + 1;
|
|
2277
|
+
insertionPoint = `after marker at line ${i + 1}`;
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
else {
|
|
2283
|
+
// Use position
|
|
2284
|
+
switch (position) {
|
|
2285
|
+
case 'beginning':
|
|
2286
|
+
insertIndex = 0;
|
|
2287
|
+
insertionPoint = 'beginning of file';
|
|
2288
|
+
break;
|
|
2289
|
+
case 'end':
|
|
2290
|
+
insertIndex = lines.length;
|
|
2291
|
+
insertionPoint = 'end of file';
|
|
2292
|
+
break;
|
|
2293
|
+
case 'after_imports': {
|
|
2294
|
+
// Find last import statement
|
|
2295
|
+
let lastImportLine = -1;
|
|
2296
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2297
|
+
const trimmed = lines[i].trim();
|
|
2298
|
+
if (trimmed.startsWith('import ') || trimmed.startsWith('require(') ||
|
|
2299
|
+
trimmed.startsWith('const ') && trimmed.includes('require(')) {
|
|
2300
|
+
lastImportLine = i;
|
|
2301
|
+
}
|
|
2302
|
+
else if (trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && lastImportLine >= 0) {
|
|
2303
|
+
// Stop at first non-import, non-comment line after imports
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (lastImportLine === -1) {
|
|
2308
|
+
return {
|
|
2309
|
+
success: false,
|
|
2310
|
+
error: 'No import statements found in file',
|
|
2311
|
+
hint: 'Use position: "beginning" instead or specify after_marker'
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
insertIndex = lastImportLine + 1;
|
|
2315
|
+
insertionPoint = `after imports at line ${lastImportLine + 1}`;
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
case 'before_first_describe': {
|
|
2319
|
+
// Find first describe block
|
|
2320
|
+
let firstDescribe = -1;
|
|
2321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2322
|
+
if (lines[i].trim().startsWith('describe(')) {
|
|
2323
|
+
firstDescribe = i;
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (firstDescribe === -1) {
|
|
2328
|
+
return {
|
|
2329
|
+
success: false,
|
|
2330
|
+
error: 'No describe blocks found in file',
|
|
2331
|
+
hint: 'Use position: "end" instead or specify after_marker'
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
insertIndex = firstDescribe;
|
|
2335
|
+
insertionPoint = `before first describe at line ${firstDescribe + 1}`;
|
|
2336
|
+
break;
|
|
2337
|
+
}
|
|
2338
|
+
default:
|
|
2339
|
+
// Default to end
|
|
2340
|
+
insertIndex = lines.length;
|
|
2341
|
+
insertionPoint = 'end of file';
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
// Insert content
|
|
2345
|
+
const contentLines = content.split('\n');
|
|
2346
|
+
lines.splice(insertIndex, 0, ...contentLines);
|
|
2347
|
+
const newContent = lines.join('\n');
|
|
2348
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
2349
|
+
// Validate syntax if this is a test file
|
|
2350
|
+
if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
|
|
2351
|
+
const validation = await validateTestFileSyntax(filePath);
|
|
2352
|
+
if (!validation.valid) {
|
|
2353
|
+
// Rollback the change by restoring original content
|
|
2354
|
+
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
2355
|
+
return {
|
|
2356
|
+
success: false,
|
|
2357
|
+
error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
|
|
2358
|
+
syntaxError: true,
|
|
2359
|
+
location: validation.location,
|
|
2360
|
+
suggestion: validation.suggestion
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return {
|
|
2365
|
+
success: true,
|
|
2366
|
+
message: `Inserted ${contentLines.length} line(s) at ${insertionPoint}`,
|
|
2367
|
+
insertedAt: insertionPoint,
|
|
2368
|
+
lineCount: contentLines.length
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
catch (error) {
|
|
2372
|
+
return { success: false, error: error.message };
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
1382
2375
|
// User-friendly messages for each tool
|
|
1383
2376
|
const TOOL_MESSAGES = {
|
|
1384
2377
|
'read_file': '📖 Reading source file',
|
|
2378
|
+
'read_file_lines': '📖 Reading file section',
|
|
1385
2379
|
'analyze_file_ast': '🔍 Analyzing codebase structure',
|
|
1386
2380
|
'get_function_ast': '🔎 Examining function details',
|
|
1387
2381
|
'get_imports_ast': '📦 Analyzing dependencies',
|
|
1388
2382
|
'get_type_definitions': '📋 Extracting type definitions',
|
|
2383
|
+
'get_file_preamble': '📋 Extracting file preamble (imports, mocks, setup)',
|
|
1389
2384
|
'get_class_methods': '🏗️ Analyzing class structure',
|
|
1390
2385
|
'resolve_import_path': '🔗 Resolving import paths',
|
|
1391
|
-
'
|
|
1392
|
-
'edit_test_file': '✏️ Updating test file',
|
|
1393
|
-
'replace_function_tests': '🔄 Replacing test cases for specific functions',
|
|
2386
|
+
'upsert_function_tests': '✍️ Writing/updating test cases for function',
|
|
1394
2387
|
'run_tests': '🧪 Running tests',
|
|
1395
2388
|
'list_directory': '📂 Exploring directory structure',
|
|
1396
2389
|
'find_file': '🔍 Locating file in repository',
|
|
1397
2390
|
'calculate_relative_path': '🧭 Calculating import path',
|
|
1398
2391
|
'report_legitimate_failure': '⚠️ Reporting legitimate test failures',
|
|
1399
|
-
'
|
|
1400
|
-
'
|
|
1401
|
-
'replace_lines': '🔄 Replacing lines in file'
|
|
2392
|
+
'search_replace_block': '🔍🔄 Searching and replacing code block',
|
|
2393
|
+
'insert_at_position': '➕ Inserting content at position',
|
|
1402
2394
|
};
|
|
1403
2395
|
// Tool execution router
|
|
1404
2396
|
async function executeTool(toolName, args) {
|
|
1405
2397
|
// Show user-friendly message with dynamic context
|
|
1406
2398
|
let friendlyMessage = TOOL_MESSAGES[toolName] || `🔧 ${toolName}`;
|
|
1407
2399
|
// Add specific details for certain tools
|
|
1408
|
-
if (toolName === '
|
|
1409
|
-
friendlyMessage =
|
|
2400
|
+
if (toolName === 'upsert_function_tests' && args.function_name) {
|
|
2401
|
+
friendlyMessage = `✍️ Writing/updating test cases for function: ${args.function_name}`;
|
|
1410
2402
|
}
|
|
1411
|
-
else if (toolName === '
|
|
1412
|
-
|
|
2403
|
+
else if (toolName === 'search_replace_block') {
|
|
2404
|
+
const preview = args.search ? args.search.substring(0, 50) + (args.search.length > 50 ? '...' : '') : '';
|
|
2405
|
+
friendlyMessage = `🔍🔄 Searching for: "${preview}"`;
|
|
1413
2406
|
}
|
|
1414
|
-
else if (toolName === '
|
|
1415
|
-
friendlyMessage = `➕ Inserting
|
|
2407
|
+
else if (toolName === 'insert_at_position' && args.position) {
|
|
2408
|
+
friendlyMessage = `➕ Inserting at: ${args.position}`;
|
|
1416
2409
|
}
|
|
1417
|
-
else if (toolName === '
|
|
1418
|
-
friendlyMessage =
|
|
2410
|
+
else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
|
|
2411
|
+
friendlyMessage = `📖 Reading lines ${args.start_line}-${args.end_line}`;
|
|
1419
2412
|
}
|
|
1420
2413
|
console.log(`\n${friendlyMessage}...`);
|
|
1421
2414
|
let result;
|
|
@@ -1424,6 +2417,9 @@ async function executeTool(toolName, args) {
|
|
|
1424
2417
|
case 'read_file':
|
|
1425
2418
|
result = await readFile(args.file_path);
|
|
1426
2419
|
break;
|
|
2420
|
+
case 'read_file_lines':
|
|
2421
|
+
result = await readFileLines(args.file_path, args.start_line, args.end_line);
|
|
2422
|
+
break;
|
|
1427
2423
|
case 'analyze_file_ast':
|
|
1428
2424
|
// Try cache first if indexer is available
|
|
1429
2425
|
if (globalIndexer) {
|
|
@@ -1468,20 +2464,17 @@ async function executeTool(toolName, args) {
|
|
|
1468
2464
|
case 'get_class_methods':
|
|
1469
2465
|
result = getClassMethods(args.file_path, args.class_name);
|
|
1470
2466
|
break;
|
|
2467
|
+
case 'get_file_preamble':
|
|
2468
|
+
result = getFilePreamble(args.file_path);
|
|
2469
|
+
break;
|
|
1471
2470
|
case 'resolve_import_path':
|
|
1472
2471
|
result = resolveImportPath(args.from_file, args.import_path);
|
|
1473
2472
|
break;
|
|
1474
|
-
case '
|
|
1475
|
-
result = await writeTestFile(args.file_path, args.content, args.source_file, args.function_mode);
|
|
1476
|
-
break;
|
|
1477
|
-
case 'edit_test_file':
|
|
1478
|
-
result = await editTestFile(args.file_path, args.old_content, args.new_content);
|
|
1479
|
-
break;
|
|
1480
|
-
case 'replace_function_tests':
|
|
2473
|
+
case 'upsert_function_tests':
|
|
1481
2474
|
result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
|
|
1482
2475
|
break;
|
|
1483
2476
|
case 'run_tests':
|
|
1484
|
-
result = runTests(args.test_file_path);
|
|
2477
|
+
result = runTests(args.test_file_path, args.function_names);
|
|
1485
2478
|
break;
|
|
1486
2479
|
case 'list_directory':
|
|
1487
2480
|
result = listDirectory(args.directory_path);
|
|
@@ -1495,14 +2488,11 @@ async function executeTool(toolName, args) {
|
|
|
1495
2488
|
case 'report_legitimate_failure':
|
|
1496
2489
|
result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
|
|
1497
2490
|
break;
|
|
1498
|
-
case '
|
|
1499
|
-
result = await
|
|
1500
|
-
break;
|
|
1501
|
-
case 'insert_lines':
|
|
1502
|
-
result = await insertLines(args.file_path, args.line_number, args.content);
|
|
2491
|
+
case 'search_replace_block':
|
|
2492
|
+
result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
|
|
1503
2493
|
break;
|
|
1504
|
-
case '
|
|
1505
|
-
result = await
|
|
2494
|
+
case 'insert_at_position':
|
|
2495
|
+
result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
|
|
1506
2496
|
break;
|
|
1507
2497
|
default:
|
|
1508
2498
|
result = { success: false, error: `Unknown tool: ${toolName}` };
|
|
@@ -1513,6 +2503,7 @@ async function executeTool(toolName, args) {
|
|
|
1513
2503
|
}
|
|
1514
2504
|
// Show result with friendly message
|
|
1515
2505
|
if (result.success) {
|
|
2506
|
+
// console.log('result', JSON.stringify(result, null, 2));
|
|
1516
2507
|
console.log(` ✅ Done`);
|
|
1517
2508
|
}
|
|
1518
2509
|
else if (result.error) {
|
|
@@ -1759,104 +2750,283 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
|
|
|
1759
2750
|
// Main conversation loop
|
|
1760
2751
|
async function generateTests(sourceFile) {
|
|
1761
2752
|
console.log(`\n📝 Generating tests for: ${sourceFile}\n`);
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2753
|
+
// Check file size and switch to function-by-function generation if large
|
|
2754
|
+
try {
|
|
2755
|
+
const fileContent = fsSync.readFileSync(sourceFile, 'utf-8');
|
|
2756
|
+
const lineCount = fileContent.split('\n').length;
|
|
2757
|
+
console.log(`📊 Source file has ${lineCount} lines`);
|
|
2758
|
+
if (lineCount > 200) {
|
|
2759
|
+
console.log(`\n⚡ File has more than 200 lines! Switching to function-by-function generation...\n`);
|
|
2760
|
+
// Analyze file to get all functions
|
|
2761
|
+
const result = analyzeFileAST(sourceFile);
|
|
2762
|
+
if (result && result.success && result.analysis && result.analysis.functions && result.analysis.functions.length > 0) {
|
|
2763
|
+
// Filter to only EXPORTED functions (public API)
|
|
2764
|
+
const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
|
|
2765
|
+
const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
|
|
2766
|
+
if (functionNames.length === 0) {
|
|
2767
|
+
console.log('⚠️ No exported functions found in file. Falling back to regular generation.');
|
|
2768
|
+
// Fall through to regular generation
|
|
2769
|
+
}
|
|
2770
|
+
else {
|
|
2771
|
+
const totalFunctions = result.analysis.functions.length;
|
|
2772
|
+
const internalFunctions = totalFunctions - exportedFunctions.length;
|
|
2773
|
+
console.log(`✅ Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
|
|
2774
|
+
if (internalFunctions > 0) {
|
|
2775
|
+
console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
|
|
2776
|
+
}
|
|
2777
|
+
// Use function-by-function generation
|
|
2778
|
+
try {
|
|
2779
|
+
return await generateTestsForFunctions(sourceFile, functionNames);
|
|
2780
|
+
}
|
|
2781
|
+
catch (funcError) {
|
|
2782
|
+
// CRITICAL: Check if test file already exists with content
|
|
2783
|
+
const testFilePath = getTestFilePath(sourceFile);
|
|
2784
|
+
if (fsSync.existsSync(testFilePath)) {
|
|
2785
|
+
const existingContent = fsSync.readFileSync(testFilePath, 'utf-8');
|
|
2786
|
+
const hasExistingTests = existingContent.includes('describe(') || existingContent.includes('test(');
|
|
2787
|
+
if (hasExistingTests) {
|
|
2788
|
+
console.error('\n❌ CRITICAL: Function-by-function generation failed, but test file already has tests!');
|
|
2789
|
+
console.error(' Cannot fall back to file-wise generation as it would OVERWRITE existing tests.');
|
|
2790
|
+
console.error(` Error: ${funcError.message}`);
|
|
2791
|
+
console.error('\n Options:');
|
|
2792
|
+
console.error(' 1. Fix the issue manually in the test file');
|
|
2793
|
+
console.error(' 2. Delete the test file and regenerate from scratch');
|
|
2794
|
+
console.error(' 3. Run function-wise generation again for remaining functions');
|
|
2795
|
+
throw new Error(`Function-by-function generation failed with existing tests. Manual intervention required. Original error: ${funcError.message}`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
// No existing tests, safe to fall back
|
|
2799
|
+
console.log('⚠️ Function-by-function generation failed, but no existing tests found. Falling back to file-wise generation.');
|
|
2800
|
+
throw funcError; // Re-throw to be caught by outer try-catch
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
else {
|
|
2805
|
+
console.log('⚠️ No functions found in file. Falling back to regular generation.');
|
|
2806
|
+
if (result && !result.success) {
|
|
2807
|
+
console.log(` Analysis error: ${result.error}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
catch (error) {
|
|
2813
|
+
// Check if this is the "existing tests" protection error
|
|
2814
|
+
if (error.message && error.message.includes('Manual intervention required')) {
|
|
2815
|
+
// This is a critical error - don't proceed with generation
|
|
2816
|
+
console.error(`\n❌ Aborting: ${error.message}`);
|
|
2817
|
+
throw error; // Re-throw to stop execution
|
|
2818
|
+
}
|
|
2819
|
+
console.log(`⚠️ Could not check file size: ${error}. Proceeding with regular generation.`);
|
|
2820
|
+
// Falls through to regular file-wise generation below
|
|
2821
|
+
}
|
|
2822
|
+
const testFilePath = getTestFilePath(sourceFile);
|
|
2823
|
+
const messages = [
|
|
2824
|
+
{
|
|
2825
|
+
role: 'user',
|
|
2826
|
+
content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests including edge cases for a TypeScript file.
|
|
2827
|
+
|
|
2828
|
+
Source file: ${sourceFile}
|
|
2829
|
+
Test file path: ${testFilePath}
|
|
2830
|
+
|
|
2831
|
+
IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
|
|
2832
|
+
|
|
2833
|
+
Your task (you MUST complete ALL steps):
|
|
2834
|
+
1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
|
|
2835
|
+
- This provides metadata about all code structures without loading full file content
|
|
2836
|
+
- CRITICAL: You have only 50 iterations to complete this task, so make sure you are using the tools efficiently.
|
|
2837
|
+
- Do not over explore, use the tools to get the information you need and start generating tests.
|
|
2838
|
+
2. Use get_imports_ast tool to understand all dependencies
|
|
2839
|
+
3. For each dependency, use find_file(filePath) to locate the file and calculate_relative_path to get correct import paths for the test file
|
|
2840
|
+
4. For complex functions, use get_function_ast tool to get detailed information
|
|
2841
|
+
- Returns complete function code WITH JSDoc comments
|
|
2842
|
+
- Includes calledFunctions and calledMethods lists showing what the function calls
|
|
2843
|
+
- Use this to fetch related helper functions if needed
|
|
2844
|
+
- [CRITICAL]: If a function calls other functions from other files, use find_file + get_function_ast tools to locate them and check if they need to mocked, since they can be making api calls to external services.
|
|
2845
|
+
5. Use get_function_ast to get detailed information about the functions.
|
|
2846
|
+
6. For large test files (>5K lines), use get_file_preamble to see existing imports/mocks/setup blocks
|
|
2847
|
+
- Automatically included when reading large test files
|
|
2848
|
+
- Use before adding new test cases to avoid duplicate mocks/imports
|
|
2849
|
+
- Particularly useful when updating existing test files with upsert_function_tests
|
|
2850
|
+
- Captures complete multi-line mocks including complex jest.mock() statements
|
|
2851
|
+
7. For classes, use get_class_methods tool to extract all methods
|
|
2852
|
+
8. Use get_type_definitions tool to understand TypeScript types and interfaces
|
|
2853
|
+
9. Generate comprehensive Jest unit tests with:
|
|
2854
|
+
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors. Ensure tests fully mock the config module with all expected properties.
|
|
2855
|
+
- If required:
|
|
2856
|
+
- Mock database modules like '../database' or '../database/index' with virtual:true.
|
|
2857
|
+
- Mock models, and any modules that access config or the database with virtual:true.
|
|
2858
|
+
- Mock config properly with virtual:true.
|
|
2859
|
+
- Mock isEmpty from lodash to return the expected values with virtual:true.
|
|
2860
|
+
- Axios should be mocked with virtual:true.
|
|
2861
|
+
- Use jest.mock() calls at the TOP of the file before any imports
|
|
2862
|
+
- [CRITICAL]: Virtual modules should only be used for db/config/models/services/index/axios/routes files. You should not use virtual:true for any other files or helpers that exist in the source code. The actual helpers should never be mocked with virtual:true.
|
|
2863
|
+
9. REQUIRED: Write tests using upsert_function_tests tool for EACH function with REAL test code (NOT placeholders!)
|
|
2864
|
+
- Call upsert_function_tests once for EACH exported function
|
|
2865
|
+
- Ensure comprehensive mocks are included in the first function's test to set up the file
|
|
1797
2866
|
- DO NOT use ANY placeholder comments like:
|
|
1798
2867
|
* "// Mock setup", "// Assertions", "// Call function"
|
|
1799
2868
|
* "// Further tests...", "// Additional tests..."
|
|
1800
2869
|
* "// Similarly, write tests for..."
|
|
1801
2870
|
* "// Add more tests...", "// TODO", "// ..."
|
|
1802
2871
|
- Write ACTUAL working test code with real mocks, real assertions, real function calls
|
|
1803
|
-
- Every test MUST have:
|
|
2872
|
+
- Every test MUST have [MANDATORY]:
|
|
1804
2873
|
* Real setup code (mock functions, create test data)
|
|
1805
2874
|
* Real execution (call the function being tested)
|
|
1806
2875
|
* Real expect() assertions (at least one per test)
|
|
2876
|
+
* null/undefined handling tests for all API responses
|
|
2877
|
+
* Happy path scenarios
|
|
2878
|
+
* Edge cases (null, undefined, empty arrays, etc.)
|
|
2879
|
+
* Error conditions
|
|
2880
|
+
* Async behavior (if applicable)
|
|
2881
|
+
- Proper TypeScript types
|
|
1807
2882
|
- Write tests for EVERY exported function (minimum 3-5 tests per function)
|
|
1808
2883
|
- If source has 4 functions, test file MUST have 4 describe blocks with actual tests
|
|
1809
2884
|
- Example of COMPLETE test structure:
|
|
1810
2885
|
* Setup: Create mocks and test data
|
|
1811
2886
|
* Execute: Call the function being tested
|
|
1812
2887
|
* Assert: Use expect() to verify results
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
- Use find_file tool to locate the
|
|
2888
|
+
10. REQUIRED: Run the tests using run_tests tool
|
|
2889
|
+
11. REQUIRED: If tests fail with import errors:
|
|
2890
|
+
- Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
|
|
1816
2891
|
- Use calculate_relative_path tool to get correct import path
|
|
1817
|
-
- PRIMARY METHOD
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2892
|
+
- ✅ PRIMARY METHOD: Use search_replace_block to fix imports:
|
|
2893
|
+
* Include 3-5 lines of context around the import to change
|
|
2894
|
+
* Example: search_replace_block({
|
|
2895
|
+
search: "import { oldImport } from './old-path';\nimport { other } from './other';",
|
|
2896
|
+
replace: "import { oldImport, newImport } from './correct-path';\nimport { other } from './other';"
|
|
2897
|
+
})
|
|
2898
|
+
- 📌 ALTERNATIVE: Use insert_at_position for adding new imports:
|
|
2899
|
+
* insert_at_position({ position: 'after_imports', content: "import { newImport } from './path';" })
|
|
2900
|
+
- ⚠️ AVOID: Line-based tools (deprecated, fragile)
|
|
2901
|
+
12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
|
|
1824
2902
|
|
|
1825
2903
|
FIXABLE ERRORS (you should fix these):
|
|
1826
2904
|
- Wrong import paths
|
|
1827
2905
|
- Missing mocks
|
|
1828
2906
|
- Incorrect mock implementations
|
|
1829
2907
|
- Wrong assertions or test logic
|
|
1830
|
-
- TypeScript compilation errors
|
|
2908
|
+
- TypeScript compilation errors (syntax errors, bracket mismatches)
|
|
1831
2909
|
- Missing test setup/teardown
|
|
1832
2910
|
- Cannot read properties of undefined
|
|
2911
|
+
- Test case failed to run. Use read_file_lines tool to read the specific problematic section and fix the issue.
|
|
2912
|
+
|
|
2913
|
+
💡 TIP: For syntax errors or bracket mismatches:
|
|
2914
|
+
- Use read_file to see the file content (it includes line numbers)
|
|
2915
|
+
- Use search_replace_block to fix the problematic section
|
|
2916
|
+
- Include 3-5 lines of context around the error to make search unique
|
|
2917
|
+
- Example: search_replace_block({
|
|
2918
|
+
search: "line before error\nproblematic code with syntax error\nline after",
|
|
2919
|
+
replace: "line before error\ncorrected code\nline after"
|
|
2920
|
+
})
|
|
1833
2921
|
|
|
1834
2922
|
LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
|
|
1835
2923
|
- Function returns wrong type (e.g., undefined instead of object)
|
|
1836
2924
|
- Missing null/undefined checks in source code
|
|
1837
2925
|
- Logic errors in source code
|
|
1838
2926
|
- Unhandled promise rejections in source code
|
|
1839
|
-
- Source code throws unexpected errors
|
|
1840
2927
|
|
|
1841
|
-
|
|
1842
|
-
- ✅ PRIMARY METHOD: Use
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2928
|
+
13. If errors are FIXABLE (AFTER test file is written):
|
|
2929
|
+
- ✅ PRIMARY METHOD: Use search_replace_block (RECOMMENDED):
|
|
2930
|
+
* Find the problematic code section
|
|
2931
|
+
* Include 3-5 lines of context before/after to make search unique
|
|
2932
|
+
* Replace with corrected version
|
|
2933
|
+
* Example: search_replace_block({
|
|
2934
|
+
file_path: "test.ts",
|
|
2935
|
+
search: "const mock = jest.fn();\ntest('old test', () => {\n mock();",
|
|
2936
|
+
replace: "const mock = jest.fn().mockResolvedValue({ data: 'test' });\ntest('fixed test', () => {\n mock();"
|
|
2937
|
+
})
|
|
2938
|
+
* Handles whitespace/indentation differences automatically!
|
|
2939
|
+
- 📌 ALTERNATIVE: Use insert_at_position for adding mocks/imports at top:
|
|
2940
|
+
* insert_at_position({ position: 'after_imports', content: "jest.mock('../database');" })
|
|
2941
|
+
- ⚠️ AVOID: Line-based tools (deprecated) - they are fragile and prone to errors
|
|
1851
2942
|
- Then retry running tests
|
|
1852
|
-
|
|
2943
|
+
14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
|
|
1853
2944
|
- Provide failing test names, reason, and source code issue description
|
|
1854
2945
|
- The test file will be kept as-is with legitimate failing tests
|
|
1855
|
-
|
|
1856
|
-
15. REQUIRED:
|
|
2946
|
+
- You are not allowed to call this tool for error - Test suite failed to run. You must ensure that test cases get executed. Fix any syntax or linting issues in the test file.
|
|
2947
|
+
15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
|
|
2948
|
+
16. REQUIRED: Ensure all functions are tested in the test file.
|
|
2949
|
+
17. CRITICAL: config and database modules must be mocked
|
|
2950
|
+
|
|
2951
|
+
18. Some known issues when running tests with fixes:
|
|
2952
|
+
Route loading issue: Importing the controller triggered route setup because axios-helper imports from index.ts, which loads all routes. Routes referenced functions that weren't available during test initialization.
|
|
2953
|
+
- Solution: Mocked index.ts to export only whitelistDomainsForHeaders without executing route setup code.
|
|
2954
|
+
Axios mock missing required properties: The axios mock didn't include properties needed by axios-retry (like interceptors).
|
|
2955
|
+
- Solution: Created a createMockAxiosInstance function that returns a mock axios instance with interceptors, defaults, and HTTP methods.
|
|
2956
|
+
axios-retry not mocked: axios-retry was trying to modify axios instances during module initialization.
|
|
2957
|
+
- Solution: Added a mock for axios-retry to prevent it from executing during tests.
|
|
2958
|
+
Routes file execution: The routes file was being executed when the controller was imported.
|
|
2959
|
+
- Solution: Mocked the routes file to return a simple Express router without executing route definitions.
|
|
1857
2960
|
|
|
1858
2961
|
CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
|
|
1859
2962
|
|
|
2963
|
+
19. [ALWAYS FOLLOW] Write Jest test cases following these MANDATORY patterns to prevent mock pollution:
|
|
2964
|
+
|
|
2965
|
+
SECTION 1: Top-Level Mock Setup
|
|
2966
|
+
Declare ALL jest.mock() calls at the top of the file, outside any describe blocks
|
|
2967
|
+
Mock EVERY function and module the controller/function uses
|
|
2968
|
+
Load the controller/module under test ONCE in beforeAll()
|
|
2969
|
+
[MANDATORY] Always use calculate_relative_path tool to get the correct import path for the module to be used in jest.mock() calls.
|
|
2970
|
+
|
|
2971
|
+
SECTION 2: Module References in Describe Blocks (CRITICAL)
|
|
2972
|
+
Declare ALL module references as let variables at the top of each describe block
|
|
2973
|
+
NEVER use const for module requires inside describe blocks
|
|
2974
|
+
Re-assign these modules inside beforeEach AFTER jest.clearAllMocks()
|
|
2975
|
+
This ensures each test gets fresh module references with clean mocks
|
|
2976
|
+
|
|
2977
|
+
SECTION 3: BeforeEach Pattern (MANDATORY)
|
|
2978
|
+
Pattern Structure:
|
|
2979
|
+
|
|
2980
|
+
Declare let variables for all modules at top of describe block
|
|
2981
|
+
Load controller once in beforeAll
|
|
2982
|
+
In beforeEach: clear mocks first, then re-assign all modules, then reset all mock implementations
|
|
2983
|
+
In afterEach: restore all mocks for extra safety
|
|
2984
|
+
In individual tests: only override specific mocks needed for that test
|
|
2985
|
+
|
|
2986
|
+
Example Pattern:
|
|
2987
|
+
|
|
2988
|
+
Declare: let controller, let helperModule, let responseHelper, let otherDependency
|
|
2989
|
+
beforeAll: controller = require path to controller
|
|
2990
|
+
beforeEach step 1: jest.clearAllMocks()
|
|
2991
|
+
beforeEach step 2: Re-assign all modules with require statements
|
|
2992
|
+
beforeEach step 3: Set default mock implementations for all mocked functions
|
|
2993
|
+
afterEach: jest.restoreAllMocks()
|
|
2994
|
+
In tests: Override only what changes for that specific test case
|
|
2995
|
+
|
|
2996
|
+
SECTION 4: What to NEVER Do
|
|
2997
|
+
|
|
2998
|
+
Never use const for module requires inside describe blocks
|
|
2999
|
+
Never rely on top-level module requires for mocking within tests
|
|
3000
|
+
Never share module references across multiple describe blocks
|
|
3001
|
+
Never skip re-assigning modules in beforeEach
|
|
3002
|
+
Never mutate module exports directly, always use mockImplementation instead
|
|
3003
|
+
Never forget to clear mocks before getting fresh references
|
|
3004
|
+
|
|
3005
|
+
SECTION 5: Key Principles
|
|
3006
|
+
|
|
3007
|
+
Isolation: Each test has its own mock instances via fresh requires
|
|
3008
|
+
Cleanup: Always clear and restore mocks between tests
|
|
3009
|
+
Explicit: Make all dependencies explicit in beforeEach
|
|
3010
|
+
Fresh References: Re-require modules after clearing to get clean mocks
|
|
3011
|
+
Default Behaviors: Set up sensible defaults in beforeEach, override in individual tests
|
|
3012
|
+
|
|
3013
|
+
SECTION 6: Verification Steps
|
|
3014
|
+
|
|
3015
|
+
Run tests multiple times with runInBand flag
|
|
3016
|
+
Run tests in random order with randomize flag
|
|
3017
|
+
Tests should pass consistently regardless of execution order
|
|
3018
|
+
Each test should be completely independent
|
|
3019
|
+
|
|
3020
|
+
SECTION 7: Critical Reminders
|
|
3021
|
+
|
|
3022
|
+
Always add missing mock initializations in relevant beforeEach blocks
|
|
3023
|
+
Ensure all mocked functions have default behaviors set in beforeEach
|
|
3024
|
+
Re-require modules after jest.clearAllMocks() to get fresh mock references
|
|
3025
|
+
Use let not const for all module references inside describe blocks
|
|
3026
|
+
Load the actual controller or module under test only once in beforeAll
|
|
3027
|
+
|
|
3028
|
+
Apply these patterns to ALL test files to ensure zero mock pollution between test suites and individual test cases.
|
|
3029
|
+
|
|
1860
3030
|
START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
1861
3031
|
}
|
|
1862
3032
|
];
|
|
@@ -1895,7 +3065,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
1895
3065
|
// Don't add the excuse to conversation, override with command
|
|
1896
3066
|
messages.push({
|
|
1897
3067
|
role: 'user',
|
|
1898
|
-
content: 'STOP making excuses! You CAN use the tools.
|
|
3068
|
+
content: 'STOP making excuses! You CAN use the tools. Use search_replace_block or insert_at_position NOW to fix the test file. Add proper mocks to prevent database initialization errors.'
|
|
1899
3069
|
});
|
|
1900
3070
|
continue;
|
|
1901
3071
|
}
|
|
@@ -1918,7 +3088,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
1918
3088
|
if (!testFileWritten) {
|
|
1919
3089
|
messages.push({
|
|
1920
3090
|
role: 'user',
|
|
1921
|
-
content: 'You have not written the test file yet. Use
|
|
3091
|
+
content: 'You have not written the test file yet. Use upsert_function_tests tool NOW with complete test code (not placeholders) for each function.'
|
|
1922
3092
|
});
|
|
1923
3093
|
}
|
|
1924
3094
|
else {
|
|
@@ -1928,11 +3098,13 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
1928
3098
|
|
|
1929
3099
|
If tests are failing:
|
|
1930
3100
|
- FIXABLE errors (imports, mocks, assertions):
|
|
1931
|
-
✅ PRIMARY: Use
|
|
1932
|
-
|
|
3101
|
+
✅ PRIMARY: Use search_replace_block with context (handles whitespace automatically!)
|
|
3102
|
+
📌 ALTERNATIVE: Use insert_at_position for adding imports/mocks
|
|
3103
|
+
⚠️ AVOID: Line-based tools (deprecated, fragile)
|
|
3104
|
+
run_tests tool to run the tests and check if the tests pass.
|
|
1933
3105
|
- LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
|
|
1934
3106
|
|
|
1935
|
-
|
|
3107
|
+
Example: search_replace_block({ search: "old code with context...", replace: "new fixed code..." })`
|
|
1936
3108
|
});
|
|
1937
3109
|
}
|
|
1938
3110
|
continue;
|
|
@@ -1955,81 +3127,41 @@ Start with read_file to see line numbers, then fix specific lines!`
|
|
|
1955
3127
|
console.log(` Recommendation: ${result.recommendation}`);
|
|
1956
3128
|
}
|
|
1957
3129
|
// Track if test file was written
|
|
1958
|
-
if (toolCall.name === '
|
|
3130
|
+
if (toolCall.name === 'upsert_function_tests') {
|
|
1959
3131
|
if (result.success) {
|
|
1960
3132
|
testFileWritten = true;
|
|
1961
|
-
console.log(`\n📝 Test file written: ${
|
|
1962
|
-
if (result.stats) {
|
|
1963
|
-
console.log(` Tests: ${result.stats.tests}, Expectations: ${result.stats.expectations}`);
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
else {
|
|
1967
|
-
// Test file was REJECTED due to validation
|
|
1968
|
-
console.log(`\n❌ Test file REJECTED: ${result.error}`);
|
|
1969
|
-
testFileWritten = false; // Make sure we track it wasn't written
|
|
1970
|
-
// Give very specific instructions based on rejection reason
|
|
1971
|
-
if (result.error.includes('placeholder')) {
|
|
1972
|
-
messages.push({
|
|
1973
|
-
role: 'user',
|
|
1974
|
-
content: `Your test file was REJECTED because it contains placeholder comments.
|
|
1975
|
-
|
|
1976
|
-
You MUST rewrite it with COMPLETE code:
|
|
1977
|
-
- Remove ALL comments like "// Further tests", "// Add test", "// Mock setup"
|
|
1978
|
-
- Write the ACTUAL test implementation for EVERY function
|
|
1979
|
-
- Each test needs: real setup, real function call, real expect() assertions
|
|
1980
|
-
|
|
1981
|
-
Try again with write_test_file and provide COMPLETE test implementations!`
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
else if (result.error.includes('NO expect()')) {
|
|
1985
|
-
messages.push({
|
|
1986
|
-
role: 'user',
|
|
1987
|
-
content: `Your test file was REJECTED because tests have no assertions!
|
|
1988
|
-
|
|
1989
|
-
Every test MUST have expect() statements. Example:
|
|
1990
|
-
expect(functionName).toHaveBeenCalled();
|
|
1991
|
-
expect(result).toEqual(expectedValue);
|
|
1992
|
-
|
|
1993
|
-
Rewrite with write_test_file and add actual expect() assertions to ALL tests!`
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
else if (result.error.includes('too few tests')) {
|
|
1997
|
-
messages.push({
|
|
1998
|
-
role: 'user',
|
|
1999
|
-
content: `Your test file was REJECTED because it has too few tests!
|
|
2000
|
-
|
|
2001
|
-
You analyzed ${toolResults.length > 0 ? 'multiple' : 'several'} functions in the source file. Write tests for ALL of them!
|
|
2002
|
-
- Minimum 2-3 test cases per function
|
|
2003
|
-
- Cover: happy path, edge cases, error cases
|
|
2004
|
-
|
|
2005
|
-
Rewrite with write_test_file and include tests for EVERY function!`
|
|
2006
|
-
});
|
|
2007
|
-
}
|
|
3133
|
+
console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
2008
3134
|
}
|
|
2009
3135
|
}
|
|
2010
|
-
// Detect
|
|
2011
|
-
if (
|
|
2012
|
-
console.log(
|
|
3136
|
+
// Detect syntax errors from validation
|
|
3137
|
+
if (result.syntaxError && result.location) {
|
|
3138
|
+
console.log(`\n❌ Syntax error introduced at line ${result.location.line}!`);
|
|
2013
3139
|
messages.push({
|
|
2014
3140
|
role: 'user',
|
|
2015
|
-
content:
|
|
3141
|
+
content: `🚨 SYNTAX ERROR DETECTED at line ${result.location.line}:${result.location.column}
|
|
3142
|
+
|
|
3143
|
+
${result.error}
|
|
2016
3144
|
|
|
2017
|
-
|
|
3145
|
+
💡 ${result.suggestion}
|
|
2018
3146
|
|
|
2019
|
-
|
|
2020
|
-
Step 2: Identify which lines need changes
|
|
2021
|
-
Step 3: Use the appropriate tool:
|
|
2022
|
-
- insert_lines: Add missing imports/mocks (e.g., line 5)
|
|
2023
|
-
- delete_lines: Remove incorrect code (e.g., lines 10-12)
|
|
2024
|
-
- replace_lines: Fix specific sections (e.g., lines 20-25)
|
|
3147
|
+
Your last modification created invalid syntax and was ROLLED BACK automatically.
|
|
2025
3148
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
3149
|
+
To fix this:
|
|
3150
|
+
1. Use read_file to see the current file content (includes line numbers)
|
|
3151
|
+
2. Find the section you need to modify around line ${result.location.line}
|
|
3152
|
+
3. Use search_replace_block with correct syntax:
|
|
3153
|
+
- Include 3-5 lines of context around the target
|
|
3154
|
+
- Ensure your replacement has valid syntax (matching brackets, quotes, etc.)
|
|
3155
|
+
- Double-check for missing semicolons, commas, or closing brackets
|
|
2029
3156
|
|
|
2030
|
-
|
|
3157
|
+
Example:
|
|
3158
|
+
search_replace_block({
|
|
3159
|
+
file_path: "${toolCall.input.file_path || toolCall.input.test_file_path || 'test file'}",
|
|
3160
|
+
search: "valid context from file\nline with issue\nmore context",
|
|
3161
|
+
replace: "valid context from file\nCORRECTED line with proper syntax\nmore context"
|
|
3162
|
+
})
|
|
2031
3163
|
|
|
2032
|
-
Start
|
|
3164
|
+
Start NOW by reading the file around line ${result.location.line}!`
|
|
2033
3165
|
});
|
|
2034
3166
|
}
|
|
2035
3167
|
// Detect repeated errors (suggests legitimate failure)
|
|
@@ -2040,15 +3172,15 @@ Start with read_file NOW to see line numbers!`
|
|
|
2040
3172
|
sameErrorCount++;
|
|
2041
3173
|
console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
|
|
2042
3174
|
if (sameErrorCount >= 3) {
|
|
2043
|
-
console.log('\n🚨 Same error repeated 3+ times!
|
|
3175
|
+
console.log('\n🚨 Same error repeated 3+ times! ');
|
|
2044
3176
|
messages.push({
|
|
2045
3177
|
role: 'user',
|
|
2046
3178
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
2047
3179
|
|
|
2048
|
-
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
2049
3180
|
|
|
2050
3181
|
Analyze the error and determine:
|
|
2051
3182
|
1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
|
|
3183
|
+
2. Use available tools file read_file_lines to read the current state of file.
|
|
2052
3184
|
2. Or is this a LEGITIMATE source code bug?
|
|
2053
3185
|
|
|
2054
3186
|
If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
|
|
@@ -2076,20 +3208,23 @@ If FIXABLE: Make one more attempt to fix it.`
|
|
|
2076
3208
|
role: 'user',
|
|
2077
3209
|
content: `Import path error detected! Module not found: "${missingModule}"
|
|
2078
3210
|
|
|
2079
|
-
✅ FIX WITH
|
|
3211
|
+
✅ FIX WITH SEARCH-REPLACE:
|
|
2080
3212
|
|
|
2081
3213
|
Step 1: find_file tool to search for "${filename}" in the repository
|
|
2082
|
-
Step 2: calculate_relative_path tool to get correct import path
|
|
2083
|
-
Step 3: Fix using
|
|
2084
|
-
a)
|
|
2085
|
-
b)
|
|
2086
|
-
c)
|
|
3214
|
+
Step 2: calculate_relative_path tool to get correct import path
|
|
3215
|
+
Step 3: Fix using search_replace_block:
|
|
3216
|
+
a) Include the broken import line + 2-3 surrounding lines for context
|
|
3217
|
+
b) Replace with corrected import using the right path
|
|
3218
|
+
c) The tool handles whitespace/indentation automatically!
|
|
2087
3219
|
|
|
2088
3220
|
Example workflow:
|
|
2089
3221
|
1. find_file({ filename: "${filename}.ts" })
|
|
2090
3222
|
2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
|
|
2091
|
-
3.
|
|
2092
|
-
|
|
3223
|
+
3. search_replace_block({
|
|
3224
|
+
file_path: "${testFilePath}",
|
|
3225
|
+
search: "import { something } from './other';\nimport { broken } from '${missingModule}';\nimport { another } from './path';",
|
|
3226
|
+
replace: "import { something } from './other';\nimport { fixed } from './correct-path';\nimport { another } from './path';"
|
|
3227
|
+
})
|
|
2093
3228
|
|
|
2094
3229
|
Start NOW with find_file!`
|
|
2095
3230
|
});
|
|
@@ -2102,28 +3237,25 @@ Start NOW with find_file!`
|
|
|
2102
3237
|
role: 'user',
|
|
2103
3238
|
content: `The test is failing because the source file imports modules that initialize database connections.
|
|
2104
3239
|
|
|
2105
|
-
✅ FIX WITH
|
|
2106
|
-
|
|
2107
|
-
Step 1: read_file to see current test file structure
|
|
2108
|
-
Step 2: insert_lines to add mocks at the TOP of the file (before any imports)
|
|
3240
|
+
✅ FIX WITH SEARCH-REPLACE:
|
|
2109
3241
|
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
3242
|
+
Option 1 (Recommended): Use insert_at_position to add mocks at beginning:
|
|
3243
|
+
insert_at_position({
|
|
3244
|
+
file_path: "${testFilePath}",
|
|
3245
|
+
position: "beginning",
|
|
3246
|
+
content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../database/index', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\n"
|
|
3247
|
+
})
|
|
2114
3248
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
line_number: 1,
|
|
2121
|
-
content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n"
|
|
3249
|
+
Option 2: If file already has some mocks, use search_replace_block to add more:
|
|
3250
|
+
search_replace_block({
|
|
3251
|
+
file_path: "${testFilePath}",
|
|
3252
|
+
search: "jest.mock('./existing-mock');\n\nimport { something }",
|
|
3253
|
+
replace: "jest.mock('./existing-mock');\njest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\nimport { something }"
|
|
2122
3254
|
})
|
|
2123
3255
|
|
|
2124
3256
|
⚠️ Mocks MUST be at the TOP before any imports!
|
|
2125
3257
|
|
|
2126
|
-
Start NOW with
|
|
3258
|
+
Start NOW with insert_at_position!`
|
|
2127
3259
|
});
|
|
2128
3260
|
}
|
|
2129
3261
|
}
|
|
@@ -2291,97 +3423,240 @@ async function generateTestsForFolder() {
|
|
|
2291
3423
|
console.log(`\n✨ Folder processing complete! Processed ${files.length} files.`);
|
|
2292
3424
|
}
|
|
2293
3425
|
// Function-wise test generation
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
3426
|
+
/**
|
|
3427
|
+
* Generate tests for a single function
|
|
3428
|
+
* @returns true if tests passed, false if legitimate failure reported
|
|
3429
|
+
*/
|
|
3430
|
+
async function generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists) {
|
|
2298
3431
|
const messages = [
|
|
2299
3432
|
{
|
|
2300
3433
|
role: 'user',
|
|
2301
|
-
content: `You are
|
|
3434
|
+
content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
|
|
2302
3435
|
|
|
2303
|
-
|
|
2304
|
-
Test file
|
|
2305
|
-
Test file exists: ${testFileExists}
|
|
2306
|
-
Selected functions to test: ${functionNames.join(', ')}
|
|
3436
|
+
## CONTEXT
|
|
3437
|
+
Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
2307
3438
|
|
|
2308
|
-
|
|
3439
|
+
---
|
|
2309
3440
|
|
|
2310
|
-
|
|
2311
|
-
🚨 CRITICAL WARNING: Test file ALREADY EXISTS at ${testFilePath}! 🚨
|
|
3441
|
+
## EXECUTION PLAN
|
|
2312
3442
|
|
|
2313
|
-
|
|
2314
|
-
|
|
3443
|
+
**Phase 1: Deep Analysis**
|
|
3444
|
+
\\\`\\\`\\\`
|
|
3445
|
+
1. analyze_file_ast(${sourceFile}) → function metadata.
|
|
3446
|
+
2. get_function_ast(${sourceFile},{functionName}) → implementation + dependencies
|
|
3447
|
+
3. For each dependency:
|
|
3448
|
+
- Same file: get_function_ast(${sourceFile},{functionName})
|
|
3449
|
+
- Other file [Can take reference from the imports of the ${sourceFile} file for the file name that has the required function]: find_file(filename) to get file path -> get_function_ast({file_path},{functionName}) + check for external calls
|
|
3450
|
+
4. get_imports_ast → all dependencies
|
|
3451
|
+
5. calculate_relative_path for each import
|
|
3452
|
+
6. get_file_preamble → imports and mocks already declared in the file
|
|
3453
|
+
\\\`\\\`\\\`
|
|
2315
3454
|
|
|
2316
|
-
|
|
2317
|
-
|
|
3455
|
+
**Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
|
|
3456
|
+
*Before writing tests, map the logic requirements for external calls.*
|
|
3457
|
+
1. Identify every external call (e.g., \`analyticsHelper.postEvent\`).
|
|
3458
|
+
2. Trace backwards: What \`if\`, \`switch\`, or \`try/catch\` block guards this call?
|
|
3459
|
+
3. Identify the dependency that controls that guard.
|
|
3460
|
+
4. Plan the Mock Return: Determine exactly what value the dependency must return to enter that block.
|
|
2318
3461
|
|
|
2319
|
-
|
|
2320
|
-
1. FIRST: Use analyze_file_ast tool to get information about the selected functions: ${functionNames.join(', ')}
|
|
2321
|
-
2. Use get_function_ast tool for each selected function to get detailed information
|
|
2322
|
-
3. Use get_imports_ast tool to understand dependencies
|
|
2323
|
-
4. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths
|
|
2324
|
-
5. Generate comprehensive Jest unit tests ONLY for these functions: ${functionNames.join(', ')}
|
|
2325
|
-
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
|
|
2326
|
-
- Mock database modules, models, services, and config modules
|
|
2327
|
-
- Use jest.mock() calls at the TOP of the file before any imports
|
|
2328
|
-
- Test suites for each selected function
|
|
2329
|
-
- Multiple test cases covering:
|
|
2330
|
-
* Happy path scenarios
|
|
2331
|
-
* Edge cases (null, undefined, empty arrays, etc.)
|
|
2332
|
-
* Error conditions
|
|
2333
|
-
* Async behavior (if applicable)
|
|
2334
|
-
- Proper TypeScript types
|
|
2335
|
-
- Clear, descriptive test names
|
|
2336
|
-
- Complete test implementations (NO placeholder comments!)
|
|
2337
|
-
${testFileExists ? `
|
|
2338
|
-
6. 🚨 CRITICAL: Test file EXISTS! Call replace_function_tests tool for EACH function: ${functionNames.join(', ')}
|
|
2339
|
-
- Call replace_function_tests ONCE for each function
|
|
2340
|
-
- Pass the complete describe block as new_test_content parameter
|
|
2341
|
-
- Example: replace_function_tests(test_file_path: "${testFilePath}", function_name: "${functionNames[0]}", new_test_content: "describe('${functionNames[0]}', () => { ... })")
|
|
2342
|
-
- This preserves ALL other existing tests in the file
|
|
2343
|
-
- DO NOT use write_test_file! It will DELETE all other tests!
|
|
2344
|
-
- DO NOT use edit_test_file! Use replace_function_tests instead!` : `
|
|
2345
|
-
6. REQUIRED: Test file does NOT exist. Use write_test_file tool with tests for: ${functionNames.join(', ')} with function_mode set to true.
|
|
2346
|
-
- Create a new test file with complete test implementation
|
|
2347
|
-
- [CRITICAL]: Set function_mode parameter to true if using write_test_file tool`}}
|
|
2348
|
-
7. REQUIRED: Run the tests using run_tests tool
|
|
2349
|
-
8. REQUIRED: If tests fail, analyze if errors are FIXABLE or LEGITIMATE:
|
|
2350
|
-
|
|
2351
|
-
FIXABLE ERRORS (fix these):
|
|
2352
|
-
- Wrong import paths → use find_file + calculate_relative_path + edit tools
|
|
2353
|
-
- Missing mocks → add proper jest.mock() calls
|
|
2354
|
-
- Incorrect mock implementations → update mock return values
|
|
2355
|
-
- Wrong test assertions → fix expect() statements
|
|
2356
|
-
- TypeScript errors → fix types and imports
|
|
2357
|
-
|
|
2358
|
-
LEGITIMATE FAILURES (report these):
|
|
2359
|
-
- Function returns wrong type (source code bug)
|
|
2360
|
-
- Missing null checks in source code
|
|
2361
|
-
- Logic errors in source code
|
|
2362
|
-
- Source code throws unexpected errors
|
|
2363
|
-
|
|
2364
|
-
9. If FIXABLE (AFTER test file is written/updated):
|
|
2365
|
-
${testFileExists ? `- ✅ PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
|
|
2366
|
-
* read_file to see current test file with line numbers
|
|
2367
|
-
* delete_lines to remove incorrect lines
|
|
2368
|
-
* insert_lines to add missing mocks, imports, or test cases
|
|
2369
|
-
* replace_lines to fix specific sections
|
|
2370
|
-
* This preserves ALL other tests and is more reliable!
|
|
2371
|
-
- ⚠️ SECONDARY: replace_function_tests for specific function updates
|
|
2372
|
-
- ❌ AVOID: write_test_file (will DELETE all other tests!)` : `- Use write_test_file to create the test file
|
|
2373
|
-
- Once written, use line-based tools for fixes (read_file + insert/delete/replace_lines)`}
|
|
2374
|
-
- Then retry tests
|
|
2375
|
-
10. If LEGITIMATE: Call report_legitimate_failure with details and STOP
|
|
2376
|
-
11. REQUIRED: Repeat steps 7-10 until tests pass OR legitimate failures reported
|
|
2377
|
-
|
|
2378
|
-
${testFileExists ? `
|
|
2379
|
-
🚨 REMINDER: The test file EXISTS! Use replace_function_tests, NOT write_test_file! 🚨
|
|
2380
|
-
` : ''}
|
|
2381
|
-
|
|
2382
|
-
CRITICAL: Fix test bugs but REPORT source code bugs (don't try to make broken code pass)!
|
|
3462
|
+
**Phase 2: Test Generation**
|
|
2383
3463
|
|
|
2384
|
-
|
|
3464
|
+
Mock Pattern (CRITICAL - Top of file, before imports):
|
|
3465
|
+
\\\`\\\`\\\`typescript
|
|
3466
|
+
// ===== MOCKS (BEFORE IMPORTS) =====
|
|
3467
|
+
jest.mock('config', () => ({
|
|
3468
|
+
get: (key: string) => ({
|
|
3469
|
+
AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
|
|
3470
|
+
USER_DEL_SECRET: 'secret'
|
|
3471
|
+
})
|
|
3472
|
+
}), { virtual: true });
|
|
3473
|
+
|
|
3474
|
+
jest.mock('../path/from/calculate_relative_path');
|
|
3475
|
+
// Never virtual:true for actual source helpers!
|
|
3476
|
+
// ⚠️ CRITICAL: Mock ALL dependencies at top level, even if unused
|
|
3477
|
+
|
|
3478
|
+
// ===== IMPORTS =====
|
|
3479
|
+
import { functionName } from '../controller';
|
|
3480
|
+
\\\`\\\`\\\`
|
|
3481
|
+
|
|
3482
|
+
Requirements (5+ tests minimum):
|
|
3483
|
+
- ✅ Happy path
|
|
3484
|
+
- 🔸 Edge cases (null, undefined, empty)
|
|
3485
|
+
- ❌ Error conditions
|
|
3486
|
+
- ⏱️ Async behavior
|
|
3487
|
+
- 🔍 API null/undefined handling
|
|
3488
|
+
|
|
3489
|
+
**Phase 3: Anti-Pollution Pattern (MANDATORY)**
|
|
3490
|
+
|
|
3491
|
+
### Step 1: Mock Setup (Top of File)
|
|
3492
|
+
// ===== MOCKS (BEFORE IMPORTS) =====
|
|
3493
|
+
jest.mock('config', () => ({
|
|
3494
|
+
get: () => ({ KEY: 'value' })
|
|
3495
|
+
}), { virtual: true }); // virtual:true ONLY for config, db, models
|
|
3496
|
+
|
|
3497
|
+
jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
|
|
3498
|
+
|
|
3499
|
+
// ===== IMPORTS =====
|
|
3500
|
+
import { functionName } from '../controller';
|
|
3501
|
+
import { dependencyMethod } from '../helpers/dependency';
|
|
3502
|
+
|
|
3503
|
+
// ===== TYPED MOCKS =====
|
|
3504
|
+
const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
|
|
3505
|
+
|
|
3506
|
+
### Step 2: Test Structure
|
|
3507
|
+
describe('functionName', () => {
|
|
3508
|
+
beforeEach(() => {
|
|
3509
|
+
// ALWAYS first line
|
|
3510
|
+
jest.clearAllMocks();
|
|
3511
|
+
|
|
3512
|
+
// Set defaults for THIS describe block only
|
|
3513
|
+
mockDependencyMethod.mockResolvedValue({ status: 'success' });
|
|
3514
|
+
});
|
|
3515
|
+
|
|
3516
|
+
test('happy path', async () => {
|
|
3517
|
+
// Override default for this test only
|
|
3518
|
+
mockDependencyMethod.mockResolvedValueOnce({ id: 123 });
|
|
3519
|
+
|
|
3520
|
+
const result = await functionName();
|
|
3521
|
+
|
|
3522
|
+
expect(result).toEqual({ id: 123 });
|
|
3523
|
+
expect(mockDependencyMethod).toHaveBeenCalledWith(expect.objectContaining({
|
|
3524
|
+
param: 'value'
|
|
3525
|
+
}));
|
|
3526
|
+
});
|
|
3527
|
+
|
|
3528
|
+
test('error case', async () => {
|
|
3529
|
+
mockDependencyMethod.mockRejectedValueOnce(new Error('fail'));
|
|
3530
|
+
|
|
3531
|
+
const result = await functionName();
|
|
3532
|
+
|
|
3533
|
+
expect(result).toEqual({});
|
|
3534
|
+
});
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3537
|
+
describe('anotherFunction', () => {
|
|
3538
|
+
beforeEach(() => {
|
|
3539
|
+
jest.clearAllMocks();
|
|
3540
|
+
|
|
3541
|
+
// Different defaults for different function
|
|
3542
|
+
mockDependencyMethod.mockResolvedValue({ status: 'pending' });
|
|
3543
|
+
});
|
|
3544
|
+
|
|
3545
|
+
// ... tests
|
|
3546
|
+
});
|
|
3547
|
+
|
|
3548
|
+
|
|
3549
|
+
### Step 3: Internal Function Mocking (When Needed)
|
|
3550
|
+
|
|
3551
|
+
describe('functionWithInternalCalls', () => {
|
|
3552
|
+
let internalFnSpy: jest.SpyInstance;
|
|
3553
|
+
|
|
3554
|
+
beforeEach(() => {
|
|
3555
|
+
jest.clearAllMocks();
|
|
3556
|
+
|
|
3557
|
+
const controller = require('../controller');
|
|
3558
|
+
internalFnSpy = jest.spyOn(controller, 'internalFunction')
|
|
3559
|
+
.mockResolvedValue(undefined);
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
afterEach(() => {
|
|
3563
|
+
internalFnSpy.mockRestore();
|
|
3564
|
+
});
|
|
3565
|
+
|
|
3566
|
+
test('calls internal function', async () => {
|
|
3567
|
+
await functionWithInternalCalls();
|
|
3568
|
+
expect(internalFnSpy).toHaveBeenCalled();
|
|
3569
|
+
});
|
|
3570
|
+
});
|
|
3571
|
+
|
|
3572
|
+
### CRITICAL RULES:
|
|
3573
|
+
**DO ✅**
|
|
3574
|
+
1. **Mock at file top** - All \`jest.mock()\` calls before any imports
|
|
3575
|
+
2. **Import directly** - Use \`import { fn } from 'module'\` (never \`require()\`)
|
|
3576
|
+
3. **Type all mocks** - \`const mockFn = fn as jest.MockedFunction<typeof fn>\`
|
|
3577
|
+
4. **Clear first always** - \`jest.clearAllMocks()\` as first line in every \`beforeEach()\`
|
|
3578
|
+
5. **Isolate describe defaults** - Each \`describe\` block sets its own mock defaults
|
|
3579
|
+
6. **Override with mockOnce** - Use \`mockResolvedValueOnce/mockReturnValueOnce\` in tests
|
|
3580
|
+
7. **Restore spies** - Use \`jest.spyOn()\` with \`mockRestore()\` for internal function spies
|
|
3581
|
+
8. **Use calculate_relative_path** - For all import and mock paths
|
|
3582
|
+
|
|
3583
|
+
**DON'T ❌**
|
|
3584
|
+
1. **Re-require modules** - Never use \`require()\` in \`beforeEach()\` or tests
|
|
3585
|
+
2. **Check mock existence** - Never \`if (!mockFn)\` - indicates broken setup
|
|
3586
|
+
3. **Share mock state** - Don't rely on defaults from other \`describe\` blocks
|
|
3587
|
+
4. **Skip jest.clearAllMocks()** - Missing this is the #1 cause of pollution
|
|
3588
|
+
5. **Use virtual:true everywhere** - Only for: config, db, models, routes, services
|
|
3589
|
+
6. **Forget to restore spies** - Always pair \`jest.spyOn()\` with \`mockRestore()\`
|
|
3590
|
+
|
|
3591
|
+
### Why This Works:
|
|
3592
|
+
- **No pollution**: Each describe block sets its own defaults in beforeEach
|
|
3593
|
+
- **No conflicts**: clearAllMocks() resets all mock state
|
|
3594
|
+
- **Type safety**: TypeScript catches mock mismatches
|
|
3595
|
+
- **Predictable**: Tests run in any order with same results
|
|
3596
|
+
|
|
3597
|
+
**Phase 4: Write Tests**
|
|
3598
|
+
→ upsert_function_tests({
|
|
3599
|
+
test_file_path: "${testFilePath}",
|
|
3600
|
+
function_name: "${functionName}",
|
|
3601
|
+
new_test_content: "describe('${functionName}', () => {...})"
|
|
3602
|
+
})
|
|
3603
|
+
This will automatically replace the existing test cases for the function with the new test cases or add new test cases if the function is not found in the test file.
|
|
3604
|
+
|
|
3605
|
+
|
|
3606
|
+
|
|
3607
|
+
## PHASE 5: SELF-REVIEW (Before Running Tests)
|
|
3608
|
+
|
|
3609
|
+
**Review Checklist:**
|
|
3610
|
+
1. ✅ All jest.mock() calls at top of file (before imports)?
|
|
3611
|
+
2. ✅ Used calculate_relative_path for all mock paths?
|
|
3612
|
+
3. ✅ Functions imported directly (not loaded via require)?
|
|
3613
|
+
4. ✅ Mocks typed with \`as jest.MockedFunction<typeof fn>\`?
|
|
3614
|
+
5. ✅ beforeEach() has jest.clearAllMocks() as FIRST line?
|
|
3615
|
+
6. ✅ Each describe block sets its own default mock values in beforeEach()?
|
|
3616
|
+
7. ✅ Used mockResolvedValueOnce/mockReturnValueOnce for test overrides?
|
|
3617
|
+
8. ✅ At least 5 test cases (happy/error/edge/async/null)?
|
|
3618
|
+
9. ✅ All async functions use async/await in tests?
|
|
3619
|
+
10. ✅ Spies on internal functions restored in afterEach()?
|
|
3620
|
+
11. ✅ No re-requiring modules or checking if mocks exist?
|
|
3621
|
+
12. ✅ No TypeScript type errors in test code?
|
|
3622
|
+
|
|
3623
|
+
**Phase 6: Tests - Run & Fix - Loop**
|
|
3624
|
+
|
|
3625
|
+
1️⃣ Run: \`run_tests({ test_file_path: "${testFilePath}", function_names: ["${functionName}"] })\`
|
|
3626
|
+
|
|
3627
|
+
2️⃣ If fails, categorize:
|
|
3628
|
+
|
|
3629
|
+
**[MUST] FIXABLE** → Fix these:
|
|
3630
|
+
| Error | Fix Method |
|
|
3631
|
+
|-------|-----------|
|
|
3632
|
+
| Wrong imports | find_file(fileName) to get the file path + calculate_relative_path + search_replace_block |
|
|
3633
|
+
| Missing mocks | insert_at_position |
|
|
3634
|
+
| Syntax errors | search_replace_block (3-5 lines context) |
|
|
3635
|
+
| Mock pollution | Fix beforeEach pattern |
|
|
3636
|
+
| "Test suite failed to run" | get_file_preamble + fix imports/mocks |
|
|
3637
|
+
| "Cannot find module" | calculate_relative_path |
|
|
3638
|
+
|
|
3639
|
+
**LEGITIMATE** → Report, don't fix:
|
|
3640
|
+
- Source returns wrong type
|
|
3641
|
+
- Missing null checks in source
|
|
3642
|
+
- Logic errors in source
|
|
3643
|
+
|
|
3644
|
+
⛔ NEVER report "Test suite failed to run" as legitimate
|
|
3645
|
+
|
|
3646
|
+
3️⃣ Repeat until: ✅ Pass OR 📋 Legitimate failure (report_legitimate_failure)
|
|
3647
|
+
|
|
3648
|
+
---
|
|
3649
|
+
|
|
3650
|
+
## CRITICAL REMINDERS
|
|
3651
|
+
|
|
3652
|
+
- Use calculate_relative_path for ALL jest.mock() paths
|
|
3653
|
+
- virtual:true ONLY for: db, config, models, routes, index, services, axios, newrelic, GOOGLE_CLOUD_STORAGE
|
|
3654
|
+
- search_replace_block preferred (handles whitespace)
|
|
3655
|
+
- Ensure test independence (no pollution)
|
|
3656
|
+
- Fix test bugs, report source bugs
|
|
3657
|
+
- [CRITICAL] Each test suite should be completely self-contained and not depend on or affect any other test suite's state.
|
|
3658
|
+
|
|
3659
|
+
**START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file.`
|
|
2385
3660
|
}
|
|
2386
3661
|
];
|
|
2387
3662
|
let iterations = 0;
|
|
@@ -2392,6 +3667,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2392
3667
|
let lastTestError = '';
|
|
2393
3668
|
let sameErrorCount = 0;
|
|
2394
3669
|
while (iterations < maxIterations) {
|
|
3670
|
+
console.log('USING CLAUDE PROMPT original 16');
|
|
2395
3671
|
iterations++;
|
|
2396
3672
|
if (iterations === 1) {
|
|
2397
3673
|
console.log(`\n🤖 AI is analyzing selected functions...`);
|
|
@@ -2400,6 +3676,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2400
3676
|
console.log(`\n🤖 AI is still working (step ${iterations})...`);
|
|
2401
3677
|
}
|
|
2402
3678
|
const response = await callAI(messages, TOOLS);
|
|
3679
|
+
console.log('response from AI', JSON.stringify(response, null, 2));
|
|
2403
3680
|
if (response.content) {
|
|
2404
3681
|
const content = response.content;
|
|
2405
3682
|
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
@@ -2418,7 +3695,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2418
3695
|
console.log('\n⚠️ AI is making excuses! Forcing it to use tools...');
|
|
2419
3696
|
messages.push({
|
|
2420
3697
|
role: 'user',
|
|
2421
|
-
content: 'STOP making excuses! You CAN use the tools. Use
|
|
3698
|
+
content: 'STOP making excuses! You CAN use the tools. Use upsert_function_tests tool NOW to write the test cases for the function.'
|
|
2422
3699
|
});
|
|
2423
3700
|
continue;
|
|
2424
3701
|
}
|
|
@@ -2439,42 +3716,35 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2439
3716
|
if (!testFileWritten) {
|
|
2440
3717
|
messages.push({
|
|
2441
3718
|
role: 'user',
|
|
2442
|
-
content:
|
|
2443
|
-
? `🚨 STOP TALKING! The test file EXISTS at ${testFilePath}!
|
|
2444
|
-
|
|
2445
|
-
Call replace_function_tests tool NOW for EACH function: ${functionNames.join(', ')}
|
|
3719
|
+
content: `🚨 STOP TALKING! Use upsert_function_tests tool NOW for: ${functionName}
|
|
2446
3720
|
|
|
2447
3721
|
Example:
|
|
2448
|
-
|
|
3722
|
+
upsert_function_tests({
|
|
2449
3723
|
test_file_path: "${testFilePath}",
|
|
2450
|
-
function_name: "${
|
|
2451
|
-
new_test_content: "describe('${
|
|
3724
|
+
function_name: "${functionName}",
|
|
3725
|
+
new_test_content: "describe('${functionName}', () => { test('should...', () => { ... }) })"
|
|
2452
3726
|
})
|
|
2453
3727
|
|
|
2454
|
-
|
|
2455
|
-
: `Use write_test_file tool NOW with complete test code for: ${functionNames.join(', ')}`
|
|
3728
|
+
This works for both NEW and EXISTING test files!`
|
|
2456
3729
|
});
|
|
2457
3730
|
}
|
|
2458
3731
|
else {
|
|
2459
3732
|
messages.push({
|
|
2460
3733
|
role: 'user',
|
|
2461
|
-
content:
|
|
2462
|
-
? `STOP talking and USE TOOLS!
|
|
3734
|
+
content: `STOP talking and USE TOOLS NOW!
|
|
2463
3735
|
|
|
2464
|
-
✅ PRIMARY
|
|
2465
|
-
1.
|
|
2466
|
-
2.
|
|
3736
|
+
✅ PRIMARY: Use search_replace_block (RECOMMENDED):
|
|
3737
|
+
1. Include 3-5 lines of context around the code to fix
|
|
3738
|
+
2. Replace with corrected version
|
|
3739
|
+
3. Handles whitespace/indentation automatically!
|
|
3740
|
+
4. Then run_tests to verify
|
|
2467
3741
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
: `STOP talking and USE TOOLS!
|
|
2473
|
-
|
|
2474
|
-
- If test file doesn't exist: write_test_file
|
|
2475
|
-
- If test file exists: read_file + line-based editing tools
|
|
3742
|
+
📌 ALTERNATIVE: Use insert_at_position for adding imports/mocks
|
|
3743
|
+
- insert_at_position({ position: 'after_imports', content: "jest.mock('../module');" })
|
|
3744
|
+
|
|
3745
|
+
⚠️ SECONDARY: Use upsert_function_tests for function-level rewrites
|
|
2476
3746
|
|
|
2477
|
-
|
|
3747
|
+
Start NOW with search_replace_block or insert_at_position!`
|
|
2478
3748
|
});
|
|
2479
3749
|
}
|
|
2480
3750
|
continue;
|
|
@@ -2504,15 +3774,14 @@ Act NOW!`
|
|
|
2504
3774
|
sameErrorCount++;
|
|
2505
3775
|
console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
|
|
2506
3776
|
if (sameErrorCount >= 3) {
|
|
2507
|
-
console.log('\n🚨 Same error repeated 3+ times!
|
|
3777
|
+
console.log('\n🚨 Same error repeated 3+ times! ');
|
|
2508
3778
|
messages.push({
|
|
2509
3779
|
role: 'user',
|
|
2510
3780
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
2511
3781
|
|
|
2512
|
-
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
2513
3782
|
|
|
2514
3783
|
If this is a source code bug: Call report_legitimate_failure tool NOW.
|
|
2515
|
-
If this is still fixable: Make
|
|
3784
|
+
If this is still fixable: Make focused attempt to fix it.`
|
|
2516
3785
|
});
|
|
2517
3786
|
}
|
|
2518
3787
|
}
|
|
@@ -2522,32 +3791,16 @@ If this is still fixable: Make ONE final attempt to fix it.`
|
|
|
2522
3791
|
}
|
|
2523
3792
|
}
|
|
2524
3793
|
// Track if test file was written
|
|
2525
|
-
if (toolCall.name === '
|
|
3794
|
+
if (toolCall.name === 'upsert_function_tests') {
|
|
2526
3795
|
if (result.success) {
|
|
2527
3796
|
testFileWritten = true;
|
|
2528
3797
|
console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
3798
|
+
messages.push({
|
|
3799
|
+
role: 'user',
|
|
3800
|
+
content: `Test files are written successfully. Please use run_tests tool to verify the tests. If the tests fail, please make focused attempts to fix the tests using the tools available.`
|
|
3801
|
+
});
|
|
2529
3802
|
}
|
|
2530
3803
|
}
|
|
2531
|
-
// Detect if AI incorrectly used write_test_file when file exists
|
|
2532
|
-
if (toolCall.name === 'write_test_file' && testFileExists) {
|
|
2533
|
-
console.log('\n⚠️ WARNING: AI used write_test_file on existing file! This overwrites all other tests!');
|
|
2534
|
-
messages.push({
|
|
2535
|
-
role: 'user',
|
|
2536
|
-
content: `❌ CRITICAL ERROR: You used write_test_file but the test file ALREADY EXISTS!
|
|
2537
|
-
|
|
2538
|
-
This OVERWROTE the entire file and DELETED all other tests! This is WRONG!
|
|
2539
|
-
|
|
2540
|
-
You MUST use replace_function_tests instead. For future fixes, call it for EACH function:
|
|
2541
|
-
|
|
2542
|
-
${functionNames.map(fname => `replace_function_tests({
|
|
2543
|
-
test_file_path: "${testFilePath}",
|
|
2544
|
-
function_name: "${fname}",
|
|
2545
|
-
new_test_content: "describe('${fname}', () => { /* your tests */ })"
|
|
2546
|
-
})`).join('\n\n')}
|
|
2547
|
-
|
|
2548
|
-
DO NOT use write_test_file when the file exists! Always use replace_function_tests!`
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
3804
|
}
|
|
2552
3805
|
// Add tool results to conversation based on provider
|
|
2553
3806
|
if (CONFIG.aiProvider === 'claude') {
|
|
@@ -2627,6 +3880,224 @@ DO NOT use write_test_file when the file exists! Always use replace_function_tes
|
|
|
2627
3880
|
console.log('\n📋 Test file updated with legitimate failures documented.');
|
|
2628
3881
|
console.log(' These failures indicate bugs in the source code that need to be fixed.');
|
|
2629
3882
|
}
|
|
3883
|
+
// Return true if tests passed, false if legitimate failure reported
|
|
3884
|
+
// Get the LAST test run result (not the first) to check final status
|
|
3885
|
+
const testRuns = allToolResults.filter(tr => tr.name === 'run_tests');
|
|
3886
|
+
const lastTestRun = testRuns.length > 0 ? testRuns[testRuns.length - 1] : null;
|
|
3887
|
+
return !legitimateFailureReported && (lastTestRun?.result?.passed || false);
|
|
3888
|
+
}
|
|
3889
|
+
/** [Not useful, introduce side-effect]
|
|
3890
|
+
* Validate and fix the complete test file after all functions are processed
|
|
3891
|
+
* Runs full test suite and fixes file-level issues (mock pollution, imports, etc)
|
|
3892
|
+
*/
|
|
3893
|
+
async function validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames) {
|
|
3894
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
3895
|
+
console.log(`🔍 FINAL VALIDATION: Running complete test suite`);
|
|
3896
|
+
console.log(`${'='.repeat(80)}\n`);
|
|
3897
|
+
// Run tests for entire file (no function filter)
|
|
3898
|
+
const testResult = runTests(testFilePath);
|
|
3899
|
+
if (testResult.passed) {
|
|
3900
|
+
console.log(`\n✅ Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
// Tests failed - give AI a chance to fix file-level issues
|
|
3904
|
+
console.log(`\n⚠️ Complete test suite has failures. Analyzing for file-level issues...\n`);
|
|
3905
|
+
const messages = [
|
|
3906
|
+
{
|
|
3907
|
+
role: 'user',
|
|
3908
|
+
content: `You are a senior software engineer fixing file-level integration issues in a Jest test file.
|
|
3909
|
+
|
|
3910
|
+
Source file: ${sourceFile}
|
|
3911
|
+
Test file: ${testFilePath}
|
|
3912
|
+
Functions tested: ${functionNames.join(', ')}
|
|
3913
|
+
|
|
3914
|
+
CONTEXT:
|
|
3915
|
+
- All ${functionNames.length} functions have been tested individually and passed their own tests
|
|
3916
|
+
- However, when running the COMPLETE test suite together, some tests are failing
|
|
3917
|
+
- This suggests FILE-LEVEL issues like:
|
|
3918
|
+
* Mock pollution between test suites
|
|
3919
|
+
* Shared state not being cleaned up
|
|
3920
|
+
* Import/mock ordering issues
|
|
3921
|
+
* beforeEach/afterEach missing or incorrect
|
|
3922
|
+
* Mock implementations interfering with each other
|
|
3923
|
+
|
|
3924
|
+
TEST OUTPUT:
|
|
3925
|
+
${testResult.output}
|
|
3926
|
+
|
|
3927
|
+
YOUR TASK:
|
|
3928
|
+
1. Use get_file_preamble to see current imports, mocks, and setup blocks
|
|
3929
|
+
2. Identify file-level issues (NOT individual function logic issues)
|
|
3930
|
+
3. Fix using search_replace_block or insert_at_position tools
|
|
3931
|
+
4. Run tests again with run_tests tool
|
|
3932
|
+
5. Repeat until tests pass OR you determine failures are legitimate source code bugs
|
|
3933
|
+
|
|
3934
|
+
COMMON FILE-LEVEL ISSUES TO CHECK:
|
|
3935
|
+
- ❌ Missing jest.clearAllMocks() in beforeEach
|
|
3936
|
+
- ❌ Mocks not being reset between test suites
|
|
3937
|
+
- ❌ Module requires inside describe blocks (should use let + beforeEach)
|
|
3938
|
+
- ❌ Missing virtual:true for config/database/models mocks
|
|
3939
|
+
- ❌ beforeEach hooks not clearing all shared state
|
|
3940
|
+
- ❌ Test suites depending on execution order
|
|
3941
|
+
|
|
3942
|
+
FIXABLE ISSUES (you should fix):
|
|
3943
|
+
- Mock pollution between test suites
|
|
3944
|
+
- Missing cleanup in beforeEach/afterEach
|
|
3945
|
+
- Incorrect mock setup at file level
|
|
3946
|
+
- Import ordering issues
|
|
3947
|
+
- Missing jest.clearAllMocks()
|
|
3948
|
+
|
|
3949
|
+
LEGITIMATE FAILURES (report these):
|
|
3950
|
+
- Source code bugs causing actual logic errors
|
|
3951
|
+
- Missing null checks in source code
|
|
3952
|
+
- Wrong return types from source functions
|
|
3953
|
+
- Use report_legitimate_failure tool for these
|
|
3954
|
+
|
|
3955
|
+
CRITICAL RULES:
|
|
3956
|
+
1. DO NOT change individual test logic (they passed individually!)
|
|
3957
|
+
2. Focus ONLY on file-level integration issues
|
|
3958
|
+
3. Use search_replace_block to fix specific sections
|
|
3959
|
+
4. Preserve all existing test cases
|
|
3960
|
+
5. If failures are due to source code bugs, call report_legitimate_failure and STOP
|
|
3961
|
+
|
|
3962
|
+
START by calling get_file_preamble to understand current file structure.`
|
|
3963
|
+
}
|
|
3964
|
+
];
|
|
3965
|
+
let iterations = 0;
|
|
3966
|
+
const maxIterations = 200; // Limit iterations for file-level fixes
|
|
3967
|
+
let allToolResults = [];
|
|
3968
|
+
let legitimateFailureReported = false;
|
|
3969
|
+
while (iterations < maxIterations) {
|
|
3970
|
+
iterations++;
|
|
3971
|
+
console.log(`\n🔧 Fixing attempt ${iterations}/${maxIterations}...`);
|
|
3972
|
+
const response = await callAI(messages, TOOLS);
|
|
3973
|
+
if (response.content) {
|
|
3974
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
3975
|
+
}
|
|
3976
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3977
|
+
// AI stopped without fixing - check if tests pass now
|
|
3978
|
+
const finalTest = runTests(testFilePath);
|
|
3979
|
+
if (finalTest.passed) {
|
|
3980
|
+
console.log('\n✅ Complete test suite now passes!');
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
console.log('\n⚠️ AI stopped but tests still failing. Prompting to continue...');
|
|
3984
|
+
messages.push({
|
|
3985
|
+
role: 'user',
|
|
3986
|
+
content: 'Tests are still failing! Use tools to fix or call report_legitimate_failure if these are source code bugs.'
|
|
3987
|
+
});
|
|
3988
|
+
continue;
|
|
3989
|
+
}
|
|
3990
|
+
// Execute tool calls
|
|
3991
|
+
const toolResults = [];
|
|
3992
|
+
for (const toolCall of response.toolCalls) {
|
|
3993
|
+
const result = await executeTool(toolCall.name, toolCall.input);
|
|
3994
|
+
const toolResult = { id: toolCall.id, name: toolCall.name, result };
|
|
3995
|
+
toolResults.push(toolResult);
|
|
3996
|
+
allToolResults.push(toolResult);
|
|
3997
|
+
// Check if legitimate failure reported
|
|
3998
|
+
if (toolCall.name === 'report_legitimate_failure' && result.success) {
|
|
3999
|
+
legitimateFailureReported = true;
|
|
4000
|
+
console.log('\n✅ Legitimate failures reported. Stopping fixes.');
|
|
4001
|
+
break;
|
|
4002
|
+
}
|
|
4003
|
+
// Check if tests passed
|
|
4004
|
+
if (toolCall.name === 'run_tests' && result.passed) {
|
|
4005
|
+
console.log('\n🎉 Complete test suite now passes!');
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
if (legitimateFailureReported) {
|
|
4010
|
+
break;
|
|
4011
|
+
}
|
|
4012
|
+
// Add tool results to conversation
|
|
4013
|
+
if (CONFIG.aiProvider === 'claude') {
|
|
4014
|
+
messages.push({
|
|
4015
|
+
role: 'assistant',
|
|
4016
|
+
content: response.toolCalls.map(tc => ({
|
|
4017
|
+
type: 'tool_use',
|
|
4018
|
+
id: tc.id,
|
|
4019
|
+
name: tc.name,
|
|
4020
|
+
input: tc.input
|
|
4021
|
+
}))
|
|
4022
|
+
});
|
|
4023
|
+
messages.push({
|
|
4024
|
+
role: 'user',
|
|
4025
|
+
content: toolResults.map(tr => ({
|
|
4026
|
+
type: 'tool_result',
|
|
4027
|
+
tool_use_id: tr.id,
|
|
4028
|
+
content: JSON.stringify(tr.result)
|
|
4029
|
+
}))
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
else if (CONFIG.aiProvider === 'openai') {
|
|
4033
|
+
messages.push({
|
|
4034
|
+
role: 'assistant',
|
|
4035
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
4036
|
+
id: tc.id,
|
|
4037
|
+
type: 'function',
|
|
4038
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
4039
|
+
}))
|
|
4040
|
+
});
|
|
4041
|
+
for (const tr of toolResults) {
|
|
4042
|
+
messages.push({
|
|
4043
|
+
role: 'tool',
|
|
4044
|
+
tool_call_id: tr.id,
|
|
4045
|
+
content: JSON.stringify(tr.result)
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
else {
|
|
4050
|
+
// Gemini
|
|
4051
|
+
for (const toolCall of response.toolCalls) {
|
|
4052
|
+
messages.push({
|
|
4053
|
+
role: 'model',
|
|
4054
|
+
functionCall: { name: toolCall.name, args: toolCall.input }
|
|
4055
|
+
});
|
|
4056
|
+
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
4057
|
+
messages.push({
|
|
4058
|
+
role: 'user',
|
|
4059
|
+
functionResponse: { name: toolCall.name, response: result?.result }
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
if (iterations >= maxIterations) {
|
|
4065
|
+
console.log(`\n⚠️ Reached max iterations (${maxIterations}) for file-level fixes.`);
|
|
4066
|
+
console.log(' Some test failures may remain. Check test output above.');
|
|
4067
|
+
}
|
|
4068
|
+
if (legitimateFailureReported) {
|
|
4069
|
+
console.log('\n📋 Test file has legitimate failures due to source code bugs.');
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
/**
|
|
4073
|
+
* Generate tests for multiple functions, one at a time
|
|
4074
|
+
*/
|
|
4075
|
+
async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
4076
|
+
console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
|
|
4077
|
+
const testFilePath = getTestFilePath(sourceFile);
|
|
4078
|
+
let testFileExists = fsSync.existsSync(testFilePath);
|
|
4079
|
+
// Process each function one at a time
|
|
4080
|
+
for (let i = 0; i < functionNames.length; i++) {
|
|
4081
|
+
const functionName = functionNames[i];
|
|
4082
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
4083
|
+
console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
|
|
4084
|
+
console.log(`${'='.repeat(80)}\n`);
|
|
4085
|
+
const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
|
|
4086
|
+
// After first function completes, test file will exist for subsequent functions
|
|
4087
|
+
testFileExists = true;
|
|
4088
|
+
if (passed) {
|
|
4089
|
+
console.log(`\n✅ Function '${functionName}' tests completed successfully!`);
|
|
4090
|
+
}
|
|
4091
|
+
else {
|
|
4092
|
+
console.log(`\n⚠️ Function '${functionName}' completed with issues. Continuing to next function...`);
|
|
4093
|
+
}
|
|
4094
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
4095
|
+
}
|
|
4096
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
4097
|
+
console.log(`✅ All ${functionNames.length} function(s) processed!`);
|
|
4098
|
+
console.log(`${'='.repeat(80)}\n`);
|
|
4099
|
+
// FINAL VALIDATION: Run complete test suite and fix file-level issues
|
|
4100
|
+
// await validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames);
|
|
2630
4101
|
return testFilePath;
|
|
2631
4102
|
}
|
|
2632
4103
|
async function generateTestsForFunction() {
|
|
@@ -2676,8 +4147,236 @@ async function generateTestsForFunction() {
|
|
|
2676
4147
|
await generateTestsForFunctions(selectedFile, selectedFunctions);
|
|
2677
4148
|
console.log('\n✨ Done!');
|
|
2678
4149
|
}
|
|
4150
|
+
/**
|
|
4151
|
+
* Get git diff output for both staged and unstaged changes
|
|
4152
|
+
* Returns the combined diff content and list of changed source files
|
|
4153
|
+
*/
|
|
4154
|
+
async function getGitDiff() {
|
|
4155
|
+
try {
|
|
4156
|
+
// Check if we're in a git repository
|
|
4157
|
+
try {
|
|
4158
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
4159
|
+
}
|
|
4160
|
+
catch {
|
|
4161
|
+
throw new Error('Not a git repository');
|
|
4162
|
+
}
|
|
4163
|
+
// Get both staged and unstaged changes with context
|
|
4164
|
+
const stagedDiff = (0, child_process_1.execSync)('git diff --staged', { encoding: 'utf-8', stdio: 'pipe' }).toString();
|
|
4165
|
+
// const unstagedDiff = execSync('git diff', { encoding: 'utf-8', stdio: 'pipe' }).toString();
|
|
4166
|
+
// Combine both diffs
|
|
4167
|
+
const fullDiff = stagedDiff;
|
|
4168
|
+
console.log('Full diff is', fullDiff);
|
|
4169
|
+
if (!fullDiff.trim()) {
|
|
4170
|
+
return { fullDiff: '', files: [] };
|
|
4171
|
+
}
|
|
4172
|
+
// Parse diff to extract per-file diffs
|
|
4173
|
+
const files = [];
|
|
4174
|
+
const diffSections = fullDiff.split(/^diff --git /m).filter(Boolean);
|
|
4175
|
+
for (const section of diffSections) {
|
|
4176
|
+
const lines = section.split('\n');
|
|
4177
|
+
const firstLine = 'diff --git ' + lines[0];
|
|
4178
|
+
// Extract file path from diff header
|
|
4179
|
+
const match = firstLine.match(/b\/(.+)$/);
|
|
4180
|
+
if (match) {
|
|
4181
|
+
const filePath = match[1];
|
|
4182
|
+
const ext = path.extname(filePath);
|
|
4183
|
+
const basename = path.basename(filePath);
|
|
4184
|
+
// Skip test files and non-source files
|
|
4185
|
+
const isTestFile = basename.includes('.test.') ||
|
|
4186
|
+
basename.includes('.spec.') ||
|
|
4187
|
+
basename.includes('test.') ||
|
|
4188
|
+
basename.includes('spec.') ||
|
|
4189
|
+
filePath.includes('/__tests__/') ||
|
|
4190
|
+
filePath.includes('/tests/') ||
|
|
4191
|
+
filePath.includes(CONFIG.testDir);
|
|
4192
|
+
// Only include supported source files (not test files)
|
|
4193
|
+
if (CONFIG.extensions.includes(ext) && !isTestFile) {
|
|
4194
|
+
files.push({
|
|
4195
|
+
filePath,
|
|
4196
|
+
diff: 'diff --git ' + section
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
return { fullDiff, files };
|
|
4202
|
+
}
|
|
4203
|
+
catch (error) {
|
|
4204
|
+
if (error.message === 'Not a git repository') {
|
|
4205
|
+
throw error;
|
|
4206
|
+
}
|
|
4207
|
+
console.error('⚠️ Error reading git diff:', error.message);
|
|
4208
|
+
return { fullDiff: '', files: [] };
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* Use AI to extract changed function names from git diff
|
|
4213
|
+
*/
|
|
4214
|
+
async function getChangedFunctionsFromDiff(filePath, diff) {
|
|
4215
|
+
try {
|
|
4216
|
+
// First, check if file exists
|
|
4217
|
+
if (!fsSync.existsSync(filePath)) {
|
|
4218
|
+
console.log(` ⚠️ File not found: ${filePath}`);
|
|
4219
|
+
return [];
|
|
4220
|
+
}
|
|
4221
|
+
// First, get all exported functions from the file
|
|
4222
|
+
console.log(` 📁 Analyzing file: ${filePath}`);
|
|
4223
|
+
const result = analyzeFileAST(filePath);
|
|
4224
|
+
console.log(` 📊 AST Analysis result:`, JSON.stringify({
|
|
4225
|
+
success: result.success,
|
|
4226
|
+
totalFunctions: result.analysis?.functions?.length || 0,
|
|
4227
|
+
error: result.error || 'none'
|
|
4228
|
+
}, null, 2));
|
|
4229
|
+
if (!result.success || !result.analysis || !result.analysis.functions) {
|
|
4230
|
+
console.log(` ⚠️ Failed to analyze file: ${result.error || 'unknown error'}`);
|
|
4231
|
+
return [];
|
|
4232
|
+
}
|
|
4233
|
+
// Filter to only exported functions
|
|
4234
|
+
const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
|
|
4235
|
+
console.log(` 📦 Found ${exportedFunctions.length} exported function(s):`, exportedFunctions.map((f) => f.name).join(', '));
|
|
4236
|
+
if (exportedFunctions.length === 0) {
|
|
4237
|
+
console.log(` ⚠️ No exported functions found in file`);
|
|
4238
|
+
console.log(` 💡 Tip: Make sure the file has exported functions (export const/function/async)`);
|
|
4239
|
+
return [];
|
|
4240
|
+
}
|
|
4241
|
+
const exportedFunctionNames = exportedFunctions.map((f) => f.name);
|
|
4242
|
+
console.log(` 📦 Exported functions in file: ${exportedFunctionNames.join(', ')}`);
|
|
4243
|
+
// Ask AI to identify which functions appear in the diff
|
|
4244
|
+
const prompt = `You are analyzing a git diff to identify which exported functions have been changed or added.
|
|
4245
|
+
|
|
4246
|
+
Here is the git diff for file: ${filePath}
|
|
4247
|
+
|
|
4248
|
+
\`\`\`diff
|
|
4249
|
+
${diff}
|
|
4250
|
+
\`\`\`
|
|
4251
|
+
|
|
4252
|
+
The file has the following exported functions: ${exportedFunctionNames.join(', ')}
|
|
4253
|
+
|
|
4254
|
+
Your task: Look at the git diff and identify which of these exported functions have changes inside them.
|
|
4255
|
+
- Look at the @@ line numbers and context to see which function the changes are in
|
|
4256
|
+
- A function is changed if any line within that function's body is modified (marked with + or -)
|
|
4257
|
+
- Return ONLY a JSON array of function names, like: ["functionName1", "functionName2"]
|
|
4258
|
+
- If no exported functions are in the diff, return an empty array: []
|
|
4259
|
+
|
|
4260
|
+
Return ONLY the JSON array, nothing else.`;
|
|
4261
|
+
const messages = [
|
|
4262
|
+
{
|
|
4263
|
+
role: 'user',
|
|
4264
|
+
content: prompt
|
|
4265
|
+
}
|
|
4266
|
+
];
|
|
4267
|
+
console.log(` 🤖 Calling AI to analyze diff...`);
|
|
4268
|
+
// Call AI without tools - just need text response
|
|
4269
|
+
const response = await callAI(messages, [], CONFIG.aiProvider);
|
|
4270
|
+
if (!response.content) {
|
|
4271
|
+
console.error(' ⚠️ AI returned no content');
|
|
4272
|
+
return [];
|
|
4273
|
+
}
|
|
4274
|
+
// Parse AI response to extract function names
|
|
4275
|
+
const content = response.content.trim();
|
|
4276
|
+
console.log(` 🤖 AI response: ${content}`);
|
|
4277
|
+
// Try to extract JSON array from response
|
|
4278
|
+
const jsonMatch = content.match(/\[.*\]/s);
|
|
4279
|
+
if (jsonMatch) {
|
|
4280
|
+
try {
|
|
4281
|
+
const functionNames = JSON.parse(jsonMatch[0]);
|
|
4282
|
+
if (Array.isArray(functionNames)) {
|
|
4283
|
+
// Filter to only include functions that actually exist in the file
|
|
4284
|
+
const filtered = functionNames.filter(name => exportedFunctionNames.includes(name));
|
|
4285
|
+
console.log(` ✅ Extracted ${filtered.length} function(s) from AI response: ${filtered.join(', ')}`);
|
|
4286
|
+
return filtered;
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
catch (parseError) {
|
|
4290
|
+
console.error(' ⚠️ Failed to parse AI response as JSON:', content);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
else {
|
|
4294
|
+
console.log(' ⚠️ No JSON array found in AI response');
|
|
4295
|
+
}
|
|
4296
|
+
return [];
|
|
4297
|
+
}
|
|
4298
|
+
catch (error) {
|
|
4299
|
+
console.error(` ⚠️ Error getting changed functions from AI:`, error.message);
|
|
4300
|
+
return [];
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
/**
|
|
4304
|
+
* Auto-generate tests for changed functions detected via git diff
|
|
4305
|
+
*/
|
|
4306
|
+
async function autoGenerateTests() {
|
|
4307
|
+
console.log('🔍 Scanning git changes...\n');
|
|
4308
|
+
try {
|
|
4309
|
+
// Get all changes from git diff
|
|
4310
|
+
console.log('Getting git diff...');
|
|
4311
|
+
const { fullDiff, files } = await getGitDiff();
|
|
4312
|
+
console.log('Full diff is', fullDiff);
|
|
4313
|
+
console.log('Files are', files);
|
|
4314
|
+
if (files.length === 0) {
|
|
4315
|
+
console.log('✅ No changes detected in source files.');
|
|
4316
|
+
console.log(' (Only staged and unstaged changes are checked)');
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
console.log(`📝 Found changes in ${files.length} file(s)\n`);
|
|
4320
|
+
let totalFunctions = 0;
|
|
4321
|
+
let processedFiles = 0;
|
|
4322
|
+
let errorFiles = 0;
|
|
4323
|
+
// Process each changed file
|
|
4324
|
+
for (const fileInfo of files) {
|
|
4325
|
+
const { filePath, diff } = fileInfo;
|
|
4326
|
+
// Check if file exists
|
|
4327
|
+
if (!fsSync.existsSync(filePath)) {
|
|
4328
|
+
console.log(`⏭️ Skipping ${filePath} (file not found)`);
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
console.log(`\n🔄 Processing: ${filePath}`);
|
|
4332
|
+
console.log(` Analyzing diff with AI...`);
|
|
4333
|
+
// Use AI to extract changed function names from diff
|
|
4334
|
+
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4335
|
+
console.log('Changed functions are', changedFunctions);
|
|
4336
|
+
if (changedFunctions.length === 0) {
|
|
4337
|
+
console.log(` ⏭️ No exported functions changed`);
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4340
|
+
console.log(` 📦 Changed functions: ${changedFunctions.join(', ')}`);
|
|
4341
|
+
try {
|
|
4342
|
+
// Use existing generateTestsForFunctions
|
|
4343
|
+
await generateTestsForFunctions(filePath, changedFunctions);
|
|
4344
|
+
processedFiles++;
|
|
4345
|
+
totalFunctions += changedFunctions.length;
|
|
4346
|
+
console.log(` ✅ Tests generated successfully`);
|
|
4347
|
+
}
|
|
4348
|
+
catch (error) {
|
|
4349
|
+
errorFiles++;
|
|
4350
|
+
console.error(` ❌ Error generating tests: ${error.message}`);
|
|
4351
|
+
// Continue with next file
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
// Summary
|
|
4355
|
+
console.log('\n' + '='.repeat(60));
|
|
4356
|
+
console.log('📊 Auto-Generation Summary');
|
|
4357
|
+
console.log('='.repeat(60));
|
|
4358
|
+
console.log(`✅ Successfully processed: ${processedFiles} file(s)`);
|
|
4359
|
+
console.log(`📝 Functions tested: ${totalFunctions}`);
|
|
4360
|
+
if (errorFiles > 0) {
|
|
4361
|
+
console.log(`⚠️ Failed: ${errorFiles} file(s)`);
|
|
4362
|
+
}
|
|
4363
|
+
console.log('='.repeat(60));
|
|
4364
|
+
console.log('\n✨ Done!');
|
|
4365
|
+
}
|
|
4366
|
+
catch (error) {
|
|
4367
|
+
if (error.message === 'Not a git repository') {
|
|
4368
|
+
console.error('❌ Error: Not a git repository');
|
|
4369
|
+
console.error(' Auto mode requires git to detect changes.');
|
|
4370
|
+
process.exit(1);
|
|
4371
|
+
}
|
|
4372
|
+
throw error;
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
2679
4375
|
async function main() {
|
|
2680
4376
|
console.log('🧪 AI-Powered Unit Test Generator with AST Analysis\n');
|
|
4377
|
+
// Check for auto mode from CLI arguments EARLY (before any prompts)
|
|
4378
|
+
const args = process.argv.slice(2);
|
|
4379
|
+
const isAutoMode = args.includes('auto');
|
|
2681
4380
|
// Load configuration from codeguard.json
|
|
2682
4381
|
try {
|
|
2683
4382
|
CONFIG = (0, config_1.loadConfig)();
|
|
@@ -2700,6 +4399,22 @@ async function main() {
|
|
|
2700
4399
|
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
2701
4400
|
process.exit(1);
|
|
2702
4401
|
}
|
|
4402
|
+
// If auto mode, skip indexing setup and proceed directly
|
|
4403
|
+
if (isAutoMode) {
|
|
4404
|
+
console.log('🤖 Auto Mode: Detecting changes via git diff\n');
|
|
4405
|
+
console.log(`✅ Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
4406
|
+
// Initialize indexer if it exists, but don't prompt
|
|
4407
|
+
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
4408
|
+
const hasExistingIndex = globalIndexer.hasIndex();
|
|
4409
|
+
if (hasExistingIndex) {
|
|
4410
|
+
await globalIndexer.loadIndex();
|
|
4411
|
+
}
|
|
4412
|
+
else {
|
|
4413
|
+
globalIndexer = null;
|
|
4414
|
+
}
|
|
4415
|
+
await autoGenerateTests();
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
2703
4418
|
// Optional: Codebase Indexing
|
|
2704
4419
|
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
2705
4420
|
const hasExistingIndex = globalIndexer.hasIndex();
|