codeguard-testgen 1.0.7 โ 1.0.9
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 +2156 -536
- 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)
|
|
@@ -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']
|
|
@@ -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',
|
|
@@ -351,8 +344,8 @@ const TOOLS = [
|
|
|
351
344
|
}
|
|
352
345
|
},
|
|
353
346
|
{
|
|
354
|
-
name: '
|
|
355
|
-
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.',
|
|
356
349
|
input_schema: {
|
|
357
350
|
type: 'object',
|
|
358
351
|
properties: {
|
|
@@ -360,66 +353,50 @@ const TOOLS = [
|
|
|
360
353
|
type: 'string',
|
|
361
354
|
description: 'The path to the file to edit'
|
|
362
355
|
},
|
|
363
|
-
|
|
364
|
-
type: 'number',
|
|
365
|
-
description: 'The starting line number to delete (1-indexed, inclusive)'
|
|
366
|
-
},
|
|
367
|
-
end_line: {
|
|
368
|
-
type: 'number',
|
|
369
|
-
description: 'The ending line number to delete (1-indexed, inclusive)'
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
required: ['file_path', 'start_line', 'end_line']
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
name: 'insert_lines',
|
|
377
|
-
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.',
|
|
378
|
-
input_schema: {
|
|
379
|
-
type: 'object',
|
|
380
|
-
properties: {
|
|
381
|
-
file_path: {
|
|
356
|
+
search: {
|
|
382
357
|
type: 'string',
|
|
383
|
-
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.'
|
|
384
359
|
},
|
|
385
|
-
|
|
386
|
-
type: '
|
|
387
|
-
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.'
|
|
388
363
|
},
|
|
389
|
-
|
|
364
|
+
match_mode: {
|
|
390
365
|
type: 'string',
|
|
391
|
-
|
|
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.'
|
|
392
368
|
}
|
|
393
369
|
},
|
|
394
|
-
required: ['file_path', '
|
|
370
|
+
required: ['file_path', 'search', 'replace']
|
|
395
371
|
}
|
|
396
372
|
},
|
|
397
373
|
{
|
|
398
|
-
name: '
|
|
399
|
-
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.',
|
|
400
376
|
input_schema: {
|
|
401
377
|
type: 'object',
|
|
402
378
|
properties: {
|
|
403
379
|
file_path: {
|
|
404
380
|
type: 'string',
|
|
405
|
-
description: 'The path to the file
|
|
381
|
+
description: 'The path to the file'
|
|
406
382
|
},
|
|
407
|
-
|
|
408
|
-
type: '
|
|
409
|
-
description: 'The
|
|
383
|
+
content: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'The content to insert'
|
|
410
386
|
},
|
|
411
|
-
|
|
412
|
-
type: '
|
|
413
|
-
|
|
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)'
|
|
414
391
|
},
|
|
415
|
-
|
|
392
|
+
after_marker: {
|
|
416
393
|
type: 'string',
|
|
417
|
-
description: '
|
|
394
|
+
description: 'Optional: Instead of position, insert after this specific text marker (uses fuzzy search)'
|
|
418
395
|
}
|
|
419
396
|
},
|
|
420
|
-
required: ['file_path', '
|
|
397
|
+
required: ['file_path', 'content']
|
|
421
398
|
}
|
|
422
|
-
}
|
|
399
|
+
},
|
|
423
400
|
];
|
|
424
401
|
exports.TOOLS = TOOLS;
|
|
425
402
|
// AST Parsing utilities
|
|
@@ -640,41 +617,86 @@ function getTypeAnnotation(typeNode) {
|
|
|
640
617
|
return 'unknown';
|
|
641
618
|
}
|
|
642
619
|
function getFunctionAST(filePath, functionName) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
path.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
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
|
+
}
|
|
667
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 };
|
|
668
653
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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;
|
|
672
665
|
}
|
|
673
|
-
return Object.assign({ success: true }, functionInfo);
|
|
674
666
|
}
|
|
675
667
|
catch (error) {
|
|
676
|
-
|
|
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
|
+
};
|
|
677
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
|
+
};
|
|
678
700
|
}
|
|
679
701
|
function extractFunctionDetails(path, lines, isVariable = false) {
|
|
680
702
|
const node = isVariable ? path.node.init : path.node;
|
|
@@ -728,6 +750,7 @@ function extractFunctionDetails(path, lines, isVariable = false) {
|
|
|
728
750
|
// If traversal fails, just continue without called functions
|
|
729
751
|
console.warn('Could not extract called functions:', error);
|
|
730
752
|
}
|
|
753
|
+
// console.log(` โ
Found code: ${code}`);
|
|
731
754
|
return {
|
|
732
755
|
name: isVariable ? path.node.id.name : node.id?.name,
|
|
733
756
|
code,
|
|
@@ -744,6 +767,7 @@ function extractFunctionDetails(path, lines, isVariable = false) {
|
|
|
744
767
|
// Removed estimateComplexity to avoid scope/parentPath traversal issues
|
|
745
768
|
// Complexity is now hardcoded to 1 in extractFunctionDetails
|
|
746
769
|
function getImportsAST(filePath) {
|
|
770
|
+
// Try to read the file
|
|
747
771
|
try {
|
|
748
772
|
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
749
773
|
const ast = parseFileToAST(filePath, content);
|
|
@@ -791,9 +815,55 @@ function getImportsAST(filePath) {
|
|
|
791
815
|
}
|
|
792
816
|
}
|
|
793
817
|
});
|
|
794
|
-
return { success: true, imports };
|
|
818
|
+
return { success: true, imports, filePath };
|
|
795
819
|
}
|
|
796
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
|
+
}
|
|
797
867
|
return { success: false, error: error.message };
|
|
798
868
|
}
|
|
799
869
|
}
|
|
@@ -855,118 +925,703 @@ function getTypeDefinitions(filePath) {
|
|
|
855
925
|
});
|
|
856
926
|
}
|
|
857
927
|
});
|
|
858
|
-
return { success: true, types };
|
|
928
|
+
return { success: true, types, filePath };
|
|
859
929
|
}
|
|
860
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
|
+
}
|
|
861
977
|
return { success: false, error: error.message };
|
|
862
978
|
}
|
|
863
979
|
}
|
|
864
|
-
function
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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'
|
|
888
1064
|
});
|
|
889
1065
|
}
|
|
890
1066
|
});
|
|
891
1067
|
}
|
|
892
|
-
classInfo = {
|
|
893
|
-
name: className,
|
|
894
|
-
methods,
|
|
895
|
-
superClass: path.node.superClass?.name
|
|
896
|
-
};
|
|
897
|
-
path.stop();
|
|
898
1068
|
}
|
|
899
1069
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
+
};
|
|
903
1109
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
// Try the provided file path first
|
|
1115
|
+
const result = extractPreamble(filePath);
|
|
1116
|
+
if (result) {
|
|
1117
|
+
return result;
|
|
908
1118
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
+
}
|
|
918
1164
|
return {
|
|
919
1165
|
success: false,
|
|
920
|
-
error: `File
|
|
921
|
-
|
|
922
|
-
|
|
1166
|
+
error: `File not found: ${filePath}`,
|
|
1167
|
+
suggestion: matches.length > 0 ? matches[0] : undefined,
|
|
1168
|
+
allMatches: matches.length > 1 ? matches : undefined
|
|
923
1169
|
};
|
|
924
1170
|
}
|
|
925
|
-
return { success: true, content };
|
|
926
|
-
}
|
|
927
|
-
catch (error) {
|
|
928
|
-
return { success: false, error: error.message };
|
|
929
1171
|
}
|
|
1172
|
+
// No indexer available
|
|
1173
|
+
return {
|
|
1174
|
+
success: false,
|
|
1175
|
+
error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
|
|
1176
|
+
};
|
|
930
1177
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
}
|
|
941
1216
|
}
|
|
1217
|
+
});
|
|
1218
|
+
if (classInfo) {
|
|
1219
|
+
return { success: true, ...classInfo };
|
|
942
1220
|
}
|
|
943
|
-
|
|
944
|
-
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
// Try the provided file path first
|
|
1228
|
+
try {
|
|
1229
|
+
const result = searchInFile(filePath);
|
|
1230
|
+
if (result) {
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
// File not found or other error - continue to fallback
|
|
1236
|
+
}
|
|
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
|
+
}
|
|
1253
|
+
}
|
|
945
1254
|
}
|
|
946
1255
|
}
|
|
947
1256
|
return {
|
|
948
|
-
success:
|
|
949
|
-
|
|
950
|
-
isExternal: true
|
|
1257
|
+
success: false,
|
|
1258
|
+
error: `Class ${className} not found in ${filePath} or any indexed files`
|
|
951
1259
|
};
|
|
952
1260
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
+
};
|
|
956
1266
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
async function writeTestFile(filePath, content, sourceFilePath, functionMode = false) {
|
|
1613
|
+
try {
|
|
1614
|
+
// VALIDATE: Check for incomplete/placeholder tests BEFORE writing
|
|
1615
|
+
const invalidPatterns = [
|
|
1616
|
+
/\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
|
|
1617
|
+
/\/\/\s*(Add more|write more|Similarly|write tests for)/i,
|
|
1618
|
+
/\/\/\s*TODO/i,
|
|
1619
|
+
/\/\/\s*\.\.\./,
|
|
1620
|
+
/\/\/.*etc\./i,
|
|
1621
|
+
/expect\(\).*\/\//, // expect() followed by comment
|
|
1622
|
+
];
|
|
1623
|
+
const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
|
|
1624
|
+
if (hasPlaceholders) {
|
|
970
1625
|
// Extract the actual placeholder comment for the error message
|
|
971
1626
|
const foundPlaceholder = content.match(invalidPatterns.find(p => p.test(content)) || /\/\/.*/);
|
|
972
1627
|
return {
|
|
@@ -1000,18 +1655,12 @@ async function writeTestFile(filePath, content, sourceFilePath, functionMode = f
|
|
|
1000
1655
|
}
|
|
1001
1656
|
}
|
|
1002
1657
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
if (testCount < Math.max(4, expectedFunctionCount * 2)) {
|
|
1010
|
-
return {
|
|
1011
|
-
success: false,
|
|
1012
|
-
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!`
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
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
|
+
// }
|
|
1015
1664
|
}
|
|
1016
1665
|
const dir = path.dirname(filePath);
|
|
1017
1666
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -1070,7 +1719,7 @@ async function editTestFile(filePath, oldContent, newContent) {
|
|
|
1070
1719
|
const preview = content.substring(0, 500);
|
|
1071
1720
|
return {
|
|
1072
1721
|
success: false,
|
|
1073
|
-
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.`,
|
|
1074
1723
|
currentContent: preview
|
|
1075
1724
|
};
|
|
1076
1725
|
}
|
|
@@ -1154,6 +1803,19 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1154
1803
|
updatedLines.push(...afterBlock);
|
|
1155
1804
|
const updatedContent = updatedLines.join('\n');
|
|
1156
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
|
+
}
|
|
1157
1819
|
return {
|
|
1158
1820
|
success: true,
|
|
1159
1821
|
message: `Replaced existing tests for function '${functionName}' (lines ${describeStartLine + 1}-${describeEndLine + 1})`,
|
|
@@ -1197,10 +1859,23 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1197
1859
|
// Validate bracket pairing
|
|
1198
1860
|
const openCount = (updatedContent.match(/{/g) || []).length;
|
|
1199
1861
|
const closeCount = (updatedContent.match(/}/g) || []).length;
|
|
1200
|
-
if (openCount !== closeCount) {
|
|
1201
|
-
|
|
1202
|
-
}
|
|
1862
|
+
// if (openCount !== closeCount) {
|
|
1863
|
+
// console.warn(`โ ๏ธ Warning: Bracket mismatch detected (${openCount} opening, ${closeCount} closing). The test file structure may be malformed.`);
|
|
1864
|
+
// }
|
|
1203
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
|
+
}
|
|
1204
1879
|
return {
|
|
1205
1880
|
success: true,
|
|
1206
1881
|
message: `Added tests for function '${functionName}' at root level (after line ${lastRootLevelClosing + 1})`,
|
|
@@ -1217,7 +1892,7 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1217
1892
|
function runTests(testFilePath, functionNames) {
|
|
1218
1893
|
try {
|
|
1219
1894
|
// Build Jest command with optional function name filter
|
|
1220
|
-
let command = `npx jest ${testFilePath} --no-coverage`;
|
|
1895
|
+
let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
|
|
1221
1896
|
// If function names provided, use Jest -t flag to run only those tests
|
|
1222
1897
|
if (functionNames && functionNames.length > 0) {
|
|
1223
1898
|
// Create regex pattern to match any of the function names
|
|
@@ -1230,6 +1905,7 @@ function runTests(testFilePath, functionNames) {
|
|
|
1230
1905
|
encoding: 'utf-8',
|
|
1231
1906
|
stdio: 'pipe'
|
|
1232
1907
|
});
|
|
1908
|
+
console.log(` Test run output: ${output}`);
|
|
1233
1909
|
return {
|
|
1234
1910
|
success: true,
|
|
1235
1911
|
output,
|
|
@@ -1238,6 +1914,8 @@ function runTests(testFilePath, functionNames) {
|
|
|
1238
1914
|
};
|
|
1239
1915
|
}
|
|
1240
1916
|
catch (error) {
|
|
1917
|
+
console.log(` Test run error: ${error.message}`);
|
|
1918
|
+
console.log(`output sent to ai: ${error.stdout + error.stderr}`);
|
|
1241
1919
|
return {
|
|
1242
1920
|
success: false,
|
|
1243
1921
|
output: error.stdout + error.stderr,
|
|
@@ -1338,6 +2016,13 @@ function calculateRelativePath(fromFile, toFile) {
|
|
|
1338
2016
|
}
|
|
1339
2017
|
// Convert backslashes to forward slashes (Windows compatibility)
|
|
1340
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
|
+
// })})`)
|
|
1341
2026
|
return {
|
|
1342
2027
|
success: true,
|
|
1343
2028
|
from: fromFile,
|
|
@@ -1454,43 +2139,276 @@ async function replaceLines(filePath, startLine, endLine, newContent) {
|
|
|
1454
2139
|
return { success: false, error: error.message };
|
|
1455
2140
|
}
|
|
1456
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
|
+
}
|
|
1457
2375
|
// User-friendly messages for each tool
|
|
1458
2376
|
const TOOL_MESSAGES = {
|
|
1459
2377
|
'read_file': '๐ Reading source file',
|
|
2378
|
+
'read_file_lines': '๐ Reading file section',
|
|
1460
2379
|
'analyze_file_ast': '๐ Analyzing codebase structure',
|
|
1461
2380
|
'get_function_ast': '๐ Examining function details',
|
|
1462
2381
|
'get_imports_ast': '๐ฆ Analyzing dependencies',
|
|
1463
2382
|
'get_type_definitions': '๐ Extracting type definitions',
|
|
2383
|
+
'get_file_preamble': '๐ Extracting file preamble (imports, mocks, setup)',
|
|
1464
2384
|
'get_class_methods': '๐๏ธ Analyzing class structure',
|
|
1465
2385
|
'resolve_import_path': '๐ Resolving import paths',
|
|
1466
|
-
'
|
|
1467
|
-
'edit_test_file': 'โ๏ธ Updating test file',
|
|
1468
|
-
'replace_function_tests': '๐ Replacing test cases for specific functions',
|
|
2386
|
+
'upsert_function_tests': 'โ๏ธ Writing/updating test cases for function',
|
|
1469
2387
|
'run_tests': '๐งช Running tests',
|
|
1470
2388
|
'list_directory': '๐ Exploring directory structure',
|
|
1471
2389
|
'find_file': '๐ Locating file in repository',
|
|
1472
2390
|
'calculate_relative_path': '๐งญ Calculating import path',
|
|
1473
2391
|
'report_legitimate_failure': 'โ ๏ธ Reporting legitimate test failures',
|
|
1474
|
-
'
|
|
1475
|
-
'
|
|
1476
|
-
'replace_lines': '๐ Replacing lines in file'
|
|
2392
|
+
'search_replace_block': '๐๐ Searching and replacing code block',
|
|
2393
|
+
'insert_at_position': 'โ Inserting content at position',
|
|
1477
2394
|
};
|
|
1478
2395
|
// Tool execution router
|
|
1479
2396
|
async function executeTool(toolName, args) {
|
|
1480
2397
|
// Show user-friendly message with dynamic context
|
|
1481
2398
|
let friendlyMessage = TOOL_MESSAGES[toolName] || `๐ง ${toolName}`;
|
|
1482
2399
|
// Add specific details for certain tools
|
|
1483
|
-
if (toolName === '
|
|
1484
|
-
friendlyMessage =
|
|
2400
|
+
if (toolName === 'upsert_function_tests' && args.function_name) {
|
|
2401
|
+
friendlyMessage = `โ๏ธ Writing/updating test cases for function: ${args.function_name}`;
|
|
1485
2402
|
}
|
|
1486
|
-
else if (toolName === '
|
|
1487
|
-
|
|
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}"`;
|
|
1488
2406
|
}
|
|
1489
|
-
else if (toolName === '
|
|
1490
|
-
friendlyMessage = `โ Inserting
|
|
2407
|
+
else if (toolName === 'insert_at_position' && args.position) {
|
|
2408
|
+
friendlyMessage = `โ Inserting at: ${args.position}`;
|
|
1491
2409
|
}
|
|
1492
|
-
else if (toolName === '
|
|
1493
|
-
friendlyMessage =
|
|
2410
|
+
else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
|
|
2411
|
+
friendlyMessage = `๐ Reading lines ${args.start_line}-${args.end_line}`;
|
|
1494
2412
|
}
|
|
1495
2413
|
console.log(`\n${friendlyMessage}...`);
|
|
1496
2414
|
let result;
|
|
@@ -1499,6 +2417,9 @@ async function executeTool(toolName, args) {
|
|
|
1499
2417
|
case 'read_file':
|
|
1500
2418
|
result = await readFile(args.file_path);
|
|
1501
2419
|
break;
|
|
2420
|
+
case 'read_file_lines':
|
|
2421
|
+
result = await readFileLines(args.file_path, args.start_line, args.end_line);
|
|
2422
|
+
break;
|
|
1502
2423
|
case 'analyze_file_ast':
|
|
1503
2424
|
// Try cache first if indexer is available
|
|
1504
2425
|
if (globalIndexer) {
|
|
@@ -1543,16 +2464,13 @@ async function executeTool(toolName, args) {
|
|
|
1543
2464
|
case 'get_class_methods':
|
|
1544
2465
|
result = getClassMethods(args.file_path, args.class_name);
|
|
1545
2466
|
break;
|
|
2467
|
+
case 'get_file_preamble':
|
|
2468
|
+
result = getFilePreamble(args.file_path);
|
|
2469
|
+
break;
|
|
1546
2470
|
case 'resolve_import_path':
|
|
1547
2471
|
result = resolveImportPath(args.from_file, args.import_path);
|
|
1548
2472
|
break;
|
|
1549
|
-
case '
|
|
1550
|
-
result = await writeTestFile(args.file_path, args.content, args.source_file, args.function_mode);
|
|
1551
|
-
break;
|
|
1552
|
-
case 'edit_test_file':
|
|
1553
|
-
result = await editTestFile(args.file_path, args.old_content, args.new_content);
|
|
1554
|
-
break;
|
|
1555
|
-
case 'replace_function_tests':
|
|
2473
|
+
case 'upsert_function_tests':
|
|
1556
2474
|
result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
|
|
1557
2475
|
break;
|
|
1558
2476
|
case 'run_tests':
|
|
@@ -1570,14 +2488,11 @@ async function executeTool(toolName, args) {
|
|
|
1570
2488
|
case 'report_legitimate_failure':
|
|
1571
2489
|
result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
|
|
1572
2490
|
break;
|
|
1573
|
-
case '
|
|
1574
|
-
result = await
|
|
1575
|
-
break;
|
|
1576
|
-
case 'insert_lines':
|
|
1577
|
-
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);
|
|
1578
2493
|
break;
|
|
1579
|
-
case '
|
|
1580
|
-
result = await
|
|
2494
|
+
case 'insert_at_position':
|
|
2495
|
+
result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
|
|
1581
2496
|
break;
|
|
1582
2497
|
default:
|
|
1583
2498
|
result = { success: false, error: `Unknown tool: ${toolName}` };
|
|
@@ -1588,6 +2503,7 @@ async function executeTool(toolName, args) {
|
|
|
1588
2503
|
}
|
|
1589
2504
|
// Show result with friendly message
|
|
1590
2505
|
if (result.success) {
|
|
2506
|
+
// console.log('result', JSON.stringify(result, null, 2));
|
|
1591
2507
|
console.log(` โ
Done`);
|
|
1592
2508
|
}
|
|
1593
2509
|
else if (result.error) {
|
|
@@ -1834,12 +2750,81 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
|
|
|
1834
2750
|
// Main conversation loop
|
|
1835
2751
|
async function generateTests(sourceFile) {
|
|
1836
2752
|
console.log(`\n๐ Generating tests for: ${sourceFile}\n`);
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
+
|
|
1843
2828
|
Source file: ${sourceFile}
|
|
1844
2829
|
Test file path: ${testFilePath}
|
|
1845
2830
|
|
|
@@ -1848,44 +2833,52 @@ IMPORTANT: You MUST use the provided tools to complete this task. Do not just re
|
|
|
1848
2833
|
Your task (you MUST complete ALL steps):
|
|
1849
2834
|
1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
|
|
1850
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.
|
|
1851
2838
|
2. Use get_imports_ast tool to understand all dependencies
|
|
1852
|
-
3. For each dependency, use find_file to locate
|
|
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
|
|
1853
2840
|
4. For complex functions, use get_function_ast tool to get detailed information
|
|
1854
2841
|
- Returns complete function code WITH JSDoc comments
|
|
1855
2842
|
- Includes calledFunctions and calledMethods lists showing what the function calls
|
|
1856
2843
|
- Use this to fetch related helper functions if needed
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
-
|
|
1862
|
-
-
|
|
1863
|
-
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
-
|
|
1868
|
-
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
-
|
|
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
|
|
1879
2866
|
- DO NOT use ANY placeholder comments like:
|
|
1880
2867
|
* "// Mock setup", "// Assertions", "// Call function"
|
|
1881
2868
|
* "// Further tests...", "// Additional tests..."
|
|
1882
2869
|
* "// Similarly, write tests for..."
|
|
1883
2870
|
* "// Add more tests...", "// TODO", "// ..."
|
|
1884
2871
|
- Write ACTUAL working test code with real mocks, real assertions, real function calls
|
|
1885
|
-
- Every test MUST have:
|
|
2872
|
+
- Every test MUST have [MANDATORY]:
|
|
1886
2873
|
* Real setup code (mock functions, create test data)
|
|
1887
2874
|
* Real execution (call the function being tested)
|
|
1888
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
|
|
1889
2882
|
- Write tests for EVERY exported function (minimum 3-5 tests per function)
|
|
1890
2883
|
- If source has 4 functions, test file MUST have 4 describe blocks with actual tests
|
|
1891
2884
|
- Example of COMPLETE test structure:
|
|
@@ -1894,14 +2887,17 @@ Your task (you MUST complete ALL steps):
|
|
|
1894
2887
|
* Assert: Use expect() to verify results
|
|
1895
2888
|
10. REQUIRED: Run the tests using run_tests tool
|
|
1896
2889
|
11. REQUIRED: If tests fail with import errors:
|
|
1897
|
-
- Use find_file tool to locate the
|
|
2890
|
+
- Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
|
|
1898
2891
|
- Use calculate_relative_path tool to get correct import path
|
|
1899
|
-
- PRIMARY METHOD
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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)
|
|
1905
2901
|
12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
|
|
1906
2902
|
|
|
1907
2903
|
FIXABLE ERRORS (you should fix these):
|
|
@@ -1909,37 +2905,128 @@ Your task (you MUST complete ALL steps):
|
|
|
1909
2905
|
- Missing mocks
|
|
1910
2906
|
- Incorrect mock implementations
|
|
1911
2907
|
- Wrong assertions or test logic
|
|
1912
|
-
- TypeScript compilation errors
|
|
2908
|
+
- TypeScript compilation errors (syntax errors, bracket mismatches)
|
|
1913
2909
|
- Missing test setup/teardown
|
|
1914
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
|
+
})
|
|
1915
2921
|
|
|
1916
2922
|
LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
|
|
1917
2923
|
- Function returns wrong type (e.g., undefined instead of object)
|
|
1918
2924
|
- Missing null/undefined checks in source code
|
|
1919
2925
|
- Logic errors in source code
|
|
1920
2926
|
- Unhandled promise rejections in source code
|
|
1921
|
-
- Source code throws unexpected errors
|
|
1922
2927
|
|
|
1923
2928
|
13. If errors are FIXABLE (AFTER test file is written):
|
|
1924
|
-
- โ
PRIMARY METHOD: Use
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
|
1933
2942
|
- Then retry running tests
|
|
1934
2943
|
14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
|
|
1935
2944
|
- Provide failing test names, reason, and source code issue description
|
|
1936
2945
|
- The test file will be kept as-is with legitimate failing tests
|
|
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.
|
|
1937
2947
|
15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
|
|
1938
2948
|
16. REQUIRED: Ensure all functions are tested in the test file.
|
|
1939
2949
|
17. CRITICAL: config and database modules must be mocked
|
|
1940
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.
|
|
2960
|
+
|
|
1941
2961
|
CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
|
|
1942
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
|
+
|
|
1943
3030
|
START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
1944
3031
|
}
|
|
1945
3032
|
];
|
|
@@ -1978,7 +3065,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
1978
3065
|
// Don't add the excuse to conversation, override with command
|
|
1979
3066
|
messages.push({
|
|
1980
3067
|
role: 'user',
|
|
1981
|
-
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.'
|
|
1982
3069
|
});
|
|
1983
3070
|
continue;
|
|
1984
3071
|
}
|
|
@@ -2001,7 +3088,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2001
3088
|
if (!testFileWritten) {
|
|
2002
3089
|
messages.push({
|
|
2003
3090
|
role: 'user',
|
|
2004
|
-
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.'
|
|
2005
3092
|
});
|
|
2006
3093
|
}
|
|
2007
3094
|
else {
|
|
@@ -2011,11 +3098,13 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2011
3098
|
|
|
2012
3099
|
If tests are failing:
|
|
2013
3100
|
- FIXABLE errors (imports, mocks, assertions):
|
|
2014
|
-
โ
PRIMARY: Use
|
|
2015
|
-
|
|
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.
|
|
2016
3105
|
- LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
|
|
2017
3106
|
|
|
2018
|
-
|
|
3107
|
+
Example: search_replace_block({ search: "old code with context...", replace: "new fixed code..." })`
|
|
2019
3108
|
});
|
|
2020
3109
|
}
|
|
2021
3110
|
continue;
|
|
@@ -2038,81 +3127,41 @@ Start with read_file to see line numbers, then fix specific lines!`
|
|
|
2038
3127
|
console.log(` Recommendation: ${result.recommendation}`);
|
|
2039
3128
|
}
|
|
2040
3129
|
// Track if test file was written
|
|
2041
|
-
if (toolCall.name === '
|
|
3130
|
+
if (toolCall.name === 'upsert_function_tests') {
|
|
2042
3131
|
if (result.success) {
|
|
2043
3132
|
testFileWritten = true;
|
|
2044
|
-
console.log(`\n๐ Test file written: ${
|
|
2045
|
-
if (result.stats) {
|
|
2046
|
-
console.log(` Tests: ${result.stats.tests}, Expectations: ${result.stats.expectations}`);
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
else {
|
|
2050
|
-
// Test file was REJECTED due to validation
|
|
2051
|
-
console.log(`\nโ Test file REJECTED: ${result.error}`);
|
|
2052
|
-
testFileWritten = false; // Make sure we track it wasn't written
|
|
2053
|
-
// Give very specific instructions based on rejection reason
|
|
2054
|
-
if (result.error.includes('placeholder')) {
|
|
2055
|
-
messages.push({
|
|
2056
|
-
role: 'user',
|
|
2057
|
-
content: `Your test file was REJECTED because it contains placeholder comments.
|
|
2058
|
-
|
|
2059
|
-
You MUST rewrite it with COMPLETE code:
|
|
2060
|
-
- Remove ALL comments like "// Further tests", "// Add test", "// Mock setup"
|
|
2061
|
-
- Write the ACTUAL test implementation for EVERY function
|
|
2062
|
-
- Each test needs: real setup, real function call, real expect() assertions
|
|
2063
|
-
|
|
2064
|
-
Try again with write_test_file and provide COMPLETE test implementations!`
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
else if (result.error.includes('NO expect()')) {
|
|
2068
|
-
messages.push({
|
|
2069
|
-
role: 'user',
|
|
2070
|
-
content: `Your test file was REJECTED because tests have no assertions!
|
|
2071
|
-
|
|
2072
|
-
Every test MUST have expect() statements. Example:
|
|
2073
|
-
expect(functionName).toHaveBeenCalled();
|
|
2074
|
-
expect(result).toEqual(expectedValue);
|
|
2075
|
-
|
|
2076
|
-
Rewrite with write_test_file and add actual expect() assertions to ALL tests!`
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
else if (result.error.includes('too few tests')) {
|
|
2080
|
-
messages.push({
|
|
2081
|
-
role: 'user',
|
|
2082
|
-
content: `Your test file was REJECTED because it has too few tests!
|
|
2083
|
-
|
|
2084
|
-
You analyzed ${toolResults.length > 0 ? 'multiple' : 'several'} functions in the source file. Write tests for ALL of them!
|
|
2085
|
-
- Minimum 2-3 test cases per function
|
|
2086
|
-
- Cover: happy path, edge cases, error cases
|
|
2087
|
-
|
|
2088
|
-
Rewrite with write_test_file and include tests for EVERY function!`
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
3133
|
+
console.log(`\n๐ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
2091
3134
|
}
|
|
2092
3135
|
}
|
|
2093
|
-
// Detect
|
|
2094
|
-
if (
|
|
2095
|
-
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}!`);
|
|
2096
3139
|
messages.push({
|
|
2097
3140
|
role: 'user',
|
|
2098
|
-
content:
|
|
3141
|
+
content: `๐จ SYNTAX ERROR DETECTED at line ${result.location.line}:${result.location.column}
|
|
3142
|
+
|
|
3143
|
+
${result.error}
|
|
2099
3144
|
|
|
2100
|
-
|
|
3145
|
+
๐ก ${result.suggestion}
|
|
2101
3146
|
|
|
2102
|
-
|
|
2103
|
-
Step 2: Identify which lines need changes
|
|
2104
|
-
Step 3: Use the appropriate tool:
|
|
2105
|
-
- insert_lines: Add missing imports/mocks (e.g., line 5)
|
|
2106
|
-
- delete_lines: Remove incorrect code (e.g., lines 10-12)
|
|
2107
|
-
- replace_lines: Fix specific sections (e.g., lines 20-25)
|
|
3147
|
+
Your last modification created invalid syntax and was ROLLED BACK automatically.
|
|
2108
3148
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
|
2112
3156
|
|
|
2113
|
-
|
|
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
|
+
})
|
|
2114
3163
|
|
|
2115
|
-
Start
|
|
3164
|
+
Start NOW by reading the file around line ${result.location.line}!`
|
|
2116
3165
|
});
|
|
2117
3166
|
}
|
|
2118
3167
|
// Detect repeated errors (suggests legitimate failure)
|
|
@@ -2123,15 +3172,15 @@ Start with read_file NOW to see line numbers!`
|
|
|
2123
3172
|
sameErrorCount++;
|
|
2124
3173
|
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
2125
3174
|
if (sameErrorCount >= 3) {
|
|
2126
|
-
console.log('\n๐จ Same error repeated 3+ times!
|
|
3175
|
+
console.log('\n๐จ Same error repeated 3+ times! ');
|
|
2127
3176
|
messages.push({
|
|
2128
3177
|
role: 'user',
|
|
2129
3178
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
2130
3179
|
|
|
2131
|
-
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
2132
3180
|
|
|
2133
3181
|
Analyze the error and determine:
|
|
2134
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.
|
|
2135
3184
|
2. Or is this a LEGITIMATE source code bug?
|
|
2136
3185
|
|
|
2137
3186
|
If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
|
|
@@ -2159,20 +3208,23 @@ If FIXABLE: Make one more attempt to fix it.`
|
|
|
2159
3208
|
role: 'user',
|
|
2160
3209
|
content: `Import path error detected! Module not found: "${missingModule}"
|
|
2161
3210
|
|
|
2162
|
-
โ
FIX WITH
|
|
3211
|
+
โ
FIX WITH SEARCH-REPLACE:
|
|
2163
3212
|
|
|
2164
3213
|
Step 1: find_file tool to search for "${filename}" in the repository
|
|
2165
|
-
Step 2: calculate_relative_path tool to get correct import path
|
|
2166
|
-
Step 3: Fix using
|
|
2167
|
-
a)
|
|
2168
|
-
b)
|
|
2169
|
-
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!
|
|
2170
3219
|
|
|
2171
3220
|
Example workflow:
|
|
2172
3221
|
1. find_file({ filename: "${filename}.ts" })
|
|
2173
3222
|
2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
|
|
2174
|
-
3.
|
|
2175
|
-
|
|
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
|
+
})
|
|
2176
3228
|
|
|
2177
3229
|
Start NOW with find_file!`
|
|
2178
3230
|
});
|
|
@@ -2185,28 +3237,25 @@ Start NOW with find_file!`
|
|
|
2185
3237
|
role: 'user',
|
|
2186
3238
|
content: `The test is failing because the source file imports modules that initialize database connections.
|
|
2187
3239
|
|
|
2188
|
-
โ
FIX WITH
|
|
2189
|
-
|
|
2190
|
-
Step 1: read_file to see current test file structure
|
|
2191
|
-
Step 2: insert_lines to add mocks at the TOP of the file (before any imports)
|
|
3240
|
+
โ
FIX WITH SEARCH-REPLACE:
|
|
2192
3241
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
+
})
|
|
2197
3248
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
line_number: 1,
|
|
2204
|
-
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 }"
|
|
2205
3254
|
})
|
|
2206
3255
|
|
|
2207
3256
|
โ ๏ธ Mocks MUST be at the TOP before any imports!
|
|
2208
3257
|
|
|
2209
|
-
Start NOW with
|
|
3258
|
+
Start NOW with insert_at_position!`
|
|
2210
3259
|
});
|
|
2211
3260
|
}
|
|
2212
3261
|
}
|
|
@@ -2374,106 +3423,240 @@ async function generateTestsForFolder() {
|
|
|
2374
3423
|
console.log(`\nโจ Folder processing complete! Processed ${files.length} files.`);
|
|
2375
3424
|
}
|
|
2376
3425
|
// Function-wise test generation
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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) {
|
|
2381
3431
|
const messages = [
|
|
2382
3432
|
{
|
|
2383
3433
|
role: 'user',
|
|
2384
|
-
content: `You are
|
|
3434
|
+
content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
|
|
2385
3435
|
|
|
2386
|
-
|
|
2387
|
-
Test file
|
|
2388
|
-
Test file exists: ${testFileExists}
|
|
2389
|
-
Selected functions to test: ${functionNames.join(', ')}
|
|
3436
|
+
## CONTEXT
|
|
3437
|
+
Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
2390
3438
|
|
|
2391
|
-
|
|
3439
|
+
---
|
|
2392
3440
|
|
|
2393
|
-
|
|
2394
|
-
๐จ CRITICAL WARNING: Test file ALREADY EXISTS at ${testFilePath}! ๐จ
|
|
3441
|
+
## EXECUTION PLAN
|
|
2395
3442
|
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
+
\\\`\\\`\\\`
|
|
2398
3454
|
|
|
2399
|
-
|
|
2400
|
-
|
|
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.
|
|
2401
3461
|
|
|
2402
|
-
|
|
2403
|
-
1. FIRST: Use analyze_file_ast tool to get information about the selected functions: ${functionNames.join(', ')}
|
|
2404
|
-
- This returns metadata about ALL functions in the file without loading full content
|
|
2405
|
-
2. Use get_function_ast tool for each selected function to get detailed information
|
|
2406
|
-
- This returns the complete function code WITH JSDoc comments
|
|
2407
|
-
- It also returns calledFunctions and calledMethods lists showing dependencies
|
|
2408
|
-
- If the function calls other functions from the same file, use get_function_ast again to fetch them
|
|
2409
|
-
3. โ ๏ธ DO NOT use read_file on the source file! It's limited to 5000 lines and wastes tokens. Use get_function_ast instead!
|
|
2410
|
-
4. Use get_imports_ast tool to understand dependencies
|
|
2411
|
-
5. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths
|
|
2412
|
-
6. Generate comprehensive Jest unit tests ONLY for these functions: ${functionNames.join(', ')}
|
|
2413
|
-
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
|
|
2414
|
-
- Mock database modules, models, services, and config modules
|
|
2415
|
-
- Use jest.mock() calls at the TOP of the file before any imports
|
|
2416
|
-
- Test suites for each selected function
|
|
2417
|
-
- Multiple test cases covering:
|
|
2418
|
-
* Happy path scenarios
|
|
2419
|
-
* Edge cases (null, undefined, empty arrays, etc.)
|
|
2420
|
-
* Error conditions
|
|
2421
|
-
* Async behavior (if applicable)
|
|
2422
|
-
- Proper TypeScript types
|
|
2423
|
-
- Clear, descriptive test names
|
|
2424
|
-
- Complete test implementations (NO placeholder comments!)
|
|
2425
|
-
${testFileExists ? `
|
|
2426
|
-
6. ๐จ CRITICAL: Test file EXISTS! Call replace_function_tests tool for EACH function: ${functionNames.join(', ')}
|
|
2427
|
-
- Call replace_function_tests ONCE for each function
|
|
2428
|
-
- Pass the complete describe block as new_test_content parameter
|
|
2429
|
-
- Example: replace_function_tests(test_file_path: "${testFilePath}", function_name: "${functionNames[0]}", new_test_content: "describe('${functionNames[0]}', () => { ... })")
|
|
2430
|
-
- This preserves ALL other existing tests in the file
|
|
2431
|
-
- DO NOT use write_test_file! It will DELETE all other tests!
|
|
2432
|
-
- DO NOT use edit_test_file! Use replace_function_tests instead!` : `
|
|
2433
|
-
6. REQUIRED: Test file does NOT exist. Use write_test_file tool with tests for: ${functionNames.join(', ')} with function_mode set to true.
|
|
2434
|
-
- Create a new test file with complete test implementation
|
|
2435
|
-
- [CRITICAL]: Set function_mode parameter to true if using write_test_file tool`}}
|
|
2436
|
-
7. REQUIRED: Run the tests using run_tests tool
|
|
2437
|
-
- ๐ฏ CRITICAL: Pass function_names parameter with the selected functions: ${JSON.stringify(functionNames)}
|
|
2438
|
-
- Example: run_tests(test_file_path: "${testFilePath}", function_names: ${JSON.stringify(functionNames)})
|
|
2439
|
-
- This runs ONLY tests for the selected functions, ignoring other tests in the file
|
|
2440
|
-
- Other tests in the file may be failing - that's OK, we only care about the functions we're testing
|
|
2441
|
-
8. REQUIRED: If tests fail, analyze if errors are FIXABLE or LEGITIMATE:
|
|
2442
|
-
|
|
2443
|
-
FIXABLE ERRORS (fix these):
|
|
2444
|
-
- Wrong import paths โ use find_file + calculate_relative_path + edit tools
|
|
2445
|
-
- Missing mocks โ add proper jest.mock() calls
|
|
2446
|
-
- Incorrect mock implementations โ update mock return values
|
|
2447
|
-
- Wrong test assertions โ fix expect() statements
|
|
2448
|
-
- TypeScript errors โ fix types and imports
|
|
2449
|
-
|
|
2450
|
-
LEGITIMATE FAILURES (report these):
|
|
2451
|
-
- Function returns wrong type (source code bug)
|
|
2452
|
-
- Missing null checks in source code
|
|
2453
|
-
- Logic errors in source code
|
|
2454
|
-
- Source code throws unexpected errors
|
|
2455
|
-
|
|
2456
|
-
9. If FIXABLE (AFTER test file is written/updated):
|
|
2457
|
-
${testFileExists ? `- โ
PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
|
|
2458
|
-
* read_file to see current test file with line numbers
|
|
2459
|
-
* delete_lines to remove incorrect lines
|
|
2460
|
-
* insert_lines to add missing mocks, imports, or test cases
|
|
2461
|
-
* replace_lines to fix specific sections
|
|
2462
|
-
* This preserves ALL other tests and is more reliable!
|
|
2463
|
-
- โ ๏ธ SECONDARY: replace_function_tests for specific function updates
|
|
2464
|
-
- โ AVOID: write_test_file (will DELETE all other tests!)` : `- Use write_test_file to create the test file
|
|
2465
|
-
- Once written, use line-based tools for fixes (read_file + insert/delete/replace_lines)`}
|
|
2466
|
-
- Then retry tests
|
|
2467
|
-
10. If LEGITIMATE: Call report_legitimate_failure with details and STOP
|
|
2468
|
-
11. REQUIRED: Repeat steps 7-10 until tests pass OR legitimate failures reported
|
|
2469
|
-
|
|
2470
|
-
${testFileExists ? `
|
|
2471
|
-
๐จ REMINDER: The test file EXISTS! Use replace_function_tests, NOT write_test_file! ๐จ
|
|
2472
|
-
` : ''}
|
|
2473
|
-
|
|
2474
|
-
CRITICAL: Fix test bugs but REPORT source code bugs (don't try to make broken code pass)!
|
|
3462
|
+
**Phase 2: Test Generation**
|
|
2475
3463
|
|
|
2476
|
-
|
|
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.`
|
|
2477
3660
|
}
|
|
2478
3661
|
];
|
|
2479
3662
|
let iterations = 0;
|
|
@@ -2484,6 +3667,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2484
3667
|
let lastTestError = '';
|
|
2485
3668
|
let sameErrorCount = 0;
|
|
2486
3669
|
while (iterations < maxIterations) {
|
|
3670
|
+
// console.log('USING CLAUDE PROMPT original 16')
|
|
2487
3671
|
iterations++;
|
|
2488
3672
|
if (iterations === 1) {
|
|
2489
3673
|
console.log(`\n๐ค AI is analyzing selected functions...`);
|
|
@@ -2492,6 +3676,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2492
3676
|
console.log(`\n๐ค AI is still working (step ${iterations})...`);
|
|
2493
3677
|
}
|
|
2494
3678
|
const response = await callAI(messages, TOOLS);
|
|
3679
|
+
// console.log('response from AI', JSON.stringify(response, null, 2));
|
|
2495
3680
|
if (response.content) {
|
|
2496
3681
|
const content = response.content;
|
|
2497
3682
|
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
@@ -2510,7 +3695,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2510
3695
|
console.log('\nโ ๏ธ AI is making excuses! Forcing it to use tools...');
|
|
2511
3696
|
messages.push({
|
|
2512
3697
|
role: 'user',
|
|
2513
|
-
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.'
|
|
2514
3699
|
});
|
|
2515
3700
|
continue;
|
|
2516
3701
|
}
|
|
@@ -2531,42 +3716,35 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
|
2531
3716
|
if (!testFileWritten) {
|
|
2532
3717
|
messages.push({
|
|
2533
3718
|
role: 'user',
|
|
2534
|
-
content:
|
|
2535
|
-
? `๐จ STOP TALKING! The test file EXISTS at ${testFilePath}!
|
|
2536
|
-
|
|
2537
|
-
Call replace_function_tests tool NOW for EACH function: ${functionNames.join(', ')}
|
|
3719
|
+
content: `๐จ STOP TALKING! Use upsert_function_tests tool NOW for: ${functionName}
|
|
2538
3720
|
|
|
2539
3721
|
Example:
|
|
2540
|
-
|
|
3722
|
+
upsert_function_tests({
|
|
2541
3723
|
test_file_path: "${testFilePath}",
|
|
2542
|
-
function_name: "${
|
|
2543
|
-
new_test_content: "describe('${
|
|
3724
|
+
function_name: "${functionName}",
|
|
3725
|
+
new_test_content: "describe('${functionName}', () => { test('should...', () => { ... }) })"
|
|
2544
3726
|
})
|
|
2545
3727
|
|
|
2546
|
-
|
|
2547
|
-
: `Use write_test_file tool NOW with complete test code for: ${functionNames.join(', ')}`
|
|
3728
|
+
This works for both NEW and EXISTING test files!`
|
|
2548
3729
|
});
|
|
2549
3730
|
}
|
|
2550
3731
|
else {
|
|
2551
3732
|
messages.push({
|
|
2552
3733
|
role: 'user',
|
|
2553
|
-
content:
|
|
2554
|
-
? `STOP talking and USE TOOLS!
|
|
3734
|
+
content: `STOP talking and USE TOOLS NOW!
|
|
2555
3735
|
|
|
2556
|
-
โ
PRIMARY
|
|
2557
|
-
1.
|
|
2558
|
-
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
|
|
2559
3741
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
: `STOP talking and USE TOOLS!
|
|
2565
|
-
|
|
2566
|
-
- If test file doesn't exist: write_test_file
|
|
2567
|
-
- 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
|
|
2568
3746
|
|
|
2569
|
-
|
|
3747
|
+
Start NOW with search_replace_block or insert_at_position!`
|
|
2570
3748
|
});
|
|
2571
3749
|
}
|
|
2572
3750
|
continue;
|
|
@@ -2596,15 +3774,14 @@ Act NOW!`
|
|
|
2596
3774
|
sameErrorCount++;
|
|
2597
3775
|
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
2598
3776
|
if (sameErrorCount >= 3) {
|
|
2599
|
-
console.log('\n๐จ Same error repeated 3+ times!
|
|
3777
|
+
console.log('\n๐จ Same error repeated 3+ times! ');
|
|
2600
3778
|
messages.push({
|
|
2601
3779
|
role: 'user',
|
|
2602
3780
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
2603
3781
|
|
|
2604
|
-
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
2605
3782
|
|
|
2606
3783
|
If this is a source code bug: Call report_legitimate_failure tool NOW.
|
|
2607
|
-
If this is still fixable: Make
|
|
3784
|
+
If this is still fixable: Make focused attempt to fix it.`
|
|
2608
3785
|
});
|
|
2609
3786
|
}
|
|
2610
3787
|
}
|
|
@@ -2614,32 +3791,16 @@ If this is still fixable: Make ONE final attempt to fix it.`
|
|
|
2614
3791
|
}
|
|
2615
3792
|
}
|
|
2616
3793
|
// Track if test file was written
|
|
2617
|
-
if (toolCall.name === '
|
|
3794
|
+
if (toolCall.name === 'upsert_function_tests') {
|
|
2618
3795
|
if (result.success) {
|
|
2619
3796
|
testFileWritten = true;
|
|
2620
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
|
+
});
|
|
2621
3802
|
}
|
|
2622
3803
|
}
|
|
2623
|
-
// Detect if AI incorrectly used write_test_file when file exists
|
|
2624
|
-
if (toolCall.name === 'write_test_file' && testFileExists) {
|
|
2625
|
-
console.log('\nโ ๏ธ WARNING: AI used write_test_file on existing file! This overwrites all other tests!');
|
|
2626
|
-
messages.push({
|
|
2627
|
-
role: 'user',
|
|
2628
|
-
content: `โ CRITICAL ERROR: You used write_test_file but the test file ALREADY EXISTS!
|
|
2629
|
-
|
|
2630
|
-
This OVERWROTE the entire file and DELETED all other tests! This is WRONG!
|
|
2631
|
-
|
|
2632
|
-
You MUST use replace_function_tests instead. For future fixes, call it for EACH function:
|
|
2633
|
-
|
|
2634
|
-
${functionNames.map(fname => `replace_function_tests({
|
|
2635
|
-
test_file_path: "${testFilePath}",
|
|
2636
|
-
function_name: "${fname}",
|
|
2637
|
-
new_test_content: "describe('${fname}', () => { /* your tests */ })"
|
|
2638
|
-
})`).join('\n\n')}
|
|
2639
|
-
|
|
2640
|
-
DO NOT use write_test_file when the file exists! Always use replace_function_tests!`
|
|
2641
|
-
});
|
|
2642
|
-
}
|
|
2643
3804
|
}
|
|
2644
3805
|
// Add tool results to conversation based on provider
|
|
2645
3806
|
if (CONFIG.aiProvider === 'claude') {
|
|
@@ -2719,6 +3880,224 @@ DO NOT use write_test_file when the file exists! Always use replace_function_tes
|
|
|
2719
3880
|
console.log('\n๐ Test file updated with legitimate failures documented.');
|
|
2720
3881
|
console.log(' These failures indicate bugs in the source code that need to be fixed.');
|
|
2721
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);
|
|
2722
4101
|
return testFilePath;
|
|
2723
4102
|
}
|
|
2724
4103
|
async function generateTestsForFunction() {
|
|
@@ -2751,7 +4130,7 @@ async function generateTestsForFunction() {
|
|
|
2751
4130
|
console.log('No exported functions found in the file!');
|
|
2752
4131
|
return;
|
|
2753
4132
|
}
|
|
2754
|
-
console.log(`\nFound ${functions.length} exported functions:\n`);
|
|
4133
|
+
// console.log(`\nFound ${functions.length} exported functions:\n`);
|
|
2755
4134
|
functions.forEach((func, index) => {
|
|
2756
4135
|
console.log(`${index + 1}. ${func.name} (${func.type}, ${func.async ? 'async' : 'sync'})`);
|
|
2757
4136
|
});
|
|
@@ -2768,8 +4147,233 @@ async function generateTestsForFunction() {
|
|
|
2768
4147
|
await generateTestsForFunctions(selectedFile, selectedFunctions);
|
|
2769
4148
|
console.log('\nโจ Done!');
|
|
2770
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: any) => 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
|
+
const { fullDiff, files } = await getGitDiff();
|
|
4311
|
+
if (files.length === 0) {
|
|
4312
|
+
console.log('โ
No changes detected in source files.');
|
|
4313
|
+
console.log(' (Only staged and unstaged changes are checked)');
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
console.log(`๐ Found changes in ${files.length} file(s)\n`);
|
|
4317
|
+
let totalFunctions = 0;
|
|
4318
|
+
let processedFiles = 0;
|
|
4319
|
+
let errorFiles = 0;
|
|
4320
|
+
// Process each changed file
|
|
4321
|
+
for (const fileInfo of files) {
|
|
4322
|
+
const { filePath, diff } = fileInfo;
|
|
4323
|
+
// Check if file exists
|
|
4324
|
+
if (!fsSync.existsSync(filePath)) {
|
|
4325
|
+
console.log(`โญ๏ธ Skipping ${filePath} (file not found)`);
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
console.log(`\n๐ Processing: ${filePath}`);
|
|
4329
|
+
console.log(` ๐ค Analyzing diff with AI...`);
|
|
4330
|
+
// Use AI to extract changed function names from diff
|
|
4331
|
+
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4332
|
+
// console.log('Changed functions are', changedFunctions);
|
|
4333
|
+
if (changedFunctions.length === 0) {
|
|
4334
|
+
console.log(` โญ๏ธ No exported functions changed`);
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
console.log(` ๐ฆ Changed functions: ${changedFunctions.join(', ')}`);
|
|
4338
|
+
try {
|
|
4339
|
+
// Use existing generateTestsForFunctions
|
|
4340
|
+
await generateTestsForFunctions(filePath, changedFunctions);
|
|
4341
|
+
processedFiles++;
|
|
4342
|
+
totalFunctions += changedFunctions.length;
|
|
4343
|
+
console.log(` โ
Tests generated successfully`);
|
|
4344
|
+
}
|
|
4345
|
+
catch (error) {
|
|
4346
|
+
errorFiles++;
|
|
4347
|
+
console.error(` โ Error generating tests: ${error.message}`);
|
|
4348
|
+
// Continue with next file
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
// Summary
|
|
4352
|
+
console.log('\n' + '='.repeat(60));
|
|
4353
|
+
console.log('๐ Auto-Generation Summary');
|
|
4354
|
+
console.log('='.repeat(60));
|
|
4355
|
+
console.log(`โ
Successfully processed: ${processedFiles} file(s)`);
|
|
4356
|
+
console.log(`๐ Functions tested: ${totalFunctions}`);
|
|
4357
|
+
if (errorFiles > 0) {
|
|
4358
|
+
console.log(`โ ๏ธ Failed: ${errorFiles} file(s)`);
|
|
4359
|
+
}
|
|
4360
|
+
console.log('='.repeat(60));
|
|
4361
|
+
console.log('\nโจ Done!');
|
|
4362
|
+
}
|
|
4363
|
+
catch (error) {
|
|
4364
|
+
if (error.message === 'Not a git repository') {
|
|
4365
|
+
console.error('โ Error: Not a git repository');
|
|
4366
|
+
console.error(' Auto mode requires git to detect changes.');
|
|
4367
|
+
process.exit(1);
|
|
4368
|
+
}
|
|
4369
|
+
throw error;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
2771
4372
|
async function main() {
|
|
2772
4373
|
console.log('๐งช AI-Powered Unit Test Generator with AST Analysis\n');
|
|
4374
|
+
// Check for auto mode from CLI arguments EARLY (before any prompts)
|
|
4375
|
+
const args = process.argv.slice(2);
|
|
4376
|
+
const isAutoMode = args.includes('auto');
|
|
2773
4377
|
// Load configuration from codeguard.json
|
|
2774
4378
|
try {
|
|
2775
4379
|
CONFIG = (0, config_1.loadConfig)();
|
|
@@ -2792,6 +4396,22 @@ async function main() {
|
|
|
2792
4396
|
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
2793
4397
|
process.exit(1);
|
|
2794
4398
|
}
|
|
4399
|
+
// If auto mode, skip indexing setup and proceed directly
|
|
4400
|
+
if (isAutoMode) {
|
|
4401
|
+
console.log('๐ค Auto Mode: Detecting changes via git diff\n');
|
|
4402
|
+
console.log(`โ
Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
4403
|
+
// Initialize indexer if it exists, but don't prompt
|
|
4404
|
+
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
4405
|
+
const hasExistingIndex = globalIndexer.hasIndex();
|
|
4406
|
+
if (hasExistingIndex) {
|
|
4407
|
+
await globalIndexer.loadIndex();
|
|
4408
|
+
}
|
|
4409
|
+
else {
|
|
4410
|
+
globalIndexer = null;
|
|
4411
|
+
}
|
|
4412
|
+
await autoGenerateTests();
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
2795
4415
|
// Optional: Codebase Indexing
|
|
2796
4416
|
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
2797
4417
|
const hasExistingIndex = globalIndexer.hasIndex();
|