codeguard-testgen 1.0.7 โ†’ 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)
@@ -92,6 +97,28 @@ const TOOLS = [
92
97
  required: ['file_path']
93
98
  }
94
99
  },
100
+ {
101
+ name: 'read_file_lines',
102
+ description: 'Read a specific portion of a file by line numbers. Useful for examining syntax errors, bracket mismatches, or specific sections that need fixing. Returns lines with line numbers for easy reference.',
103
+ input_schema: {
104
+ type: 'object',
105
+ properties: {
106
+ file_path: {
107
+ type: 'string',
108
+ description: 'The relative path to the file from the repository root'
109
+ },
110
+ start_line: {
111
+ type: 'number',
112
+ description: 'The starting line number (1-indexed)'
113
+ },
114
+ end_line: {
115
+ type: 'number',
116
+ description: 'The ending line number (1-indexed)'
117
+ }
118
+ },
119
+ required: ['file_path', 'start_line', 'end_line']
120
+ }
121
+ },
95
122
  {
96
123
  name: 'analyze_file_ast',
97
124
  description: 'Parse file using AST and extract detailed information about all functions, classes, types, and exports',
@@ -100,7 +127,7 @@ const TOOLS = [
100
127
  properties: {
101
128
  file_path: {
102
129
  type: 'string',
103
- description: 'The relative path to the source file'
130
+ description: 'The relative path to the source file.'
104
131
  }
105
132
  },
106
133
  required: ['file_path']
@@ -118,7 +145,7 @@ const TOOLS = [
118
145
  },
119
146
  function_name: {
120
147
  type: 'string',
121
- description: 'The name of the function to analyze'
148
+ description: 'The name of the function to analyze, this tool must be used for functions information'
122
149
  }
123
150
  },
124
151
  required: ['file_path', 'function_name']
@@ -152,6 +179,20 @@ const TOOLS = [
152
179
  required: ['file_path']
153
180
  }
154
181
  },
182
+ {
183
+ name: 'get_file_preamble',
184
+ description: 'Extract top-level code (imports, jest.mocks, setup blocks, module-level variables) from a file. Captures complete multi-line statements. Perfect for understanding large test files without reading entire content.',
185
+ input_schema: {
186
+ type: 'object',
187
+ properties: {
188
+ file_path: {
189
+ type: 'string',
190
+ description: 'The relative path to the file'
191
+ }
192
+ },
193
+ required: ['file_path']
194
+ }
195
+ },
155
196
  {
156
197
  name: 'resolve_import_path',
157
198
  description: 'Resolve a relative import path to an absolute path',
@@ -189,56 +230,8 @@ const TOOLS = [
189
230
  }
190
231
  },
191
232
  {
192
- name: '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',
@@ -351,8 +344,8 @@ const TOOLS = [
351
344
  }
352
345
  },
353
346
  {
354
- name: 'delete_lines',
355
- 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.',
356
349
  input_schema: {
357
350
  type: 'object',
358
351
  properties: {
@@ -360,66 +353,50 @@ const TOOLS = [
360
353
  type: 'string',
361
354
  description: 'The path to the file to edit'
362
355
  },
363
- start_line: {
364
- type: 'number',
365
- description: 'The starting line number to delete (1-indexed, inclusive)'
366
- },
367
- end_line: {
368
- type: 'number',
369
- description: 'The ending line number to delete (1-indexed, inclusive)'
370
- }
371
- },
372
- required: ['file_path', 'start_line', 'end_line']
373
- }
374
- },
375
- {
376
- name: 'insert_lines',
377
- description: 'Insert new lines at a specific position in a file. Useful for adding missing imports, mocks, or test cases. Use this after reading the file to know exact line numbers.',
378
- input_schema: {
379
- type: 'object',
380
- properties: {
381
- file_path: {
356
+ search: {
382
357
  type: 'string',
383
- description: 'The 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.'
384
359
  },
385
- line_number: {
386
- type: 'number',
387
- 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.'
388
363
  },
389
- content: {
364
+ match_mode: {
390
365
  type: 'string',
391
- 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.'
392
368
  }
393
369
  },
394
- required: ['file_path', 'line_number', 'content']
370
+ required: ['file_path', 'search', 'replace']
395
371
  }
396
372
  },
397
373
  {
398
- name: 'replace_lines',
399
- 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.',
400
376
  input_schema: {
401
377
  type: 'object',
402
378
  properties: {
403
379
  file_path: {
404
380
  type: 'string',
405
- description: 'The path to the file to edit'
381
+ description: 'The path to the file'
406
382
  },
407
- start_line: {
408
- type: 'number',
409
- description: 'The starting line number to replace (1-indexed, inclusive)'
383
+ content: {
384
+ type: 'string',
385
+ description: 'The content to insert'
410
386
  },
411
- end_line: {
412
- type: 'number',
413
- 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)'
414
391
  },
415
- new_content: {
392
+ after_marker: {
416
393
  type: 'string',
417
- 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)'
418
395
  }
419
396
  },
420
- required: ['file_path', 'start_line', 'end_line', 'new_content']
397
+ required: ['file_path', 'content']
421
398
  }
422
- }
399
+ },
423
400
  ];
424
401
  exports.TOOLS = TOOLS;
425
402
  // AST Parsing utilities
@@ -640,41 +617,86 @@ function getTypeAnnotation(typeNode) {
640
617
  return 'unknown';
641
618
  }
642
619
  function getFunctionAST(filePath, functionName) {
643
- try {
644
- const content = fsSync.readFileSync(filePath, 'utf-8');
645
- const ast = parseFileToAST(filePath, content);
646
- const lines = content.split('\n');
647
- let functionInfo = null;
648
- traverse(ast, {
649
- FunctionDeclaration(path) {
650
- if (path.node.id?.name === functionName) {
651
- functionInfo = extractFunctionDetails(path, lines);
652
- path.stop();
653
- }
654
- },
655
- VariableDeclarator(path) {
656
- if (path.node.id.name === functionName &&
657
- (path.node.init?.type === 'ArrowFunctionExpression' ||
658
- path.node.init?.type === 'FunctionExpression')) {
659
- functionInfo = extractFunctionDetails(path, lines, true);
660
- path.stop();
661
- }
662
- },
663
- ClassMethod(path) {
664
- if (path.node.key.name === functionName) {
665
- functionInfo = extractFunctionDetails(path, lines);
666
- 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
+ }
667
647
  }
648
+ });
649
+ // console.log(` โœ… Found functionInfo: ${JSON.stringify(functionInfo)}`);
650
+ // console.log(` โœ… Found targetPath: ${JSON.stringify(targetPath)}`);
651
+ if (functionInfo) {
652
+ return { success: true, ...functionInfo, filePath: targetPath };
668
653
  }
669
- });
670
- if (!functionInfo) {
671
- 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;
672
665
  }
673
- return Object.assign({ success: true }, functionInfo);
674
666
  }
675
667
  catch (error) {
676
- 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
+ };
677
694
  }
695
+ // No indexer available and not found in provided path
696
+ return {
697
+ success: false,
698
+ error: `Function ${functionName} not found in ${filePath}. Tip: Enable indexing for better function lookup across files.`
699
+ };
678
700
  }
679
701
  function extractFunctionDetails(path, lines, isVariable = false) {
680
702
  const node = isVariable ? path.node.init : path.node;
@@ -728,6 +750,7 @@ function extractFunctionDetails(path, lines, isVariable = false) {
728
750
  // If traversal fails, just continue without called functions
729
751
  console.warn('Could not extract called functions:', error);
730
752
  }
753
+ // console.log(` โœ… Found code: ${code}`);
731
754
  return {
732
755
  name: isVariable ? path.node.id.name : node.id?.name,
733
756
  code,
@@ -744,6 +767,7 @@ function extractFunctionDetails(path, lines, isVariable = false) {
744
767
  // Removed estimateComplexity to avoid scope/parentPath traversal issues
745
768
  // Complexity is now hardcoded to 1 in extractFunctionDetails
746
769
  function getImportsAST(filePath) {
770
+ // Try to read the file
747
771
  try {
748
772
  const content = fsSync.readFileSync(filePath, 'utf-8');
749
773
  const ast = parseFileToAST(filePath, content);
@@ -791,9 +815,55 @@ function getImportsAST(filePath) {
791
815
  }
792
816
  }
793
817
  });
794
- return { success: true, imports };
818
+ return { success: true, imports, filePath };
795
819
  }
796
820
  catch (error) {
821
+ // File not found - try to find similar file in index
822
+ if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
823
+ console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
824
+ const indexData = globalIndexer.index;
825
+ if (indexData && indexData.files) {
826
+ const searchName = path.basename(filePath);
827
+ const searchDir = path.dirname(filePath);
828
+ const matches = [];
829
+ // Find files with matching basename
830
+ for (const indexedFilePath of Object.keys(indexData.files)) {
831
+ if (path.basename(indexedFilePath) === searchName) {
832
+ matches.push(indexedFilePath);
833
+ }
834
+ }
835
+ if (matches.length > 0) {
836
+ let suggestion = matches[0];
837
+ // If multiple matches, try to find the best one by matching directory structure
838
+ if (matches.length > 1) {
839
+ const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
840
+ // Score each match by how many path components match
841
+ const scored = matches.map(m => {
842
+ const matchParts = path.dirname(m).split(path.sep);
843
+ let score = 0;
844
+ for (let i = 0; i < searchParts.length; i++) {
845
+ const searchPart = searchParts[searchParts.length - 1 - i];
846
+ const matchPart = matchParts[matchParts.length - 1 - i];
847
+ if (searchPart === matchPart) {
848
+ score += (i + 1); // Weight recent parts higher
849
+ }
850
+ }
851
+ return { path: m, score };
852
+ });
853
+ // Sort by score (highest first)
854
+ scored.sort((a, b) => b.score - a.score);
855
+ suggestion = scored[0].path;
856
+ }
857
+ console.log(` ๐Ÿ’ก Did you mean: ${suggestion}?`);
858
+ return {
859
+ success: false,
860
+ error: `File not found: ${filePath}`,
861
+ suggestion: suggestion,
862
+ allMatches: matches.length > 1 ? matches : undefined
863
+ };
864
+ }
865
+ }
866
+ }
797
867
  return { success: false, error: error.message };
798
868
  }
799
869
  }
@@ -855,118 +925,703 @@ function getTypeDefinitions(filePath) {
855
925
  });
856
926
  }
857
927
  });
858
- return { success: true, types };
928
+ return { success: true, types, filePath };
859
929
  }
860
930
  catch (error) {
931
+ // File not found - try to find similar file in index
932
+ if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
933
+ console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
934
+ const indexData = globalIndexer.index;
935
+ if (indexData && indexData.files) {
936
+ const searchName = path.basename(filePath);
937
+ const searchDir = path.dirname(filePath);
938
+ const matches = [];
939
+ // Find files with matching basename
940
+ for (const indexedFilePath of Object.keys(indexData.files)) {
941
+ if (path.basename(indexedFilePath) === searchName) {
942
+ matches.push(indexedFilePath);
943
+ }
944
+ }
945
+ if (matches.length > 0) {
946
+ let suggestion = matches[0];
947
+ // If multiple matches, try to find the best one by matching directory structure
948
+ if (matches.length > 1) {
949
+ const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
950
+ // Score each match by how many path components match
951
+ const scored = matches.map(m => {
952
+ const matchParts = path.dirname(m).split(path.sep);
953
+ let score = 0;
954
+ for (let i = 0; i < searchParts.length; i++) {
955
+ const searchPart = searchParts[searchParts.length - 1 - i];
956
+ const matchPart = matchParts[matchParts.length - 1 - i];
957
+ if (searchPart === matchPart) {
958
+ score += (i + 1); // Weight recent parts higher
959
+ }
960
+ }
961
+ return { path: m, score };
962
+ });
963
+ // Sort by score (highest first)
964
+ scored.sort((a, b) => b.score - a.score);
965
+ suggestion = scored[0].path;
966
+ }
967
+ console.log(` ๐Ÿ’ก Did you mean: ${suggestion}?`);
968
+ return {
969
+ success: false,
970
+ error: `File not found: ${filePath}`,
971
+ suggestion: suggestion,
972
+ allMatches: matches.length > 1 ? matches : undefined
973
+ };
974
+ }
975
+ }
976
+ }
861
977
  return { success: false, error: error.message };
862
978
  }
863
979
  }
864
- function getClassMethods(filePath, className) {
865
- try {
866
- const content = fsSync.readFileSync(filePath, 'utf-8');
867
- const ast = parseFileToAST(filePath, content);
868
- const lines = content.split('\n');
869
- let classInfo = null;
870
- traverse(ast, {
871
- ClassDeclaration(path) {
872
- if (path.node.id?.name === className) {
873
- const methods = [];
874
- if (path.node.body && path.node.body.body) {
875
- path.node.body.body.forEach(member => {
876
- if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
877
- const startLine = member.loc.start.line - 1;
878
- const endLine = member.loc.end.line - 1;
879
- methods.push({
880
- name: member.key.name,
881
- kind: member.kind,
882
- static: member.static,
883
- async: member.async,
884
- params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
885
- returnType: member.returnType ? getTypeAnnotation(member.returnType) : null,
886
- code: lines.slice(startLine, endLine + 1).join('\n'),
887
- 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'
888
1064
  });
889
1065
  }
890
1066
  });
891
1067
  }
892
- classInfo = {
893
- name: className,
894
- methods,
895
- superClass: path.node.superClass?.name
896
- };
897
- path.stop();
898
1068
  }
899
1069
  }
900
- });
901
- if (!classInfo) {
902
- 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
+ };
903
1109
  }
904
- return { success: true, ...classInfo };
905
- }
906
- catch (error) {
907
- return { success: false, error: error.message };
1110
+ catch (error) {
1111
+ return null;
1112
+ }
1113
+ };
1114
+ // Try the provided file path first
1115
+ const result = extractPreamble(filePath);
1116
+ if (result) {
1117
+ return result;
908
1118
  }
909
- }
910
- // Other tool implementations
911
- async function readFile(filePath) {
912
- try {
913
- const content = await fs.readFile(filePath, 'utf-8');
914
- const lines = content.split('\n');
915
- // Enforce line limit to prevent token bloat on large files
916
- const MAX_LINES = 5000;
917
- if (lines.length > MAX_LINES) {
1119
+ // Fallback: If we have an indexer, search for file with matching path
1120
+ if (globalIndexer) {
1121
+ console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1122
+ const indexData = globalIndexer.index;
1123
+ if (indexData && indexData.files) {
1124
+ const searchName = path.basename(filePath);
1125
+ const searchDir = path.dirname(filePath);
1126
+ const matches = [];
1127
+ // Find files with matching basename
1128
+ for (const indexedFilePath of Object.keys(indexData.files)) {
1129
+ if (path.basename(indexedFilePath) === searchName) {
1130
+ matches.push(indexedFilePath);
1131
+ }
1132
+ }
1133
+ if (matches.length > 0) {
1134
+ let correctPath = matches[0];
1135
+ // If multiple matches, try to find the best one by matching directory structure
1136
+ if (matches.length > 1) {
1137
+ const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
1138
+ // Score each match by how many path components match
1139
+ const scored = matches.map(m => {
1140
+ const matchParts = path.dirname(m).split(path.sep);
1141
+ let score = 0;
1142
+ for (let i = 0; i < searchParts.length; i++) {
1143
+ const searchPart = searchParts[searchParts.length - 1 - i];
1144
+ const matchPart = matchParts[matchParts.length - 1 - i];
1145
+ if (searchPart === matchPart) {
1146
+ score += (i + 1); // Weight recent parts higher
1147
+ }
1148
+ }
1149
+ return { path: m, score };
1150
+ });
1151
+ // Sort by score (highest first)
1152
+ scored.sort((a, b) => b.score - a.score);
1153
+ correctPath = scored[0].path;
1154
+ console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1155
+ }
1156
+ else {
1157
+ console.log(` โœ… Found file at ${correctPath}`);
1158
+ }
1159
+ const retryResult = extractPreamble(correctPath);
1160
+ if (retryResult) {
1161
+ return retryResult;
1162
+ }
1163
+ }
918
1164
  return {
919
1165
  success: false,
920
- error: `File too large (${lines.length} lines, max ${MAX_LINES}). Use analyze_file_ast to see structure, then get_function_ast for specific functions instead.`,
921
- fileSize: lines.length,
922
- suggestion: 'For large source files, use: 1) analyze_file_ast to see all functions, 2) get_function_ast for specific functions you need'
1166
+ error: `File not found: ${filePath}`,
1167
+ suggestion: matches.length > 0 ? matches[0] : undefined,
1168
+ allMatches: matches.length > 1 ? matches : undefined
923
1169
  };
924
1170
  }
925
- return { success: true, content };
926
- }
927
- catch (error) {
928
- return { success: false, error: error.message };
929
1171
  }
1172
+ // No indexer available
1173
+ return {
1174
+ success: false,
1175
+ error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
1176
+ };
930
1177
  }
931
- function resolveImportPath(fromFile, importPath) {
932
- try {
933
- if (importPath.startsWith('.')) {
934
- const dir = path.dirname(fromFile);
935
- const resolved = path.resolve(dir, importPath);
936
- const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
937
- for (const ext of extensions) {
938
- const withExt = resolved + ext;
939
- if (fsSync.existsSync(withExt)) {
940
- return { success: true, resolvedPath: withExt };
1178
+ function getClassMethods(filePath, className) {
1179
+ // Helper to search for class in a file
1180
+ const searchInFile = (targetPath) => {
1181
+ try {
1182
+ const content = fsSync.readFileSync(targetPath, 'utf-8');
1183
+ const ast = parseFileToAST(targetPath, content);
1184
+ const lines = content.split('\n');
1185
+ let classInfo = null;
1186
+ traverse(ast, {
1187
+ ClassDeclaration(path) {
1188
+ if (path.node.id?.name === className) {
1189
+ const methods = [];
1190
+ if (path.node.body && path.node.body.body) {
1191
+ path.node.body.body.forEach(member => {
1192
+ if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
1193
+ const startLine = member.loc.start.line - 1;
1194
+ const endLine = member.loc.end.line - 1;
1195
+ methods.push({
1196
+ name: member.key.name,
1197
+ kind: member.kind,
1198
+ static: member.static,
1199
+ async: member.async,
1200
+ params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
1201
+ returnType: member.returnType ? getTypeAnnotation(member.returnType) : null,
1202
+ code: lines.slice(startLine, endLine + 1).join('\n'),
1203
+ line: startLine + 1
1204
+ });
1205
+ }
1206
+ });
1207
+ }
1208
+ classInfo = {
1209
+ name: className,
1210
+ methods,
1211
+ superClass: path.node.superClass?.name,
1212
+ filePath: targetPath
1213
+ };
1214
+ path.stop();
1215
+ }
941
1216
  }
1217
+ });
1218
+ if (classInfo) {
1219
+ return { success: true, ...classInfo };
942
1220
  }
943
- if (fsSync.existsSync(resolved)) {
944
- return { success: true, resolvedPath: resolved };
1221
+ return null;
1222
+ }
1223
+ catch (error) {
1224
+ return null;
1225
+ }
1226
+ };
1227
+ // Try the provided file path first
1228
+ try {
1229
+ const result = searchInFile(filePath);
1230
+ if (result) {
1231
+ return result;
1232
+ }
1233
+ }
1234
+ catch (error) {
1235
+ // File not found or other error - continue to fallback
1236
+ }
1237
+ // Fallback: If we have an indexer, search for the class in the index
1238
+ if (globalIndexer) {
1239
+ console.log(` ๐Ÿ” Class not found in ${filePath}, searching index...`);
1240
+ // Search through all indexed files
1241
+ const indexData = globalIndexer.index;
1242
+ if (indexData && indexData.files) {
1243
+ for (const [indexedFilePath, fileData] of Object.entries(indexData.files)) {
1244
+ const analysis = fileData.analysis;
1245
+ // Check if this file contains the class
1246
+ const hasClass = analysis.classes?.some((cls) => cls.name === className);
1247
+ if (hasClass) {
1248
+ console.log(` โœ… Found ${className} in ${indexedFilePath}`);
1249
+ const result = searchInFile(indexedFilePath);
1250
+ if (result) {
1251
+ return result;
1252
+ }
1253
+ }
945
1254
  }
946
1255
  }
947
1256
  return {
948
- success: true,
949
- resolvedPath: importPath,
950
- isExternal: true
1257
+ success: false,
1258
+ error: `Class ${className} not found in ${filePath} or any indexed files`
951
1259
  };
952
1260
  }
953
- catch (error) {
954
- return { success: false, error: error.message };
955
- }
1261
+ // No indexer available and not found in provided path
1262
+ return {
1263
+ success: false,
1264
+ error: `Class ${className} not found in ${filePath}. Tip: Enable indexing for better class lookup across files.`
1265
+ };
956
1266
  }
957
- async function writeTestFile(filePath, content, sourceFilePath, functionMode = false) {
958
- try {
959
- // VALIDATE: Check for incomplete/placeholder tests BEFORE writing
960
- const invalidPatterns = [
961
- /\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
962
- /\/\/\s*(Add more|write more|Similarly|write tests for)/i,
963
- /\/\/\s*TODO/i,
964
- /\/\/\s*\.\.\./,
965
- /\/\/.*etc\./i,
966
- /expect\(\).*\/\//, // expect() followed by comment
967
- ];
968
- const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
969
- if (hasPlaceholders) {
1267
+ // Other tool implementations
1268
+ async function readFile(filePath) {
1269
+ const MAX_LINES = 2000;
1270
+ // Helper to read and validate file
1271
+ const tryReadFile = async (targetPath) => {
1272
+ try {
1273
+ const content = await fs.readFile(targetPath, 'utf-8');
1274
+ const lines = content.split('\n');
1275
+ if (lines.length > MAX_LINES) {
1276
+ // Check if this is a test file
1277
+ const isTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(targetPath);
1278
+ // File is too large - check if we have cached analysis
1279
+ if (globalIndexer) {
1280
+ const cached = globalIndexer.getFileAnalysis(targetPath);
1281
+ if (cached) {
1282
+ console.log(` ๐Ÿ“ฆ File too large (${lines.length} lines), returning cached analysis instead`);
1283
+ // For test files, also include preamble automatically
1284
+ let preamble = undefined;
1285
+ if (isTestFile) {
1286
+ const preambleResult = getFilePreamble(targetPath);
1287
+ if (preambleResult.success) {
1288
+ preamble = preambleResult;
1289
+ console.log(` ๐Ÿ“‹ Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
1290
+ }
1291
+ }
1292
+ return {
1293
+ success: true,
1294
+ usedCache: true,
1295
+ fileSize: lines.length,
1296
+ filePath: targetPath,
1297
+ analysis: cached,
1298
+ preamble: preamble,
1299
+ message: isTestFile
1300
+ ? `Test file has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/mocks/setup). Use get_function_ast with proper file path for specific test functions.`
1301
+ : `File has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/setup). Use read_file_lines to read specific section of file or get_function_ast with proper file path for specific functions.`
1302
+ };
1303
+ }
1304
+ }
1305
+ // No cache available
1306
+ return {
1307
+ success: false,
1308
+ error: `File too large (${lines.length} lines, max ${MAX_LINES}). Use get_file_preamble for imports/mocks, analyze_file_ast for structure, or get_function_ast for specific functions or read_file_lines to read specific section of file.`,
1309
+ fileSize: lines.length,
1310
+ filePath: targetPath,
1311
+ suggestion: 'For large files: 1) get_file_preamble for imports/mocks/setup, 2) analyze_file_ast for all functions, 3) get_function_ast for specific functions'
1312
+ };
1313
+ }
1314
+ return { success: true, content, filePath: targetPath, fileSize: lines.length };
1315
+ }
1316
+ catch (error) {
1317
+ return null; // Signal to try fallback
1318
+ }
1319
+ };
1320
+ // Try the provided file path first
1321
+ const result = await tryReadFile(filePath);
1322
+ if (result) {
1323
+ return result;
1324
+ }
1325
+ // Fallback: If we have an indexer, search for file with matching path
1326
+ if (globalIndexer) {
1327
+ console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1328
+ const indexData = globalIndexer.index;
1329
+ if (indexData && indexData.files) {
1330
+ const searchName = path.basename(filePath);
1331
+ const searchDir = path.dirname(filePath);
1332
+ const matches = [];
1333
+ // Find files with matching basename
1334
+ for (const indexedFilePath of Object.keys(indexData.files)) {
1335
+ if (path.basename(indexedFilePath) === searchName) {
1336
+ matches.push(indexedFilePath);
1337
+ }
1338
+ }
1339
+ if (matches.length > 0) {
1340
+ let correctPath = matches[0];
1341
+ // If multiple matches, try to find the best one by matching directory structure
1342
+ if (matches.length > 1) {
1343
+ const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
1344
+ // Score each match by how many path components match
1345
+ const scored = matches.map(m => {
1346
+ const matchParts = path.dirname(m).split(path.sep);
1347
+ let score = 0;
1348
+ for (let i = 0; i < searchParts.length; i++) {
1349
+ const searchPart = searchParts[searchParts.length - 1 - i];
1350
+ const matchPart = matchParts[matchParts.length - 1 - i];
1351
+ if (searchPart === matchPart) {
1352
+ score += (i + 1); // Weight recent parts higher
1353
+ }
1354
+ }
1355
+ return { path: m, score };
1356
+ });
1357
+ // Sort by score (highest first)
1358
+ scored.sort((a, b) => b.score - a.score);
1359
+ correctPath = scored[0].path;
1360
+ console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1361
+ }
1362
+ else {
1363
+ console.log(` โœ… Found file at ${correctPath}`);
1364
+ }
1365
+ // Try to read from correct path
1366
+ const retryResult = await tryReadFile(correctPath);
1367
+ if (retryResult) {
1368
+ return retryResult;
1369
+ }
1370
+ // If still failed, at least return the correct path
1371
+ return {
1372
+ success: false,
1373
+ error: `Found file at ${correctPath} but failed to read it`,
1374
+ suggestion: correctPath,
1375
+ allMatches: matches.length > 1 ? matches : undefined
1376
+ };
1377
+ }
1378
+ }
1379
+ return {
1380
+ success: false,
1381
+ error: `File not found: ${filePath}. No matching files found in index.`
1382
+ };
1383
+ }
1384
+ // No indexer available
1385
+ return {
1386
+ success: false,
1387
+ error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
1388
+ };
1389
+ }
1390
+ /**
1391
+ * Read specific lines from a file
1392
+ * Useful for examining syntax errors, bracket mismatches, or specific sections
1393
+ */
1394
+ async function readFileLines(filePath, startLine, endLine) {
1395
+ // Validate line numbers
1396
+ if (startLine < 1) {
1397
+ return {
1398
+ success: false,
1399
+ error: 'Start line must be >= 1'
1400
+ };
1401
+ }
1402
+ if (endLine < startLine) {
1403
+ return {
1404
+ success: false,
1405
+ error: 'End line must be >= start line'
1406
+ };
1407
+ }
1408
+ if (endLine - startLine > 2000) {
1409
+ return {
1410
+ succes: false,
1411
+ error: 'You are note allowed to read more than 2000 lines at once. End line - start line must be less than 2000'
1412
+ };
1413
+ }
1414
+ // Helper to read lines from a file
1415
+ const tryReadFileLines = async (targetPath) => {
1416
+ try {
1417
+ const content = await fs.readFile(targetPath, 'utf-8');
1418
+ const allLines = content.split('\n');
1419
+ // Check if requested range is valid
1420
+ if (startLine > allLines.length) {
1421
+ return {
1422
+ success: false,
1423
+ error: `Start line ${startLine} exceeds file length (${allLines.length} lines)`,
1424
+ fileLength: allLines.length,
1425
+ filePath: targetPath
1426
+ };
1427
+ }
1428
+ // Adjust end line if it exceeds file length
1429
+ const actualEndLine = Math.min(endLine, allLines.length);
1430
+ // Extract requested lines (convert to 0-indexed)
1431
+ const requestedLines = allLines.slice(startLine - 1, actualEndLine);
1432
+ // Format lines with line numbers for easy reference
1433
+ const formattedLines = requestedLines.map((line, index) => {
1434
+ const lineNumber = startLine + index;
1435
+ return `${lineNumber.toString().padStart(6, ' ')}|${line}`;
1436
+ }).join('\n');
1437
+ return {
1438
+ success: true,
1439
+ filePath: targetPath,
1440
+ startLine,
1441
+ endLine: actualEndLine,
1442
+ lineCount: requestedLines.length,
1443
+ content: formattedLines,
1444
+ rawLines: requestedLines
1445
+ };
1446
+ }
1447
+ catch (error) {
1448
+ return null; // Signal to try fallback
1449
+ }
1450
+ };
1451
+ // Try the provided file path first
1452
+ const result = await tryReadFileLines(filePath);
1453
+ if (result) {
1454
+ return result;
1455
+ }
1456
+ // Fallback: If we have an indexer, search for file with matching path
1457
+ if (globalIndexer) {
1458
+ console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1459
+ const indexData = globalIndexer.index;
1460
+ if (indexData && indexData.files) {
1461
+ const searchName = path.basename(filePath);
1462
+ const searchDir = path.dirname(filePath);
1463
+ const matches = [];
1464
+ // Find files with matching basename
1465
+ for (const indexedFilePath of Object.keys(indexData.files)) {
1466
+ if (path.basename(indexedFilePath) === searchName) {
1467
+ matches.push(indexedFilePath);
1468
+ }
1469
+ }
1470
+ if (matches.length > 0) {
1471
+ let correctPath = matches[0];
1472
+ // If multiple matches, try to find the best one by matching directory structure
1473
+ if (matches.length > 1) {
1474
+ const searchParts = searchDir.split(path.sep).filter(p => p && p !== '.');
1475
+ // Score each match by how many path components match
1476
+ const scored = matches.map(m => {
1477
+ const matchParts = path.dirname(m).split(path.sep);
1478
+ let score = 0;
1479
+ for (let i = 0; i < searchParts.length; i++) {
1480
+ const searchPart = searchParts[searchParts.length - 1 - i];
1481
+ const matchPart = matchParts[matchParts.length - 1 - i];
1482
+ if (searchPart === matchPart) {
1483
+ score += (i + 1); // Weight recent parts higher
1484
+ }
1485
+ }
1486
+ return { path: m, score };
1487
+ });
1488
+ // Sort by score (highest first)
1489
+ scored.sort((a, b) => b.score - a.score);
1490
+ correctPath = scored[0].path;
1491
+ console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1492
+ }
1493
+ else {
1494
+ console.log(` โœ… Found file at ${correctPath}`);
1495
+ }
1496
+ // Try to read from correct path
1497
+ const retryResult = await tryReadFileLines(correctPath);
1498
+ if (retryResult) {
1499
+ return retryResult;
1500
+ }
1501
+ // If still failed, return error with suggestion
1502
+ return {
1503
+ success: false,
1504
+ error: `Found file at ${correctPath} but failed to read it`,
1505
+ suggestion: correctPath,
1506
+ allMatches: matches.length > 1 ? matches : undefined
1507
+ };
1508
+ }
1509
+ }
1510
+ return {
1511
+ success: false,
1512
+ error: `File not found: ${filePath}. No matching files found in index.`
1513
+ };
1514
+ }
1515
+ // No indexer available
1516
+ return {
1517
+ success: false,
1518
+ error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
1519
+ };
1520
+ }
1521
+ function resolveImportPath(fromFile, importPath) {
1522
+ try {
1523
+ if (importPath.startsWith('.')) {
1524
+ const dir = path.dirname(fromFile);
1525
+ const resolved = path.resolve(dir, importPath);
1526
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
1527
+ for (const ext of extensions) {
1528
+ const withExt = resolved + ext;
1529
+ if (fsSync.existsSync(withExt)) {
1530
+ // console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
1531
+ // success: true,
1532
+ // resolvedPath: withExt
1533
+ // })})`)
1534
+ return { success: true, resolvedPath: withExt };
1535
+ }
1536
+ }
1537
+ if (fsSync.existsSync(resolved)) {
1538
+ // console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
1539
+ // success: true,
1540
+ // resolvedPath: resolved
1541
+ // })})`)
1542
+ return { success: true, resolvedPath: resolved };
1543
+ }
1544
+ }
1545
+ // console.log(`resolve_import_path: JSON.stringify(${JSON.stringify({
1546
+ // success: true,
1547
+ // resolvedPath: importPath,
1548
+ // isExternal: true
1549
+ // })})`)
1550
+ return {
1551
+ success: true,
1552
+ resolvedPath: importPath,
1553
+ isExternal: true
1554
+ };
1555
+ }
1556
+ catch (error) {
1557
+ return { success: false, error: error.message };
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Validate test file syntax using AST parsing
1562
+ * Provides detailed error information for AI to retry
1563
+ */
1564
+ async function validateTestFileSyntax(filePath) {
1565
+ try {
1566
+ // Read the file content
1567
+ const content = await fs.readFile(filePath, 'utf-8');
1568
+ // Try to parse with Babel - this will throw if syntax is invalid
1569
+ babelParser.parse(content, {
1570
+ sourceType: 'module',
1571
+ plugins: [
1572
+ 'typescript',
1573
+ 'jsx',
1574
+ 'decorators-legacy'
1575
+ ]
1576
+ });
1577
+ // If parsing succeeded, syntax is valid
1578
+ return { valid: true };
1579
+ }
1580
+ catch (error) {
1581
+ // Extract error details
1582
+ const line = error.loc?.line || error.lineNumber || 0;
1583
+ const column = error.loc?.column || error.column || 0;
1584
+ let errorMessage = error.message || 'Unknown syntax error';
1585
+ // Clean up error message (remove file path prefix if present)
1586
+ errorMessage = errorMessage.replace(/^.*?: /, '');
1587
+ // Generate helpful suggestion based on error type
1588
+ let suggestion = '';
1589
+ if (errorMessage.includes('Unexpected token') || errorMessage.includes('unexpected')) {
1590
+ suggestion = 'Check for missing/extra brackets, parentheses, or commas near this location.';
1591
+ }
1592
+ else if (errorMessage.includes('Unterminated')) {
1593
+ suggestion = 'You have an unclosed string, comment, or template literal.';
1594
+ }
1595
+ else if (errorMessage.includes('Expected')) {
1596
+ suggestion = 'Missing required syntax element. Check if you closed all blocks properly.';
1597
+ }
1598
+ else if (errorMessage.includes('duplicate') || errorMessage.includes('already')) {
1599
+ suggestion = 'Duplicate declaration detected. Check for repeated variable/function names.';
1600
+ }
1601
+ else {
1602
+ suggestion = 'Review the code structure around this line for syntax issues.';
1603
+ }
1604
+ return {
1605
+ valid: false,
1606
+ error: errorMessage,
1607
+ location: { line, column },
1608
+ suggestion
1609
+ };
1610
+ }
1611
+ }
1612
+ async function writeTestFile(filePath, content, sourceFilePath, functionMode = false) {
1613
+ try {
1614
+ // VALIDATE: Check for incomplete/placeholder tests BEFORE writing
1615
+ const invalidPatterns = [
1616
+ /\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
1617
+ /\/\/\s*(Add more|write more|Similarly|write tests for)/i,
1618
+ /\/\/\s*TODO/i,
1619
+ /\/\/\s*\.\.\./,
1620
+ /\/\/.*etc\./i,
1621
+ /expect\(\).*\/\//, // expect() followed by comment
1622
+ ];
1623
+ const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
1624
+ if (hasPlaceholders) {
970
1625
  // Extract the actual placeholder comment for the error message
971
1626
  const foundPlaceholder = content.match(invalidPatterns.find(p => p.test(content)) || /\/\/.*/);
972
1627
  return {
@@ -1000,18 +1655,12 @@ async function writeTestFile(filePath, content, sourceFilePath, functionMode = f
1000
1655
  }
1001
1656
  }
1002
1657
  }
1003
- else if (describeCount < 3) {
1004
- return {
1005
- success: false,
1006
- 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!`
1007
- };
1008
- }
1009
- if (testCount < Math.max(4, expectedFunctionCount * 2)) {
1010
- return {
1011
- success: false,
1012
- error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
1013
- };
1014
- }
1658
+ // if (testCount < Math.max(4, expectedFunctionCount * 2)) {
1659
+ // return {
1660
+ // success: false,
1661
+ // error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
1662
+ // };
1663
+ // }
1015
1664
  }
1016
1665
  const dir = path.dirname(filePath);
1017
1666
  await fs.mkdir(dir, { recursive: true });
@@ -1070,7 +1719,7 @@ async function editTestFile(filePath, oldContent, newContent) {
1070
1719
  const preview = content.substring(0, 500);
1071
1720
  return {
1072
1721
  success: false,
1073
- error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use 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.`,
1074
1723
  currentContent: preview
1075
1724
  };
1076
1725
  }
@@ -1154,6 +1803,19 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
1154
1803
  updatedLines.push(...afterBlock);
1155
1804
  const updatedContent = updatedLines.join('\n');
1156
1805
  await fs.writeFile(testFilePath, updatedContent, 'utf-8');
1806
+ // Validate syntax after modification
1807
+ const validation = await validateTestFileSyntax(testFilePath);
1808
+ if (!validation.valid) {
1809
+ // Rollback the change by restoring original content
1810
+ await fs.writeFile(testFilePath, content, 'utf-8');
1811
+ return {
1812
+ success: false,
1813
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
1814
+ syntaxError: true,
1815
+ location: validation.location,
1816
+ suggestion: validation.suggestion
1817
+ };
1818
+ }
1157
1819
  return {
1158
1820
  success: true,
1159
1821
  message: `Replaced existing tests for function '${functionName}' (lines ${describeStartLine + 1}-${describeEndLine + 1})`,
@@ -1197,10 +1859,23 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
1197
1859
  // Validate bracket pairing
1198
1860
  const openCount = (updatedContent.match(/{/g) || []).length;
1199
1861
  const closeCount = (updatedContent.match(/}/g) || []).length;
1200
- if (openCount !== closeCount) {
1201
- console.warn(`โš ๏ธ Warning: Bracket mismatch detected (${openCount} opening, ${closeCount} closing). The test file structure may be malformed.`);
1202
- }
1862
+ // if (openCount !== closeCount) {
1863
+ // console.warn(`โš ๏ธ Warning: Bracket mismatch detected (${openCount} opening, ${closeCount} closing). The test file structure may be malformed.`);
1864
+ // }
1203
1865
  await fs.writeFile(testFilePath, updatedContent, 'utf-8');
1866
+ // Validate syntax after modification
1867
+ const validation = await validateTestFileSyntax(testFilePath);
1868
+ if (!validation.valid) {
1869
+ // Rollback the change by restoring original content
1870
+ await fs.writeFile(testFilePath, content, 'utf-8');
1871
+ return {
1872
+ success: false,
1873
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
1874
+ syntaxError: true,
1875
+ location: validation.location,
1876
+ suggestion: validation.suggestion
1877
+ };
1878
+ }
1204
1879
  return {
1205
1880
  success: true,
1206
1881
  message: `Added tests for function '${functionName}' at root level (after line ${lastRootLevelClosing + 1})`,
@@ -1217,7 +1892,7 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
1217
1892
  function runTests(testFilePath, functionNames) {
1218
1893
  try {
1219
1894
  // Build Jest command with optional function name filter
1220
- let command = `npx jest ${testFilePath} --no-coverage`;
1895
+ let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
1221
1896
  // If function names provided, use Jest -t flag to run only those tests
1222
1897
  if (functionNames && functionNames.length > 0) {
1223
1898
  // Create regex pattern to match any of the function names
@@ -1230,6 +1905,7 @@ function runTests(testFilePath, functionNames) {
1230
1905
  encoding: 'utf-8',
1231
1906
  stdio: 'pipe'
1232
1907
  });
1908
+ console.log(` Test run output: ${output}`);
1233
1909
  return {
1234
1910
  success: true,
1235
1911
  output,
@@ -1238,6 +1914,8 @@ function runTests(testFilePath, functionNames) {
1238
1914
  };
1239
1915
  }
1240
1916
  catch (error) {
1917
+ console.log(` Test run error: ${error.message}`);
1918
+ console.log(`output sent to ai: ${error.stdout + error.stderr}`);
1241
1919
  return {
1242
1920
  success: false,
1243
1921
  output: error.stdout + error.stderr,
@@ -1338,6 +2016,13 @@ function calculateRelativePath(fromFile, toFile) {
1338
2016
  }
1339
2017
  // Convert backslashes to forward slashes (Windows compatibility)
1340
2018
  relativePath = relativePath.replace(/\\/g, '/');
2019
+ // console.log(`calculate_relative_path: JSON.stringify(${JSON.stringify({
2020
+ // success: true,
2021
+ // from: fromFile,
2022
+ // to: toFile,
2023
+ // relativePath,
2024
+ // importStatement: `import { ... } from '${relativePath}';`
2025
+ // })})`)
1341
2026
  return {
1342
2027
  success: true,
1343
2028
  from: fromFile,
@@ -1454,43 +2139,276 @@ async function replaceLines(filePath, startLine, endLine, newContent) {
1454
2139
  return { success: false, error: error.message };
1455
2140
  }
1456
2141
  }
2142
+ /**
2143
+ * Search and replace a code block using fuzzy matching
2144
+ * Much more reliable than line-based editing!
2145
+ */
2146
+ async function searchReplaceBlock(filePath, search, replace, matchMode = 'normalized') {
2147
+ try {
2148
+ const content = await fs.readFile(filePath, 'utf-8');
2149
+ // Try exact match first if in normalized/fuzzy mode (faster)
2150
+ if (matchMode !== 'exact' && content.includes(search)) {
2151
+ const newContent = content.replace(search, replace);
2152
+ await fs.writeFile(filePath, newContent, 'utf-8');
2153
+ // Validate syntax if this is a test file
2154
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
2155
+ const validation = await validateTestFileSyntax(filePath);
2156
+ if (!validation.valid) {
2157
+ // Rollback the change by restoring original content
2158
+ await fs.writeFile(filePath, content, 'utf-8');
2159
+ return {
2160
+ success: false,
2161
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
2162
+ syntaxError: true,
2163
+ location: validation.location,
2164
+ suggestion: validation.suggestion
2165
+ };
2166
+ }
2167
+ }
2168
+ return {
2169
+ success: true,
2170
+ message: 'Replaced block (exact match)',
2171
+ matchMode: 'exact',
2172
+ confidence: 1.0,
2173
+ linesChanged: newContent.split('\n').length - content.split('\n').length
2174
+ };
2175
+ }
2176
+ // Bailout for very large files/searches to prevent hanging
2177
+ const contentSize = content.length;
2178
+ const searchSize = search.length;
2179
+ const isLargeOperation = contentSize > 200000 || searchSize > 2000;
2180
+ // Use smart search (force normalized for large files to skip expensive fuzzy)
2181
+ const searchMode = isLargeOperation ? 'normalized' : matchMode;
2182
+ const result = (0, fuzzyMatcher_1.smartSearch)(content, search, searchMode);
2183
+ if (!result || !result.found) {
2184
+ // Skip expensive similarity search for large files
2185
+ const suggestions = isLargeOperation ? [] : (0, fuzzyMatcher_1.findSimilarBlocks)(content, search, 3);
2186
+ const errorMessage = isLargeOperation
2187
+ ? 'Search block not found. File is large - try including more unique context or use exact match mode.'
2188
+ : (0, fuzzyMatcher_1.getSearchFailureMessage)(content, search);
2189
+ return {
2190
+ success: false,
2191
+ error: errorMessage,
2192
+ searchPreview: search.substring(0, 200) + (search.length > 200 ? '...' : ''),
2193
+ suggestions: suggestions.map(s => ({
2194
+ text: s.text.substring(0, 150) + (s.text.length > 150 ? '...' : ''),
2195
+ similarity: Math.round(s.similarity * 100) + '%',
2196
+ lines: `${s.startLine}-${s.endLine}`
2197
+ })),
2198
+ hint: suggestions.length > 0
2199
+ ? `Found ${suggestions.length} similar block(s). Check if your search text has typos or try including more context lines.`
2200
+ : 'No similar blocks found. Make sure your search text matches the file content and includes enough context (3-5 lines around the target).'
2201
+ };
2202
+ }
2203
+ // Perform replacement
2204
+ const newContent = content.substring(0, result.startIndex) +
2205
+ replace +
2206
+ content.substring(result.endIndex);
2207
+ await fs.writeFile(filePath, newContent, 'utf-8');
2208
+ // Validate syntax if this is a test file
2209
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
2210
+ const validation = await validateTestFileSyntax(filePath);
2211
+ if (!validation.valid) {
2212
+ // Rollback the change by restoring original content
2213
+ await fs.writeFile(filePath, content, 'utf-8');
2214
+ return {
2215
+ success: false,
2216
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
2217
+ syntaxError: true,
2218
+ location: validation.location,
2219
+ suggestion: validation.suggestion
2220
+ };
2221
+ }
2222
+ }
2223
+ // Get lines for context
2224
+ const getLineNumber = (charIndex) => {
2225
+ return content.substring(0, charIndex).split('\n').length;
2226
+ };
2227
+ const startLine = getLineNumber(result.startIndex);
2228
+ const endLine = getLineNumber(result.endIndex);
2229
+ const result1 = { success: true,
2230
+ message: `Replaced block successfully using ${result.matchType} matching`,
2231
+ matchMode: result.matchType,
2232
+ confidence: result.confidence,
2233
+ linesChanged: newContent.split('\n').length - content.split('\n').length,
2234
+ linesReplaced: `${startLine}-${endLine}`,
2235
+ replacedText: result.originalText.substring(0, 200) + (result.originalText.length > 200 ? '...' : '') };
2236
+ // console.log(`SearchReplaceBlock: JSON.stringify(${JSON.stringify(result1)})`)
2237
+ return {
2238
+ success: true,
2239
+ message: `Replaced block successfully using ${result.matchType} matching`,
2240
+ matchMode: result.matchType,
2241
+ confidence: result.confidence,
2242
+ linesChanged: newContent.split('\n').length - content.split('\n').length,
2243
+ linesReplaced: `${startLine}-${endLine}`,
2244
+ replacedText: result.originalText.substring(0, 200) + (result.originalText.length > 200 ? '...' : '')
2245
+ };
2246
+ }
2247
+ catch (error) {
2248
+ return { success: false, error: error.message };
2249
+ }
2250
+ }
2251
+ /**
2252
+ * Insert content at a specific position in the file
2253
+ * Simpler than search_replace_block for adding new content
2254
+ */
2255
+ async function insertAtPosition(filePath, content, position, afterMarker) {
2256
+ try {
2257
+ const fileContent = await fs.readFile(filePath, 'utf-8');
2258
+ const lines = fileContent.split('\n');
2259
+ let insertIndex = 0;
2260
+ let insertionPoint = '';
2261
+ if (afterMarker) {
2262
+ // Use smart search to find marker
2263
+ const result = (0, fuzzyMatcher_1.smartSearch)(fileContent, afterMarker, 'normalized');
2264
+ if (!result || !result.found) {
2265
+ return {
2266
+ success: false,
2267
+ error: `Marker not found: "${afterMarker.substring(0, 100)}..."`,
2268
+ hint: 'Use search_replace_block or specify a position instead'
2269
+ };
2270
+ }
2271
+ // Find line number of marker end
2272
+ let charCount = 0;
2273
+ for (let i = 0; i < lines.length; i++) {
2274
+ charCount += lines[i].length + 1; // +1 for newline
2275
+ if (charCount >= result.endIndex) {
2276
+ insertIndex = i + 1;
2277
+ insertionPoint = `after marker at line ${i + 1}`;
2278
+ break;
2279
+ }
2280
+ }
2281
+ }
2282
+ else {
2283
+ // Use position
2284
+ switch (position) {
2285
+ case 'beginning':
2286
+ insertIndex = 0;
2287
+ insertionPoint = 'beginning of file';
2288
+ break;
2289
+ case 'end':
2290
+ insertIndex = lines.length;
2291
+ insertionPoint = 'end of file';
2292
+ break;
2293
+ case 'after_imports': {
2294
+ // Find last import statement
2295
+ let lastImportLine = -1;
2296
+ for (let i = 0; i < lines.length; i++) {
2297
+ const trimmed = lines[i].trim();
2298
+ if (trimmed.startsWith('import ') || trimmed.startsWith('require(') ||
2299
+ trimmed.startsWith('const ') && trimmed.includes('require(')) {
2300
+ lastImportLine = i;
2301
+ }
2302
+ else if (trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && lastImportLine >= 0) {
2303
+ // Stop at first non-import, non-comment line after imports
2304
+ break;
2305
+ }
2306
+ }
2307
+ if (lastImportLine === -1) {
2308
+ return {
2309
+ success: false,
2310
+ error: 'No import statements found in file',
2311
+ hint: 'Use position: "beginning" instead or specify after_marker'
2312
+ };
2313
+ }
2314
+ insertIndex = lastImportLine + 1;
2315
+ insertionPoint = `after imports at line ${lastImportLine + 1}`;
2316
+ break;
2317
+ }
2318
+ case 'before_first_describe': {
2319
+ // Find first describe block
2320
+ let firstDescribe = -1;
2321
+ for (let i = 0; i < lines.length; i++) {
2322
+ if (lines[i].trim().startsWith('describe(')) {
2323
+ firstDescribe = i;
2324
+ break;
2325
+ }
2326
+ }
2327
+ if (firstDescribe === -1) {
2328
+ return {
2329
+ success: false,
2330
+ error: 'No describe blocks found in file',
2331
+ hint: 'Use position: "end" instead or specify after_marker'
2332
+ };
2333
+ }
2334
+ insertIndex = firstDescribe;
2335
+ insertionPoint = `before first describe at line ${firstDescribe + 1}`;
2336
+ break;
2337
+ }
2338
+ default:
2339
+ // Default to end
2340
+ insertIndex = lines.length;
2341
+ insertionPoint = 'end of file';
2342
+ }
2343
+ }
2344
+ // Insert content
2345
+ const contentLines = content.split('\n');
2346
+ lines.splice(insertIndex, 0, ...contentLines);
2347
+ const newContent = lines.join('\n');
2348
+ await fs.writeFile(filePath, newContent, 'utf-8');
2349
+ // Validate syntax if this is a test file
2350
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
2351
+ const validation = await validateTestFileSyntax(filePath);
2352
+ if (!validation.valid) {
2353
+ // Rollback the change by restoring original content
2354
+ await fs.writeFile(filePath, fileContent, 'utf-8');
2355
+ return {
2356
+ success: false,
2357
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
2358
+ syntaxError: true,
2359
+ location: validation.location,
2360
+ suggestion: validation.suggestion
2361
+ };
2362
+ }
2363
+ }
2364
+ return {
2365
+ success: true,
2366
+ message: `Inserted ${contentLines.length} line(s) at ${insertionPoint}`,
2367
+ insertedAt: insertionPoint,
2368
+ lineCount: contentLines.length
2369
+ };
2370
+ }
2371
+ catch (error) {
2372
+ return { success: false, error: error.message };
2373
+ }
2374
+ }
1457
2375
  // User-friendly messages for each tool
1458
2376
  const TOOL_MESSAGES = {
1459
2377
  'read_file': '๐Ÿ“– Reading source file',
2378
+ 'read_file_lines': '๐Ÿ“– Reading file section',
1460
2379
  'analyze_file_ast': '๐Ÿ” Analyzing codebase structure',
1461
2380
  'get_function_ast': '๐Ÿ”Ž Examining function details',
1462
2381
  'get_imports_ast': '๐Ÿ“ฆ Analyzing dependencies',
1463
2382
  'get_type_definitions': '๐Ÿ“‹ Extracting type definitions',
2383
+ 'get_file_preamble': '๐Ÿ“‹ Extracting file preamble (imports, mocks, setup)',
1464
2384
  'get_class_methods': '๐Ÿ—๏ธ Analyzing class structure',
1465
2385
  'resolve_import_path': '๐Ÿ”— Resolving import paths',
1466
- 'write_test_file': 'โœ๏ธ Writing test cases to file',
1467
- 'edit_test_file': 'โœ๏ธ Updating test file',
1468
- 'replace_function_tests': '๐Ÿ”„ Replacing test cases for specific functions',
2386
+ 'upsert_function_tests': 'โœ๏ธ Writing/updating test cases for function',
1469
2387
  'run_tests': '๐Ÿงช Running tests',
1470
2388
  'list_directory': '๐Ÿ“‚ Exploring directory structure',
1471
2389
  'find_file': '๐Ÿ” Locating file in repository',
1472
2390
  'calculate_relative_path': '๐Ÿงญ Calculating import path',
1473
2391
  'report_legitimate_failure': 'โš ๏ธ Reporting legitimate test failures',
1474
- 'delete_lines': '๐Ÿ—‘๏ธ Deleting lines from file',
1475
- 'insert_lines': 'โž• Inserting lines into file',
1476
- 'replace_lines': '๐Ÿ”„ Replacing lines in file'
2392
+ 'search_replace_block': '๐Ÿ”๐Ÿ”„ Searching and replacing code block',
2393
+ 'insert_at_position': 'โž• Inserting content at position',
1477
2394
  };
1478
2395
  // Tool execution router
1479
2396
  async function executeTool(toolName, args) {
1480
2397
  // Show user-friendly message with dynamic context
1481
2398
  let friendlyMessage = TOOL_MESSAGES[toolName] || `๐Ÿ”ง ${toolName}`;
1482
2399
  // Add specific details for certain tools
1483
- if (toolName === 'replace_function_tests' && args.function_name) {
1484
- 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}`;
1485
2402
  }
1486
- else if (toolName === 'delete_lines' && args.start_line && args.end_line) {
1487
- 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}"`;
1488
2406
  }
1489
- else if (toolName === 'insert_lines' && args.line_number) {
1490
- friendlyMessage = `โž• Inserting lines at line ${args.line_number}`;
2407
+ else if (toolName === 'insert_at_position' && args.position) {
2408
+ friendlyMessage = `โž• Inserting at: ${args.position}`;
1491
2409
  }
1492
- else if (toolName === 'replace_lines' && args.start_line && args.end_line) {
1493
- 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}`;
1494
2412
  }
1495
2413
  console.log(`\n${friendlyMessage}...`);
1496
2414
  let result;
@@ -1499,6 +2417,9 @@ async function executeTool(toolName, args) {
1499
2417
  case 'read_file':
1500
2418
  result = await readFile(args.file_path);
1501
2419
  break;
2420
+ case 'read_file_lines':
2421
+ result = await readFileLines(args.file_path, args.start_line, args.end_line);
2422
+ break;
1502
2423
  case 'analyze_file_ast':
1503
2424
  // Try cache first if indexer is available
1504
2425
  if (globalIndexer) {
@@ -1543,16 +2464,13 @@ async function executeTool(toolName, args) {
1543
2464
  case 'get_class_methods':
1544
2465
  result = getClassMethods(args.file_path, args.class_name);
1545
2466
  break;
2467
+ case 'get_file_preamble':
2468
+ result = getFilePreamble(args.file_path);
2469
+ break;
1546
2470
  case 'resolve_import_path':
1547
2471
  result = resolveImportPath(args.from_file, args.import_path);
1548
2472
  break;
1549
- case 'write_test_file':
1550
- result = await writeTestFile(args.file_path, args.content, args.source_file, args.function_mode);
1551
- break;
1552
- case 'edit_test_file':
1553
- result = await editTestFile(args.file_path, args.old_content, args.new_content);
1554
- break;
1555
- case 'replace_function_tests':
2473
+ case 'upsert_function_tests':
1556
2474
  result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
1557
2475
  break;
1558
2476
  case 'run_tests':
@@ -1570,14 +2488,11 @@ async function executeTool(toolName, args) {
1570
2488
  case 'report_legitimate_failure':
1571
2489
  result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
1572
2490
  break;
1573
- case 'delete_lines':
1574
- result = await deleteLines(args.file_path, args.start_line, args.end_line);
1575
- break;
1576
- case 'insert_lines':
1577
- result = await insertLines(args.file_path, args.line_number, args.content);
2491
+ case 'search_replace_block':
2492
+ result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
1578
2493
  break;
1579
- case 'replace_lines':
1580
- 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);
1581
2496
  break;
1582
2497
  default:
1583
2498
  result = { success: false, error: `Unknown tool: ${toolName}` };
@@ -1588,6 +2503,7 @@ async function executeTool(toolName, args) {
1588
2503
  }
1589
2504
  // Show result with friendly message
1590
2505
  if (result.success) {
2506
+ // console.log('result', JSON.stringify(result, null, 2));
1591
2507
  console.log(` โœ… Done`);
1592
2508
  }
1593
2509
  else if (result.error) {
@@ -1834,12 +2750,81 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
1834
2750
  // Main conversation loop
1835
2751
  async function generateTests(sourceFile) {
1836
2752
  console.log(`\n๐Ÿ“ Generating tests for: ${sourceFile}\n`);
1837
- const testFilePath = getTestFilePath(sourceFile);
1838
- const messages = [
1839
- {
1840
- role: 'user',
1841
- content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests including edge cases for a TypeScript file.
1842
-
2753
+ // Check file size and switch to function-by-function generation if large
2754
+ try {
2755
+ const fileContent = fsSync.readFileSync(sourceFile, 'utf-8');
2756
+ const lineCount = fileContent.split('\n').length;
2757
+ console.log(`๐Ÿ“Š Source file has ${lineCount} lines`);
2758
+ if (lineCount > 200) {
2759
+ console.log(`\nโšก File has more than 200 lines! Switching to function-by-function generation...\n`);
2760
+ // Analyze file to get all functions
2761
+ const result = analyzeFileAST(sourceFile);
2762
+ if (result && result.success && result.analysis && result.analysis.functions && result.analysis.functions.length > 0) {
2763
+ // Filter to only EXPORTED functions (public API)
2764
+ const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
2765
+ const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
2766
+ if (functionNames.length === 0) {
2767
+ console.log('โš ๏ธ No exported functions found in file. Falling back to regular generation.');
2768
+ // Fall through to regular generation
2769
+ }
2770
+ else {
2771
+ const totalFunctions = result.analysis.functions.length;
2772
+ const internalFunctions = totalFunctions - exportedFunctions.length;
2773
+ console.log(`โœ… Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
2774
+ if (internalFunctions > 0) {
2775
+ console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
2776
+ }
2777
+ // Use function-by-function generation
2778
+ try {
2779
+ return await generateTestsForFunctions(sourceFile, functionNames);
2780
+ }
2781
+ catch (funcError) {
2782
+ // CRITICAL: Check if test file already exists with content
2783
+ const testFilePath = getTestFilePath(sourceFile);
2784
+ if (fsSync.existsSync(testFilePath)) {
2785
+ const existingContent = fsSync.readFileSync(testFilePath, 'utf-8');
2786
+ const hasExistingTests = existingContent.includes('describe(') || existingContent.includes('test(');
2787
+ if (hasExistingTests) {
2788
+ console.error('\nโŒ CRITICAL: Function-by-function generation failed, but test file already has tests!');
2789
+ console.error(' Cannot fall back to file-wise generation as it would OVERWRITE existing tests.');
2790
+ console.error(` Error: ${funcError.message}`);
2791
+ console.error('\n Options:');
2792
+ console.error(' 1. Fix the issue manually in the test file');
2793
+ console.error(' 2. Delete the test file and regenerate from scratch');
2794
+ console.error(' 3. Run function-wise generation again for remaining functions');
2795
+ throw new Error(`Function-by-function generation failed with existing tests. Manual intervention required. Original error: ${funcError.message}`);
2796
+ }
2797
+ }
2798
+ // No existing tests, safe to fall back
2799
+ console.log('โš ๏ธ Function-by-function generation failed, but no existing tests found. Falling back to file-wise generation.');
2800
+ throw funcError; // Re-throw to be caught by outer try-catch
2801
+ }
2802
+ }
2803
+ }
2804
+ else {
2805
+ console.log('โš ๏ธ No functions found in file. Falling back to regular generation.');
2806
+ if (result && !result.success) {
2807
+ console.log(` Analysis error: ${result.error}`);
2808
+ }
2809
+ }
2810
+ }
2811
+ }
2812
+ catch (error) {
2813
+ // Check if this is the "existing tests" protection error
2814
+ if (error.message && error.message.includes('Manual intervention required')) {
2815
+ // This is a critical error - don't proceed with generation
2816
+ console.error(`\nโŒ Aborting: ${error.message}`);
2817
+ throw error; // Re-throw to stop execution
2818
+ }
2819
+ console.log(`โš ๏ธ Could not check file size: ${error}. Proceeding with regular generation.`);
2820
+ // Falls through to regular file-wise generation below
2821
+ }
2822
+ const testFilePath = getTestFilePath(sourceFile);
2823
+ const messages = [
2824
+ {
2825
+ role: 'user',
2826
+ content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests including edge cases for a TypeScript file.
2827
+
1843
2828
  Source file: ${sourceFile}
1844
2829
  Test file path: ${testFilePath}
1845
2830
 
@@ -1848,44 +2833,52 @@ IMPORTANT: You MUST use the provided tools to complete this task. Do not just re
1848
2833
  Your task (you MUST complete ALL steps):
1849
2834
  1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
1850
2835
  - This provides metadata about all code structures without loading full file content
2836
+ - CRITICAL: You have only 50 iterations to complete this task, so make sure you are using the tools efficiently.
2837
+ - Do not over explore, use the tools to get the information you need and start generating tests.
1851
2838
  2. Use get_imports_ast tool to understand all dependencies
1852
- 3. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths for the test file
2839
+ 3. For each dependency, use find_file(filePath) to locate the file and calculate_relative_path to get correct import paths for the test file
1853
2840
  4. For complex functions, use get_function_ast tool to get detailed information
1854
2841
  - Returns complete function code WITH JSDoc comments
1855
2842
  - Includes calledFunctions and calledMethods lists showing what the function calls
1856
2843
  - Use this to fetch related helper functions if needed
1857
- 5. โš ๏ธ AVOID read_file on source files over 5000 lines! Use get_function_ast for specific functions instead
1858
- 6. For classes, use get_class_methods tool to extract all methods
1859
- 7. Use get_type_definitions tool to understand TypeScript types and interfaces
1860
- 8. Generate comprehensive Jest unit tests with:
1861
- - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
1862
- - Mock database modules like '../database' or '../database/index'
1863
- - Mock models, services, and any modules that access config/database
1864
- - Mock config properly
1865
- - Mock isEmpty from lodash to return the expected values.
1866
- - Use jest.mock() calls at the TOP of the file before any imports
1867
- - Test suites for each function/class
1868
- - REQUIRED: Test cases should include security and input validation tests.
1869
- - REQUIRED: Multiple test cases covering:
1870
- * Happy path scenarios
1871
- * Edge cases (null, undefined, empty arrays, etc.)
1872
- * Error conditions
1873
- * Async behavior (if applicable)
1874
- - Proper TypeScript types
1875
- - Clear, descriptive test names
1876
- - Complete test implementations (NO placeholder comments!)
1877
- 9. REQUIRED: Write the COMPLETE test file using write_test_file tool with REAL test code (NOT placeholders!)
1878
- - CRITICAL: Include source_file parameter with path to source file (e.g., source_file: "${sourceFile}")
2844
+ - [CRITICAL]: If a function calls other functions from other files, use find_file + get_function_ast tools to locate them and check if they need to mocked, since they can be making api calls to external services.
2845
+ 5. Use get_function_ast to get detailed information about the functions.
2846
+ 6. For large test files (>5K lines), use get_file_preamble to see existing imports/mocks/setup blocks
2847
+ - Automatically included when reading large test files
2848
+ - Use before adding new test cases to avoid duplicate mocks/imports
2849
+ - Particularly useful when updating existing test files with upsert_function_tests
2850
+ - Captures complete multi-line mocks including complex jest.mock() statements
2851
+ 7. For classes, use get_class_methods tool to extract all methods
2852
+ 8. Use get_type_definitions tool to understand TypeScript types and interfaces
2853
+ 9. Generate comprehensive Jest unit tests with:
2854
+ - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors. Ensure tests fully mock the config module with all expected properties.
2855
+ - If required:
2856
+ - Mock database modules like '../database' or '../database/index' with virtual:true.
2857
+ - Mock models, and any modules that access config or the database with virtual:true.
2858
+ - Mock config properly with virtual:true.
2859
+ - Mock isEmpty from lodash to return the expected values with virtual:true.
2860
+ - Axios should be mocked with virtual:true.
2861
+ - Use jest.mock() calls at the TOP of the file before any imports
2862
+ - [CRITICAL]: Virtual modules should only be used for db/config/models/services/index/axios/routes files. You should not use virtual:true for any other files or helpers that exist in the source code. The actual helpers should never be mocked with virtual:true.
2863
+ 9. REQUIRED: Write tests using upsert_function_tests tool for EACH function with REAL test code (NOT placeholders!)
2864
+ - Call upsert_function_tests once for EACH exported function
2865
+ - Ensure comprehensive mocks are included in the first function's test to set up the file
1879
2866
  - DO NOT use ANY placeholder comments like:
1880
2867
  * "// Mock setup", "// Assertions", "// Call function"
1881
2868
  * "// Further tests...", "// Additional tests..."
1882
2869
  * "// Similarly, write tests for..."
1883
2870
  * "// Add more tests...", "// TODO", "// ..."
1884
2871
  - Write ACTUAL working test code with real mocks, real assertions, real function calls
1885
- - Every test MUST have:
2872
+ - Every test MUST have [MANDATORY]:
1886
2873
  * Real setup code (mock functions, create test data)
1887
2874
  * Real execution (call the function being tested)
1888
2875
  * Real expect() assertions (at least one per test)
2876
+ * null/undefined handling tests for all API responses
2877
+ * Happy path scenarios
2878
+ * Edge cases (null, undefined, empty arrays, etc.)
2879
+ * Error conditions
2880
+ * Async behavior (if applicable)
2881
+ - Proper TypeScript types
1889
2882
  - Write tests for EVERY exported function (minimum 3-5 tests per function)
1890
2883
  - If source has 4 functions, test file MUST have 4 describe blocks with actual tests
1891
2884
  - Example of COMPLETE test structure:
@@ -1894,14 +2887,17 @@ Your task (you MUST complete ALL steps):
1894
2887
  * Assert: Use expect() to verify results
1895
2888
  10. REQUIRED: Run the tests using run_tests tool
1896
2889
  11. REQUIRED: If tests fail with import errors:
1897
- - Use find_file tool to locate the missing module
2890
+ - Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
1898
2891
  - Use calculate_relative_path tool to get correct import path
1899
- - PRIMARY METHOD (once test file exists): Use line-based editing:
1900
- * read_file to get current test file with line numbers
1901
- * insert_lines to add missing imports at correct position (e.g., line 3)
1902
- * delete_lines to remove incorrect imports
1903
- * replace_lines to fix import paths
1904
- - FALLBACK: Only use edit_test_file or write_test_file if line-based editing isn't suitable
2892
+ - โœ… PRIMARY METHOD: Use search_replace_block to fix imports:
2893
+ * Include 3-5 lines of context around the import to change
2894
+ * Example: search_replace_block({
2895
+ search: "import { oldImport } from './old-path';\nimport { other } from './other';",
2896
+ replace: "import { oldImport, newImport } from './correct-path';\nimport { other } from './other';"
2897
+ })
2898
+ - ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding new imports:
2899
+ * insert_at_position({ position: 'after_imports', content: "import { newImport } from './path';" })
2900
+ - โš ๏ธ AVOID: Line-based tools (deprecated, fragile)
1905
2901
  12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
1906
2902
 
1907
2903
  FIXABLE ERRORS (you should fix these):
@@ -1909,37 +2905,128 @@ Your task (you MUST complete ALL steps):
1909
2905
  - Missing mocks
1910
2906
  - Incorrect mock implementations
1911
2907
  - Wrong assertions or test logic
1912
- - TypeScript compilation errors
2908
+ - TypeScript compilation errors (syntax errors, bracket mismatches)
1913
2909
  - Missing test setup/teardown
1914
2910
  - Cannot read properties of undefined
2911
+ - Test case failed to run. Use read_file_lines tool to read the specific problematic section and fix the issue.
2912
+
2913
+ ๐Ÿ’ก TIP: For syntax errors or bracket mismatches:
2914
+ - Use read_file to see the file content (it includes line numbers)
2915
+ - Use search_replace_block to fix the problematic section
2916
+ - Include 3-5 lines of context around the error to make search unique
2917
+ - Example: search_replace_block({
2918
+ search: "line before error\nproblematic code with syntax error\nline after",
2919
+ replace: "line before error\ncorrected code\nline after"
2920
+ })
1915
2921
 
1916
2922
  LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
1917
2923
  - Function returns wrong type (e.g., undefined instead of object)
1918
2924
  - Missing null/undefined checks in source code
1919
2925
  - Logic errors in source code
1920
2926
  - Unhandled promise rejections in source code
1921
- - Source code throws unexpected errors
1922
2927
 
1923
2928
  13. If errors are FIXABLE (AFTER test file is written):
1924
- - โœ… PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
1925
- * read_file to get current test file with line numbers
1926
- * delete_lines to remove incorrect lines
1927
- * insert_lines to add missing code (e.g., mocks, imports)
1928
- * replace_lines to fix specific line ranges
1929
- * This is FASTER and MORE RELIABLE than rewriting entire file!
1930
- - โš ๏ธ FALLBACK: Only use edit_test_file or write_test_file if:
1931
- * Line-based editing is too complex (needs major restructuring)
1932
- * Multiple scattered changes across the file
2929
+ - โœ… PRIMARY METHOD: Use search_replace_block (RECOMMENDED):
2930
+ * Find the problematic code section
2931
+ * Include 3-5 lines of context before/after to make search unique
2932
+ * Replace with corrected version
2933
+ * Example: search_replace_block({
2934
+ file_path: "test.ts",
2935
+ search: "const mock = jest.fn();\ntest('old test', () => {\n mock();",
2936
+ replace: "const mock = jest.fn().mockResolvedValue({ data: 'test' });\ntest('fixed test', () => {\n mock();"
2937
+ })
2938
+ * Handles whitespace/indentation differences automatically!
2939
+ - ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding mocks/imports at top:
2940
+ * insert_at_position({ position: 'after_imports', content: "jest.mock('../database');" })
2941
+ - โš ๏ธ AVOID: Line-based tools (deprecated) - they are fragile and prone to errors
1933
2942
  - Then retry running tests
1934
2943
  14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
1935
2944
  - Provide failing test names, reason, and source code issue description
1936
2945
  - The test file will be kept as-is with legitimate failing tests
2946
+ - You are not allowed to call this tool for error - Test suite failed to run. You must ensure that test cases get executed. Fix any syntax or linting issues in the test file.
1937
2947
  15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
1938
2948
  16. REQUIRED: Ensure all functions are tested in the test file.
1939
2949
  17. CRITICAL: config and database modules must be mocked
1940
2950
 
2951
+ 18. Some known issues when running tests with fixes:
2952
+ Route loading issue: Importing the controller triggered route setup because axios-helper imports from index.ts, which loads all routes. Routes referenced functions that weren't available during test initialization.
2953
+ - Solution: Mocked index.ts to export only whitelistDomainsForHeaders without executing route setup code.
2954
+ Axios mock missing required properties: The axios mock didn't include properties needed by axios-retry (like interceptors).
2955
+ - Solution: Created a createMockAxiosInstance function that returns a mock axios instance with interceptors, defaults, and HTTP methods.
2956
+ axios-retry not mocked: axios-retry was trying to modify axios instances during module initialization.
2957
+ - Solution: Added a mock for axios-retry to prevent it from executing during tests.
2958
+ Routes file execution: The routes file was being executed when the controller was imported.
2959
+ - Solution: Mocked the routes file to return a simple Express router without executing route definitions.
2960
+
1941
2961
  CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
1942
2962
 
2963
+ 19. [ALWAYS FOLLOW] Write Jest test cases following these MANDATORY patterns to prevent mock pollution:
2964
+
2965
+ SECTION 1: Top-Level Mock Setup
2966
+ Declare ALL jest.mock() calls at the top of the file, outside any describe blocks
2967
+ Mock EVERY function and module the controller/function uses
2968
+ Load the controller/module under test ONCE in beforeAll()
2969
+ [MANDATORY] Always use calculate_relative_path tool to get the correct import path for the module to be used in jest.mock() calls.
2970
+
2971
+ SECTION 2: Module References in Describe Blocks (CRITICAL)
2972
+ Declare ALL module references as let variables at the top of each describe block
2973
+ NEVER use const for module requires inside describe blocks
2974
+ Re-assign these modules inside beforeEach AFTER jest.clearAllMocks()
2975
+ This ensures each test gets fresh module references with clean mocks
2976
+
2977
+ SECTION 3: BeforeEach Pattern (MANDATORY)
2978
+ Pattern Structure:
2979
+
2980
+ Declare let variables for all modules at top of describe block
2981
+ Load controller once in beforeAll
2982
+ In beforeEach: clear mocks first, then re-assign all modules, then reset all mock implementations
2983
+ In afterEach: restore all mocks for extra safety
2984
+ In individual tests: only override specific mocks needed for that test
2985
+
2986
+ Example Pattern:
2987
+
2988
+ Declare: let controller, let helperModule, let responseHelper, let otherDependency
2989
+ beforeAll: controller = require path to controller
2990
+ beforeEach step 1: jest.clearAllMocks()
2991
+ beforeEach step 2: Re-assign all modules with require statements
2992
+ beforeEach step 3: Set default mock implementations for all mocked functions
2993
+ afterEach: jest.restoreAllMocks()
2994
+ In tests: Override only what changes for that specific test case
2995
+
2996
+ SECTION 4: What to NEVER Do
2997
+
2998
+ Never use const for module requires inside describe blocks
2999
+ Never rely on top-level module requires for mocking within tests
3000
+ Never share module references across multiple describe blocks
3001
+ Never skip re-assigning modules in beforeEach
3002
+ Never mutate module exports directly, always use mockImplementation instead
3003
+ Never forget to clear mocks before getting fresh references
3004
+
3005
+ SECTION 5: Key Principles
3006
+
3007
+ Isolation: Each test has its own mock instances via fresh requires
3008
+ Cleanup: Always clear and restore mocks between tests
3009
+ Explicit: Make all dependencies explicit in beforeEach
3010
+ Fresh References: Re-require modules after clearing to get clean mocks
3011
+ Default Behaviors: Set up sensible defaults in beforeEach, override in individual tests
3012
+
3013
+ SECTION 6: Verification Steps
3014
+
3015
+ Run tests multiple times with runInBand flag
3016
+ Run tests in random order with randomize flag
3017
+ Tests should pass consistently regardless of execution order
3018
+ Each test should be completely independent
3019
+
3020
+ SECTION 7: Critical Reminders
3021
+
3022
+ Always add missing mock initializations in relevant beforeEach blocks
3023
+ Ensure all mocked functions have default behaviors set in beforeEach
3024
+ Re-require modules after jest.clearAllMocks() to get fresh mock references
3025
+ Use let not const for all module references inside describe blocks
3026
+ Load the actual controller or module under test only once in beforeAll
3027
+
3028
+ Apply these patterns to ALL test files to ensure zero mock pollution between test suites and individual test cases.
3029
+
1943
3030
  START NOW by calling the analyze_file_ast tool with the source file path.`
1944
3031
  }
1945
3032
  ];
@@ -1978,7 +3065,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
1978
3065
  // Don't add the excuse to conversation, override with command
1979
3066
  messages.push({
1980
3067
  role: 'user',
1981
- content: 'STOP making excuses! You CAN use the tools. 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.'
1982
3069
  });
1983
3070
  continue;
1984
3071
  }
@@ -2001,7 +3088,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2001
3088
  if (!testFileWritten) {
2002
3089
  messages.push({
2003
3090
  role: 'user',
2004
- content: 'You have not written the test file yet. Use 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.'
2005
3092
  });
2006
3093
  }
2007
3094
  else {
@@ -2011,11 +3098,13 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2011
3098
 
2012
3099
  If tests are failing:
2013
3100
  - FIXABLE errors (imports, mocks, assertions):
2014
- โœ… PRIMARY: Use line-based editing (read_file + insert_lines/delete_lines/replace_lines)
2015
- โš ๏ธ 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.
2016
3105
  - LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
2017
3106
 
2018
- 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..." })`
2019
3108
  });
2020
3109
  }
2021
3110
  continue;
@@ -2038,81 +3127,41 @@ Start with read_file to see line numbers, then fix specific lines!`
2038
3127
  console.log(` Recommendation: ${result.recommendation}`);
2039
3128
  }
2040
3129
  // Track if test file was written
2041
- if (toolCall.name === 'write_test_file') {
3130
+ if (toolCall.name === 'upsert_function_tests') {
2042
3131
  if (result.success) {
2043
3132
  testFileWritten = true;
2044
- console.log(`\n๐Ÿ“ Test file written: ${result.path}`);
2045
- if (result.stats) {
2046
- console.log(` Tests: ${result.stats.tests}, Expectations: ${result.stats.expectations}`);
2047
- }
2048
- }
2049
- else {
2050
- // Test file was REJECTED due to validation
2051
- console.log(`\nโŒ Test file REJECTED: ${result.error}`);
2052
- testFileWritten = false; // Make sure we track it wasn't written
2053
- // Give very specific instructions based on rejection reason
2054
- if (result.error.includes('placeholder')) {
2055
- messages.push({
2056
- role: 'user',
2057
- content: `Your test file was REJECTED because it contains placeholder comments.
2058
-
2059
- You MUST rewrite it with COMPLETE code:
2060
- - Remove ALL comments like "// Further tests", "// Add test", "// Mock setup"
2061
- - Write the ACTUAL test implementation for EVERY function
2062
- - Each test needs: real setup, real function call, real expect() assertions
2063
-
2064
- Try again with write_test_file and provide COMPLETE test implementations!`
2065
- });
2066
- }
2067
- else if (result.error.includes('NO expect()')) {
2068
- messages.push({
2069
- role: 'user',
2070
- content: `Your test file was REJECTED because tests have no assertions!
2071
-
2072
- Every test MUST have expect() statements. Example:
2073
- expect(functionName).toHaveBeenCalled();
2074
- expect(result).toEqual(expectedValue);
2075
-
2076
- Rewrite with write_test_file and add actual expect() assertions to ALL tests!`
2077
- });
2078
- }
2079
- else if (result.error.includes('too few tests')) {
2080
- messages.push({
2081
- role: 'user',
2082
- content: `Your test file was REJECTED because it has too few tests!
2083
-
2084
- You analyzed ${toolResults.length > 0 ? 'multiple' : 'several'} functions in the source file. Write tests for ALL of them!
2085
- - Minimum 2-3 test cases per function
2086
- - Cover: happy path, edge cases, error cases
2087
-
2088
- Rewrite with write_test_file and include tests for EVERY function!`
2089
- });
2090
- }
3133
+ console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
2091
3134
  }
2092
3135
  }
2093
- // Detect if edit_test_file failed
2094
- if (toolCall.name === 'edit_test_file' && !result.success) {
2095
- 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}!`);
2096
3139
  messages.push({
2097
3140
  role: 'user',
2098
- 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}
2099
3144
 
2100
- โœ… SWITCH TO LINE-BASED EDITING (Primary Method):
3145
+ ๐Ÿ’ก ${result.suggestion}
2101
3146
 
2102
- Step 1: Call read_file tool to see the test file with line numbers
2103
- Step 2: Identify which lines need changes
2104
- Step 3: Use the appropriate tool:
2105
- - insert_lines: Add missing imports/mocks (e.g., line 5)
2106
- - delete_lines: Remove incorrect code (e.g., lines 10-12)
2107
- - replace_lines: Fix specific sections (e.g., lines 20-25)
3147
+ Your last modification created invalid syntax and was ROLLED BACK automatically.
2108
3148
 
2109
- Examples:
2110
- insert_lines({ file_path: "${testFilePath}", line_number: 5, content: "jest.mock('../database');" })
2111
- 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
2112
3156
 
2113
- โš ๏ธ 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
+ })
2114
3163
 
2115
- Start with read_file NOW to see line numbers!`
3164
+ Start NOW by reading the file around line ${result.location.line}!`
2116
3165
  });
2117
3166
  }
2118
3167
  // Detect repeated errors (suggests legitimate failure)
@@ -2123,15 +3172,15 @@ Start with read_file NOW to see line numbers!`
2123
3172
  sameErrorCount++;
2124
3173
  console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
2125
3174
  if (sameErrorCount >= 3) {
2126
- console.log('\n๐Ÿšจ Same error repeated 3+ times! Likely a legitimate source code issue.');
3175
+ console.log('\n๐Ÿšจ Same error repeated 3+ times! ');
2127
3176
  messages.push({
2128
3177
  role: 'user',
2129
3178
  content: `The same test error has occurred ${sameErrorCount} times in a row!
2130
3179
 
2131
- This suggests the failure is LEGITIMATE (source code bug), not a test issue.
2132
3180
 
2133
3181
  Analyze the error and determine:
2134
3182
  1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
3183
+ 2. Use available tools file read_file_lines to read the current state of file.
2135
3184
  2. Or is this a LEGITIMATE source code bug?
2136
3185
 
2137
3186
  If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
@@ -2159,20 +3208,23 @@ If FIXABLE: Make one more attempt to fix it.`
2159
3208
  role: 'user',
2160
3209
  content: `Import path error detected! Module not found: "${missingModule}"
2161
3210
 
2162
- โœ… FIX WITH LINE-BASED EDITING:
3211
+ โœ… FIX WITH SEARCH-REPLACE:
2163
3212
 
2164
3213
  Step 1: find_file tool to search for "${filename}" in the repository
2165
- Step 2: calculate_relative_path tool to get correct import path
2166
- Step 3: Fix using line-based tools:
2167
- a) read_file to see the test file with line numbers
2168
- b) Find the incorrect import line (search for "${missingModule}")
2169
- 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!
2170
3219
 
2171
3220
  Example workflow:
2172
3221
  1. find_file({ filename: "${filename}.ts" })
2173
3222
  2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
2174
- 3. read_file({ file_path: "${testFilePath}" })
2175
- 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
+ })
2176
3228
 
2177
3229
  Start NOW with find_file!`
2178
3230
  });
@@ -2185,28 +3237,25 @@ Start NOW with find_file!`
2185
3237
  role: 'user',
2186
3238
  content: `The test is failing because the source file imports modules that initialize database connections.
2187
3239
 
2188
- โœ… FIX WITH LINE-BASED EDITING:
2189
-
2190
- Step 1: read_file to see current test file structure
2191
- Step 2: insert_lines to add mocks at the TOP of the file (before any imports)
3240
+ โœ… FIX WITH SEARCH-REPLACE:
2192
3241
 
2193
- Required mocks to add:
2194
- jest.mock('../database', () => ({ default: {} }));
2195
- jest.mock('../database/index', () => ({ default: {} }));
2196
- 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
+ })
2197
3248
 
2198
- Example:
2199
- 1. read_file({ file_path: "${testFilePath}" })
2200
- 2. Find where imports start (usually line 1-3)
2201
- 3. insert_lines({
2202
- file_path: "${testFilePath}",
2203
- line_number: 1,
2204
- content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n"
3249
+ Option 2: If file already has some mocks, use search_replace_block to add more:
3250
+ search_replace_block({
3251
+ file_path: "${testFilePath}",
3252
+ search: "jest.mock('./existing-mock');\n\nimport { something }",
3253
+ replace: "jest.mock('./existing-mock');\njest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\nimport { something }"
2205
3254
  })
2206
3255
 
2207
3256
  โš ๏ธ Mocks MUST be at the TOP before any imports!
2208
3257
 
2209
- Start NOW with read_file to see current structure!`
3258
+ Start NOW with insert_at_position!`
2210
3259
  });
2211
3260
  }
2212
3261
  }
@@ -2374,106 +3423,240 @@ async function generateTestsForFolder() {
2374
3423
  console.log(`\nโœจ Folder processing complete! Processed ${files.length} files.`);
2375
3424
  }
2376
3425
  // Function-wise test generation
2377
- async function generateTestsForFunctions(sourceFile, functionNames) {
2378
- console.log(`\n๐Ÿ“ Generating tests for selected functions in: ${sourceFile}\n`);
2379
- const testFilePath = getTestFilePath(sourceFile);
2380
- 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) {
2381
3431
  const messages = [
2382
3432
  {
2383
3433
  role: 'user',
2384
- 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}.
2385
3435
 
2386
- Source file: ${sourceFile}
2387
- Test file path: ${testFilePath}
2388
- Test file exists: ${testFileExists}
2389
- Selected functions to test: ${functionNames.join(', ')}
3436
+ ## CONTEXT
3437
+ Test file: ${testFilePath} | Exists: ${testFileExists}
2390
3438
 
2391
- IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
3439
+ ---
2392
3440
 
2393
- ${testFileExists ? `
2394
- ๐Ÿšจ CRITICAL WARNING: Test file ALREADY EXISTS at ${testFilePath}! ๐Ÿšจ
3441
+ ## EXECUTION PLAN
2395
3442
 
2396
- You MUST use the replace_function_tests tool to update ONLY the selected function tests!
2397
- 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
+ \\\`\\\`\\\`
2398
3454
 
2399
- Other tests in this file for different functions MUST be preserved!
2400
- ` : ''}
3455
+ **Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
3456
+ *Before writing tests, map the logic requirements for external calls.*
3457
+ 1. Identify every external call (e.g., \`analyticsHelper.postEvent\`).
3458
+ 2. Trace backwards: What \`if\`, \`switch\`, or \`try/catch\` block guards this call?
3459
+ 3. Identify the dependency that controls that guard.
3460
+ 4. Plan the Mock Return: Determine exactly what value the dependency must return to enter that block.
2401
3461
 
2402
- Your task (you MUST complete ALL steps):
2403
- 1. FIRST: Use analyze_file_ast tool to get information about the selected functions: ${functionNames.join(', ')}
2404
- - This returns metadata about ALL functions in the file without loading full content
2405
- 2. Use get_function_ast tool for each selected function to get detailed information
2406
- - This returns the complete function code WITH JSDoc comments
2407
- - It also returns calledFunctions and calledMethods lists showing dependencies
2408
- - If the function calls other functions from the same file, use get_function_ast again to fetch them
2409
- 3. โš ๏ธ DO NOT use read_file on the source file! It's limited to 5000 lines and wastes tokens. Use get_function_ast instead!
2410
- 4. Use get_imports_ast tool to understand dependencies
2411
- 5. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths
2412
- 6. Generate comprehensive Jest unit tests ONLY for these functions: ${functionNames.join(', ')}
2413
- - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
2414
- - Mock database modules, models, services, and config modules
2415
- - Use jest.mock() calls at the TOP of the file before any imports
2416
- - Test suites for each selected function
2417
- - Multiple test cases covering:
2418
- * Happy path scenarios
2419
- * Edge cases (null, undefined, empty arrays, etc.)
2420
- * Error conditions
2421
- * Async behavior (if applicable)
2422
- - Proper TypeScript types
2423
- - Clear, descriptive test names
2424
- - Complete test implementations (NO placeholder comments!)
2425
- ${testFileExists ? `
2426
- 6. ๐Ÿšจ CRITICAL: Test file EXISTS! Call replace_function_tests tool for EACH function: ${functionNames.join(', ')}
2427
- - Call replace_function_tests ONCE for each function
2428
- - Pass the complete describe block as new_test_content parameter
2429
- - Example: replace_function_tests(test_file_path: "${testFilePath}", function_name: "${functionNames[0]}", new_test_content: "describe('${functionNames[0]}', () => { ... })")
2430
- - This preserves ALL other existing tests in the file
2431
- - DO NOT use write_test_file! It will DELETE all other tests!
2432
- - DO NOT use edit_test_file! Use replace_function_tests instead!` : `
2433
- 6. REQUIRED: Test file does NOT exist. Use write_test_file tool with tests for: ${functionNames.join(', ')} with function_mode set to true.
2434
- - Create a new test file with complete test implementation
2435
- - [CRITICAL]: Set function_mode parameter to true if using write_test_file tool`}}
2436
- 7. REQUIRED: Run the tests using run_tests tool
2437
- - ๐ŸŽฏ CRITICAL: Pass function_names parameter with the selected functions: ${JSON.stringify(functionNames)}
2438
- - Example: run_tests(test_file_path: "${testFilePath}", function_names: ${JSON.stringify(functionNames)})
2439
- - This runs ONLY tests for the selected functions, ignoring other tests in the file
2440
- - Other tests in the file may be failing - that's OK, we only care about the functions we're testing
2441
- 8. REQUIRED: If tests fail, analyze if errors are FIXABLE or LEGITIMATE:
2442
-
2443
- FIXABLE ERRORS (fix these):
2444
- - Wrong import paths โ†’ use find_file + calculate_relative_path + edit tools
2445
- - Missing mocks โ†’ add proper jest.mock() calls
2446
- - Incorrect mock implementations โ†’ update mock return values
2447
- - Wrong test assertions โ†’ fix expect() statements
2448
- - TypeScript errors โ†’ fix types and imports
2449
-
2450
- LEGITIMATE FAILURES (report these):
2451
- - Function returns wrong type (source code bug)
2452
- - Missing null checks in source code
2453
- - Logic errors in source code
2454
- - Source code throws unexpected errors
2455
-
2456
- 9. If FIXABLE (AFTER test file is written/updated):
2457
- ${testFileExists ? `- โœ… PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
2458
- * read_file to see current test file with line numbers
2459
- * delete_lines to remove incorrect lines
2460
- * insert_lines to add missing mocks, imports, or test cases
2461
- * replace_lines to fix specific sections
2462
- * This preserves ALL other tests and is more reliable!
2463
- - โš ๏ธ SECONDARY: replace_function_tests for specific function updates
2464
- - โŒ AVOID: write_test_file (will DELETE all other tests!)` : `- Use write_test_file to create the test file
2465
- - Once written, use line-based tools for fixes (read_file + insert/delete/replace_lines)`}
2466
- - Then retry tests
2467
- 10. If LEGITIMATE: Call report_legitimate_failure with details and STOP
2468
- 11. REQUIRED: Repeat steps 7-10 until tests pass OR legitimate failures reported
2469
-
2470
- ${testFileExists ? `
2471
- ๐Ÿšจ REMINDER: The test file EXISTS! Use replace_function_tests, NOT write_test_file! ๐Ÿšจ
2472
- ` : ''}
2473
-
2474
- CRITICAL: Fix test bugs but REPORT source code bugs (don't try to make broken code pass)!
3462
+ **Phase 2: Test Generation**
2475
3463
 
2476
- 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.`
2477
3660
  }
2478
3661
  ];
2479
3662
  let iterations = 0;
@@ -2484,6 +3667,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2484
3667
  let lastTestError = '';
2485
3668
  let sameErrorCount = 0;
2486
3669
  while (iterations < maxIterations) {
3670
+ console.log('USING CLAUDE PROMPT original 16');
2487
3671
  iterations++;
2488
3672
  if (iterations === 1) {
2489
3673
  console.log(`\n๐Ÿค– AI is analyzing selected functions...`);
@@ -2492,6 +3676,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2492
3676
  console.log(`\n๐Ÿค– AI is still working (step ${iterations})...`);
2493
3677
  }
2494
3678
  const response = await callAI(messages, TOOLS);
3679
+ console.log('response from AI', JSON.stringify(response, null, 2));
2495
3680
  if (response.content) {
2496
3681
  const content = response.content;
2497
3682
  // Only show AI message if it's making excuses (for debugging), otherwise skip
@@ -2510,7 +3695,7 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2510
3695
  console.log('\nโš ๏ธ AI is making excuses! Forcing it to use tools...');
2511
3696
  messages.push({
2512
3697
  role: 'user',
2513
- content: 'STOP making excuses! You CAN use the tools. Use 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.'
2514
3699
  });
2515
3700
  continue;
2516
3701
  }
@@ -2531,42 +3716,35 @@ START NOW by calling the analyze_file_ast tool with the source file path.`
2531
3716
  if (!testFileWritten) {
2532
3717
  messages.push({
2533
3718
  role: 'user',
2534
- content: testFileExists
2535
- ? `๐Ÿšจ STOP TALKING! The test file EXISTS at ${testFilePath}!
2536
-
2537
- Call replace_function_tests tool NOW for EACH function: ${functionNames.join(', ')}
3719
+ content: `๐Ÿšจ STOP TALKING! Use upsert_function_tests tool NOW for: ${functionName}
2538
3720
 
2539
3721
  Example:
2540
- replace_function_tests({
3722
+ upsert_function_tests({
2541
3723
  test_file_path: "${testFilePath}",
2542
- function_name: "${functionNames[0]}",
2543
- new_test_content: "describe('${functionNames[0]}', () => { test('should...', () => { ... }) })"
3724
+ function_name: "${functionName}",
3725
+ new_test_content: "describe('${functionName}', () => { test('should...', () => { ... }) })"
2544
3726
  })
2545
3727
 
2546
- DO NOT use write_test_file! It will DELETE all other tests!`
2547
- : `Use write_test_file tool NOW with complete test code for: ${functionNames.join(', ')}`
3728
+ This works for both NEW and EXISTING test files!`
2548
3729
  });
2549
3730
  }
2550
3731
  else {
2551
3732
  messages.push({
2552
3733
  role: 'user',
2553
- content: testFileExists
2554
- ? `STOP talking and USE TOOLS!
3734
+ content: `STOP talking and USE TOOLS NOW!
2555
3735
 
2556
- โœ… PRIMARY METHOD: Fix using line-based editing:
2557
- 1. read_file to see test file with line numbers
2558
- 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
2559
3741
 
2560
- โš ๏ธ SECONDARY: Use replace_function_tests for function-level updates
2561
- โŒ NEVER: Use write_test_file (will delete all other tests!)
2562
-
2563
- Start NOW with read_file!`
2564
- : `STOP talking and USE TOOLS!
2565
-
2566
- - If test file doesn't exist: write_test_file
2567
- - If test file exists: read_file + line-based editing tools
3742
+ ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding imports/mocks
3743
+ - insert_at_position({ position: 'after_imports', content: "jest.mock('../module');" })
3744
+
3745
+ โš ๏ธ SECONDARY: Use upsert_function_tests for function-level rewrites
2568
3746
 
2569
- Act NOW!`
3747
+ Start NOW with search_replace_block or insert_at_position!`
2570
3748
  });
2571
3749
  }
2572
3750
  continue;
@@ -2596,15 +3774,14 @@ Act NOW!`
2596
3774
  sameErrorCount++;
2597
3775
  console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
2598
3776
  if (sameErrorCount >= 3) {
2599
- console.log('\n๐Ÿšจ Same error repeated 3+ times! Likely a legitimate source code issue.');
3777
+ console.log('\n๐Ÿšจ Same error repeated 3+ times! ');
2600
3778
  messages.push({
2601
3779
  role: 'user',
2602
3780
  content: `The same test error has occurred ${sameErrorCount} times in a row!
2603
3781
 
2604
- This suggests the failure is LEGITIMATE (source code bug), not a test issue.
2605
3782
 
2606
3783
  If this is a source code bug: Call report_legitimate_failure tool NOW.
2607
- If this is still fixable: Make ONE final attempt to fix it.`
3784
+ If this is still fixable: Make focused attempt to fix it.`
2608
3785
  });
2609
3786
  }
2610
3787
  }
@@ -2614,32 +3791,16 @@ If this is still fixable: Make ONE final attempt to fix it.`
2614
3791
  }
2615
3792
  }
2616
3793
  // Track if test file was written
2617
- if (toolCall.name === 'write_test_file' || toolCall.name === 'replace_function_tests') {
3794
+ if (toolCall.name === 'upsert_function_tests') {
2618
3795
  if (result.success) {
2619
3796
  testFileWritten = true;
2620
3797
  console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
3798
+ messages.push({
3799
+ role: 'user',
3800
+ content: `Test files are written successfully. Please use run_tests tool to verify the tests. If the tests fail, please make focused attempts to fix the tests using the tools available.`
3801
+ });
2621
3802
  }
2622
3803
  }
2623
- // Detect if AI incorrectly used write_test_file when file exists
2624
- if (toolCall.name === 'write_test_file' && testFileExists) {
2625
- console.log('\nโš ๏ธ WARNING: AI used write_test_file on existing file! This overwrites all other tests!');
2626
- messages.push({
2627
- role: 'user',
2628
- content: `โŒ CRITICAL ERROR: You used write_test_file but the test file ALREADY EXISTS!
2629
-
2630
- This OVERWROTE the entire file and DELETED all other tests! This is WRONG!
2631
-
2632
- You MUST use replace_function_tests instead. For future fixes, call it for EACH function:
2633
-
2634
- ${functionNames.map(fname => `replace_function_tests({
2635
- test_file_path: "${testFilePath}",
2636
- function_name: "${fname}",
2637
- new_test_content: "describe('${fname}', () => { /* your tests */ })"
2638
- })`).join('\n\n')}
2639
-
2640
- DO NOT use write_test_file when the file exists! Always use replace_function_tests!`
2641
- });
2642
- }
2643
3804
  }
2644
3805
  // Add tool results to conversation based on provider
2645
3806
  if (CONFIG.aiProvider === 'claude') {
@@ -2719,6 +3880,224 @@ DO NOT use write_test_file when the file exists! Always use replace_function_tes
2719
3880
  console.log('\n๐Ÿ“‹ Test file updated with legitimate failures documented.');
2720
3881
  console.log(' These failures indicate bugs in the source code that need to be fixed.');
2721
3882
  }
3883
+ // Return true if tests passed, false if legitimate failure reported
3884
+ // Get the LAST test run result (not the first) to check final status
3885
+ const testRuns = allToolResults.filter(tr => tr.name === 'run_tests');
3886
+ const lastTestRun = testRuns.length > 0 ? testRuns[testRuns.length - 1] : null;
3887
+ return !legitimateFailureReported && (lastTestRun?.result?.passed || false);
3888
+ }
3889
+ /** [Not useful, introduce side-effect]
3890
+ * Validate and fix the complete test file after all functions are processed
3891
+ * Runs full test suite and fixes file-level issues (mock pollution, imports, etc)
3892
+ */
3893
+ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames) {
3894
+ console.log(`\n${'='.repeat(80)}`);
3895
+ console.log(`๐Ÿ” FINAL VALIDATION: Running complete test suite`);
3896
+ console.log(`${'='.repeat(80)}\n`);
3897
+ // Run tests for entire file (no function filter)
3898
+ const testResult = runTests(testFilePath);
3899
+ if (testResult.passed) {
3900
+ console.log(`\nโœ… Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
3901
+ return;
3902
+ }
3903
+ // Tests failed - give AI a chance to fix file-level issues
3904
+ console.log(`\nโš ๏ธ Complete test suite has failures. Analyzing for file-level issues...\n`);
3905
+ const messages = [
3906
+ {
3907
+ role: 'user',
3908
+ content: `You are a senior software engineer fixing file-level integration issues in a Jest test file.
3909
+
3910
+ Source file: ${sourceFile}
3911
+ Test file: ${testFilePath}
3912
+ Functions tested: ${functionNames.join(', ')}
3913
+
3914
+ CONTEXT:
3915
+ - All ${functionNames.length} functions have been tested individually and passed their own tests
3916
+ - However, when running the COMPLETE test suite together, some tests are failing
3917
+ - This suggests FILE-LEVEL issues like:
3918
+ * Mock pollution between test suites
3919
+ * Shared state not being cleaned up
3920
+ * Import/mock ordering issues
3921
+ * beforeEach/afterEach missing or incorrect
3922
+ * Mock implementations interfering with each other
3923
+
3924
+ TEST OUTPUT:
3925
+ ${testResult.output}
3926
+
3927
+ YOUR TASK:
3928
+ 1. Use get_file_preamble to see current imports, mocks, and setup blocks
3929
+ 2. Identify file-level issues (NOT individual function logic issues)
3930
+ 3. Fix using search_replace_block or insert_at_position tools
3931
+ 4. Run tests again with run_tests tool
3932
+ 5. Repeat until tests pass OR you determine failures are legitimate source code bugs
3933
+
3934
+ COMMON FILE-LEVEL ISSUES TO CHECK:
3935
+ - โŒ Missing jest.clearAllMocks() in beforeEach
3936
+ - โŒ Mocks not being reset between test suites
3937
+ - โŒ Module requires inside describe blocks (should use let + beforeEach)
3938
+ - โŒ Missing virtual:true for config/database/models mocks
3939
+ - โŒ beforeEach hooks not clearing all shared state
3940
+ - โŒ Test suites depending on execution order
3941
+
3942
+ FIXABLE ISSUES (you should fix):
3943
+ - Mock pollution between test suites
3944
+ - Missing cleanup in beforeEach/afterEach
3945
+ - Incorrect mock setup at file level
3946
+ - Import ordering issues
3947
+ - Missing jest.clearAllMocks()
3948
+
3949
+ LEGITIMATE FAILURES (report these):
3950
+ - Source code bugs causing actual logic errors
3951
+ - Missing null checks in source code
3952
+ - Wrong return types from source functions
3953
+ - Use report_legitimate_failure tool for these
3954
+
3955
+ CRITICAL RULES:
3956
+ 1. DO NOT change individual test logic (they passed individually!)
3957
+ 2. Focus ONLY on file-level integration issues
3958
+ 3. Use search_replace_block to fix specific sections
3959
+ 4. Preserve all existing test cases
3960
+ 5. If failures are due to source code bugs, call report_legitimate_failure and STOP
3961
+
3962
+ START by calling get_file_preamble to understand current file structure.`
3963
+ }
3964
+ ];
3965
+ let iterations = 0;
3966
+ const maxIterations = 200; // Limit iterations for file-level fixes
3967
+ let allToolResults = [];
3968
+ let legitimateFailureReported = false;
3969
+ while (iterations < maxIterations) {
3970
+ iterations++;
3971
+ console.log(`\n๐Ÿ”ง Fixing attempt ${iterations}/${maxIterations}...`);
3972
+ const response = await callAI(messages, TOOLS);
3973
+ if (response.content) {
3974
+ messages.push({ role: 'assistant', content: response.content });
3975
+ }
3976
+ if (!response.toolCalls || response.toolCalls.length === 0) {
3977
+ // AI stopped without fixing - check if tests pass now
3978
+ const finalTest = runTests(testFilePath);
3979
+ if (finalTest.passed) {
3980
+ console.log('\nโœ… Complete test suite now passes!');
3981
+ return;
3982
+ }
3983
+ console.log('\nโš ๏ธ AI stopped but tests still failing. Prompting to continue...');
3984
+ messages.push({
3985
+ role: 'user',
3986
+ content: 'Tests are still failing! Use tools to fix or call report_legitimate_failure if these are source code bugs.'
3987
+ });
3988
+ continue;
3989
+ }
3990
+ // Execute tool calls
3991
+ const toolResults = [];
3992
+ for (const toolCall of response.toolCalls) {
3993
+ const result = await executeTool(toolCall.name, toolCall.input);
3994
+ const toolResult = { id: toolCall.id, name: toolCall.name, result };
3995
+ toolResults.push(toolResult);
3996
+ allToolResults.push(toolResult);
3997
+ // Check if legitimate failure reported
3998
+ if (toolCall.name === 'report_legitimate_failure' && result.success) {
3999
+ legitimateFailureReported = true;
4000
+ console.log('\nโœ… Legitimate failures reported. Stopping fixes.');
4001
+ break;
4002
+ }
4003
+ // Check if tests passed
4004
+ if (toolCall.name === 'run_tests' && result.passed) {
4005
+ console.log('\n๐ŸŽ‰ Complete test suite now passes!');
4006
+ return;
4007
+ }
4008
+ }
4009
+ if (legitimateFailureReported) {
4010
+ break;
4011
+ }
4012
+ // Add tool results to conversation
4013
+ if (CONFIG.aiProvider === 'claude') {
4014
+ messages.push({
4015
+ role: 'assistant',
4016
+ content: response.toolCalls.map(tc => ({
4017
+ type: 'tool_use',
4018
+ id: tc.id,
4019
+ name: tc.name,
4020
+ input: tc.input
4021
+ }))
4022
+ });
4023
+ messages.push({
4024
+ role: 'user',
4025
+ content: toolResults.map(tr => ({
4026
+ type: 'tool_result',
4027
+ tool_use_id: tr.id,
4028
+ content: JSON.stringify(tr.result)
4029
+ }))
4030
+ });
4031
+ }
4032
+ else if (CONFIG.aiProvider === 'openai') {
4033
+ messages.push({
4034
+ role: 'assistant',
4035
+ tool_calls: response.toolCalls.map(tc => ({
4036
+ id: tc.id,
4037
+ type: 'function',
4038
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
4039
+ }))
4040
+ });
4041
+ for (const tr of toolResults) {
4042
+ messages.push({
4043
+ role: 'tool',
4044
+ tool_call_id: tr.id,
4045
+ content: JSON.stringify(tr.result)
4046
+ });
4047
+ }
4048
+ }
4049
+ else {
4050
+ // Gemini
4051
+ for (const toolCall of response.toolCalls) {
4052
+ messages.push({
4053
+ role: 'model',
4054
+ functionCall: { name: toolCall.name, args: toolCall.input }
4055
+ });
4056
+ const result = toolResults.find(tr => tr.name === toolCall.name);
4057
+ messages.push({
4058
+ role: 'user',
4059
+ functionResponse: { name: toolCall.name, response: result?.result }
4060
+ });
4061
+ }
4062
+ }
4063
+ }
4064
+ if (iterations >= maxIterations) {
4065
+ console.log(`\nโš ๏ธ Reached max iterations (${maxIterations}) for file-level fixes.`);
4066
+ console.log(' Some test failures may remain. Check test output above.');
4067
+ }
4068
+ if (legitimateFailureReported) {
4069
+ console.log('\n๐Ÿ“‹ Test file has legitimate failures due to source code bugs.');
4070
+ }
4071
+ }
4072
+ /**
4073
+ * Generate tests for multiple functions, one at a time
4074
+ */
4075
+ async function generateTestsForFunctions(sourceFile, functionNames) {
4076
+ console.log(`\n๐Ÿ“ Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
4077
+ const testFilePath = getTestFilePath(sourceFile);
4078
+ let testFileExists = fsSync.existsSync(testFilePath);
4079
+ // Process each function one at a time
4080
+ for (let i = 0; i < functionNames.length; i++) {
4081
+ const functionName = functionNames[i];
4082
+ console.log(`\n${'='.repeat(80)}`);
4083
+ console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
4084
+ console.log(`${'='.repeat(80)}\n`);
4085
+ const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
4086
+ // After first function completes, test file will exist for subsequent functions
4087
+ testFileExists = true;
4088
+ if (passed) {
4089
+ console.log(`\nโœ… Function '${functionName}' tests completed successfully!`);
4090
+ }
4091
+ else {
4092
+ console.log(`\nโš ๏ธ Function '${functionName}' completed with issues. Continuing to next function...`);
4093
+ }
4094
+ await new Promise(resolve => setTimeout(resolve, 5000));
4095
+ }
4096
+ console.log(`\n${'='.repeat(80)}`);
4097
+ console.log(`โœ… All ${functionNames.length} function(s) processed!`);
4098
+ console.log(`${'='.repeat(80)}\n`);
4099
+ // FINAL VALIDATION: Run complete test suite and fix file-level issues
4100
+ // await validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames);
2722
4101
  return testFilePath;
2723
4102
  }
2724
4103
  async function generateTestsForFunction() {
@@ -2768,8 +4147,236 @@ async function generateTestsForFunction() {
2768
4147
  await generateTestsForFunctions(selectedFile, selectedFunctions);
2769
4148
  console.log('\nโœจ Done!');
2770
4149
  }
4150
+ /**
4151
+ * Get git diff output for both staged and unstaged changes
4152
+ * Returns the combined diff content and list of changed source files
4153
+ */
4154
+ async function getGitDiff() {
4155
+ try {
4156
+ // Check if we're in a git repository
4157
+ try {
4158
+ (0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
4159
+ }
4160
+ catch {
4161
+ throw new Error('Not a git repository');
4162
+ }
4163
+ // Get both staged and unstaged changes with context
4164
+ const stagedDiff = (0, child_process_1.execSync)('git diff --staged', { encoding: 'utf-8', stdio: 'pipe' }).toString();
4165
+ // const unstagedDiff = execSync('git diff', { encoding: 'utf-8', stdio: 'pipe' }).toString();
4166
+ // Combine both diffs
4167
+ const fullDiff = stagedDiff;
4168
+ console.log('Full diff is', fullDiff);
4169
+ if (!fullDiff.trim()) {
4170
+ return { fullDiff: '', files: [] };
4171
+ }
4172
+ // Parse diff to extract per-file diffs
4173
+ const files = [];
4174
+ const diffSections = fullDiff.split(/^diff --git /m).filter(Boolean);
4175
+ for (const section of diffSections) {
4176
+ const lines = section.split('\n');
4177
+ const firstLine = 'diff --git ' + lines[0];
4178
+ // Extract file path from diff header
4179
+ const match = firstLine.match(/b\/(.+)$/);
4180
+ if (match) {
4181
+ const filePath = match[1];
4182
+ const ext = path.extname(filePath);
4183
+ const basename = path.basename(filePath);
4184
+ // Skip test files and non-source files
4185
+ const isTestFile = basename.includes('.test.') ||
4186
+ basename.includes('.spec.') ||
4187
+ basename.includes('test.') ||
4188
+ basename.includes('spec.') ||
4189
+ filePath.includes('/__tests__/') ||
4190
+ filePath.includes('/tests/') ||
4191
+ filePath.includes(CONFIG.testDir);
4192
+ // Only include supported source files (not test files)
4193
+ if (CONFIG.extensions.includes(ext) && !isTestFile) {
4194
+ files.push({
4195
+ filePath,
4196
+ diff: 'diff --git ' + section
4197
+ });
4198
+ }
4199
+ }
4200
+ }
4201
+ return { fullDiff, files };
4202
+ }
4203
+ catch (error) {
4204
+ if (error.message === 'Not a git repository') {
4205
+ throw error;
4206
+ }
4207
+ console.error('โš ๏ธ Error reading git diff:', error.message);
4208
+ return { fullDiff: '', files: [] };
4209
+ }
4210
+ }
4211
+ /**
4212
+ * Use AI to extract changed function names from git diff
4213
+ */
4214
+ async function getChangedFunctionsFromDiff(filePath, diff) {
4215
+ try {
4216
+ // First, check if file exists
4217
+ if (!fsSync.existsSync(filePath)) {
4218
+ console.log(` โš ๏ธ File not found: ${filePath}`);
4219
+ return [];
4220
+ }
4221
+ // First, get all exported functions from the file
4222
+ console.log(` ๐Ÿ“ Analyzing file: ${filePath}`);
4223
+ const result = analyzeFileAST(filePath);
4224
+ console.log(` ๐Ÿ“Š AST Analysis result:`, JSON.stringify({
4225
+ success: result.success,
4226
+ totalFunctions: result.analysis?.functions?.length || 0,
4227
+ error: result.error || 'none'
4228
+ }, null, 2));
4229
+ if (!result.success || !result.analysis || !result.analysis.functions) {
4230
+ console.log(` โš ๏ธ Failed to analyze file: ${result.error || 'unknown error'}`);
4231
+ return [];
4232
+ }
4233
+ // Filter to only exported functions
4234
+ const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
4235
+ console.log(` ๐Ÿ“ฆ Found ${exportedFunctions.length} exported function(s):`, exportedFunctions.map((f) => 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
+ }
2771
4375
  async function main() {
2772
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');
2773
4380
  // Load configuration from codeguard.json
2774
4381
  try {
2775
4382
  CONFIG = (0, config_1.loadConfig)();
@@ -2792,6 +4399,22 @@ async function main() {
2792
4399
  console.error('npm install @babel/parser @babel/traverse ts-node\n');
2793
4400
  process.exit(1);
2794
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
+ }
2795
4418
  // Optional: Codebase Indexing
2796
4419
  globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
2797
4420
  const hasExistingIndex = globalIndexer.hasIndex();