codeguard-testgen 1.0.6 → 1.0.8

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