codeguard-testgen 1.0.8 → 1.0.10

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
@@ -33,6 +33,7 @@ exports.insertAtPosition = insertAtPosition;
33
33
  exports.deleteLines = deleteLines;
34
34
  exports.insertLines = insertLines;
35
35
  exports.replaceLines = replaceLines;
36
+ exports.writeReview = writeReview;
36
37
  const fs = require("fs/promises");
37
38
  const fsSync = require("fs");
38
39
  const path = require("path");
@@ -52,6 +53,8 @@ const fuzzyMatcher_1 = require("./fuzzyMatcher");
52
53
  let CONFIG;
53
54
  // Global indexer instance (optional - only initialized if user chooses to index)
54
55
  let globalIndexer = null;
56
+ // Global variable to track expected test file path (to prevent AI from creating per-function files)
57
+ let EXPECTED_TEST_FILE_PATH = null;
55
58
  // AI Provider configurations - models will be set from CONFIG
56
59
  function getAIProviders() {
57
60
  return {
@@ -397,8 +400,28 @@ const TOOLS = [
397
400
  required: ['file_path', 'content']
398
401
  }
399
402
  },
403
+ {
404
+ name: 'write_review',
405
+ description: 'Write code review findings to a markdown file in the reviews/ directory. Use this to output your comprehensive code review.',
406
+ input_schema: {
407
+ type: 'object',
408
+ properties: {
409
+ file_path: {
410
+ type: 'string',
411
+ description: 'The path to the review file (e.g., "reviews/index.review.md"). Should be in reviews/ directory with .review.md extension.'
412
+ },
413
+ review_content: {
414
+ type: 'string',
415
+ description: 'The complete markdown content of the code review including summary, findings by category (Code Quality, Bugs, Performance, Security), severity levels, and recommendations.'
416
+ }
417
+ },
418
+ required: ['file_path', 'review_content']
419
+ }
420
+ },
400
421
  ];
401
422
  exports.TOOLS = TOOLS;
423
+ // Filtered tools for test generation (excludes write_review)
424
+ const TOOLS_FOR_TEST_GENERATION = TOOLS.filter(tool => tool.name !== 'write_review');
402
425
  // AST Parsing utilities
403
426
  function parseFileToAST(filePath, content) {
404
427
  const ext = path.extname(filePath);
@@ -1903,7 +1926,8 @@ function runTests(testFilePath, functionNames) {
1903
1926
  }
1904
1927
  const output = (0, child_process_1.execSync)(command, {
1905
1928
  encoding: 'utf-8',
1906
- stdio: 'pipe'
1929
+ stdio: 'pipe',
1930
+ timeout: 10000
1907
1931
  });
1908
1932
  console.log(` Test run output: ${output}`);
1909
1933
  return {
@@ -1914,8 +1938,43 @@ function runTests(testFilePath, functionNames) {
1914
1938
  };
1915
1939
  }
1916
1940
  catch (error) {
1917
- console.log(` Test run error: ${error.message}`);
1918
- console.log(`output sent to ai: ${error.stdout + error.stderr}`);
1941
+ // console.log(` Test run error: ${error.message}`);
1942
+ // console.log(`output sent to ai: ${error.stdout + error.stderr}`);
1943
+ return {
1944
+ success: false,
1945
+ output: error.stdout + error.stderr,
1946
+ passed: false,
1947
+ error: error.message
1948
+ };
1949
+ }
1950
+ }
1951
+ /**
1952
+ * Run specific tests in isolation to detect if failure is due to pollution or regression
1953
+ * Used by smartValidateTestSuite to differentiate between test infrastructure issues and source code bugs
1954
+ */
1955
+ function runTestsIsolated(testFilePath, specificTestNames) {
1956
+ try {
1957
+ // Build Jest command with specific test name filter
1958
+ let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
1959
+ if (specificTestNames && specificTestNames.length > 0) {
1960
+ // Escape special regex characters in test names
1961
+ const escapedNames = specificTestNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1962
+ const pattern = escapedNames.join('|');
1963
+ command += ` -t "${pattern}"`;
1964
+ }
1965
+ const output = (0, child_process_1.execSync)(command, {
1966
+ encoding: 'utf-8',
1967
+ stdio: 'pipe',
1968
+ timeout: 10000
1969
+ });
1970
+ return {
1971
+ success: true,
1972
+ output,
1973
+ passed: true,
1974
+ command
1975
+ };
1976
+ }
1977
+ catch (error) {
1919
1978
  return {
1920
1979
  success: false,
1921
1980
  output: error.stdout + error.stderr,
@@ -1924,6 +1983,43 @@ function runTests(testFilePath, functionNames) {
1924
1983
  };
1925
1984
  }
1926
1985
  }
1986
+ /**
1987
+ * Parse Jest output to extract names of failing tests
1988
+ * Returns array of test names that failed
1989
+ */
1990
+ function parseFailingTestNames(jestOutput) {
1991
+ const failingTests = [];
1992
+ // Jest output patterns for failing tests:
1993
+ // ● describe block › test name
1994
+ // or: FAIL path/to/test.ts
1995
+ // ✕ test name (XXms)
1996
+ const lines = jestOutput.split('\n');
1997
+ for (let i = 0; i < lines.length; i++) {
1998
+ const line = lines[i];
1999
+ // Pattern 1: ● describe block › test name
2000
+ const bulletMatch = line.match(/^\s*●\s+(.+?)\s+›\s+(.+?)$/);
2001
+ if (bulletMatch) {
2002
+ const testName = bulletMatch[2].trim();
2003
+ failingTests.push(testName);
2004
+ continue;
2005
+ }
2006
+ // Pattern 2: ✕ test name
2007
+ const xMatch = line.match(/^\s*✕\s+(.+?)(?:\s+\(\d+m?s\))?$/);
2008
+ if (xMatch) {
2009
+ const testName = xMatch[1].trim();
2010
+ failingTests.push(testName);
2011
+ continue;
2012
+ }
2013
+ // Pattern 3: FAIL in summary
2014
+ const failMatch = line.match(/^\s*✓?\s*(.+?)\s+\(\d+m?s\)$/);
2015
+ if (failMatch && line.includes('✕')) {
2016
+ const testName = failMatch[1].trim();
2017
+ failingTests.push(testName);
2018
+ }
2019
+ }
2020
+ // Remove duplicates
2021
+ return [...new Set(failingTests)];
2022
+ }
1927
2023
  function listDirectory(directoryPath) {
1928
2024
  try {
1929
2025
  if (!fsSync.existsSync(directoryPath)) {
@@ -2372,6 +2468,31 @@ async function insertAtPosition(filePath, content, position, afterMarker) {
2372
2468
  return { success: false, error: error.message };
2373
2469
  }
2374
2470
  }
2471
+ /**
2472
+ * Write code review findings to a markdown file in the reviews/ directory
2473
+ */
2474
+ async function writeReview(filePath, reviewContent) {
2475
+ try {
2476
+ // Ensure the reviews directory exists
2477
+ const reviewsDir = path.dirname(filePath);
2478
+ if (!fsSync.existsSync(reviewsDir)) {
2479
+ await fs.mkdir(reviewsDir, { recursive: true });
2480
+ }
2481
+ // Write the review file
2482
+ await fs.writeFile(filePath, reviewContent, 'utf-8');
2483
+ return {
2484
+ success: true,
2485
+ message: `Review written to ${filePath}`,
2486
+ filePath: filePath
2487
+ };
2488
+ }
2489
+ catch (error) {
2490
+ return {
2491
+ success: false,
2492
+ error: `Failed to write review: ${error.message}`
2493
+ };
2494
+ }
2495
+ }
2375
2496
  // User-friendly messages for each tool
2376
2497
  const TOOL_MESSAGES = {
2377
2498
  'read_file': '📖 Reading source file',
@@ -2391,6 +2512,7 @@ const TOOL_MESSAGES = {
2391
2512
  'report_legitimate_failure': '⚠️ Reporting legitimate test failures',
2392
2513
  'search_replace_block': '🔍🔄 Searching and replacing code block',
2393
2514
  'insert_at_position': '➕ Inserting content at position',
2515
+ 'write_review': '📝 Writing code review',
2394
2516
  };
2395
2517
  // Tool execution router
2396
2518
  async function executeTool(toolName, args) {
@@ -2471,6 +2593,20 @@ async function executeTool(toolName, args) {
2471
2593
  result = resolveImportPath(args.from_file, args.import_path);
2472
2594
  break;
2473
2595
  case 'upsert_function_tests':
2596
+ // CRITICAL VALIDATION: Prevent AI from creating per-function test files
2597
+ if (EXPECTED_TEST_FILE_PATH && args.test_file_path !== EXPECTED_TEST_FILE_PATH) {
2598
+ // Normalize paths for comparison (handle different path separators)
2599
+ const normalizedExpected = path.normalize(EXPECTED_TEST_FILE_PATH).replace(/\\/g, '/');
2600
+ const normalizedProvided = path.normalize(args.test_file_path).replace(/\\/g, '/');
2601
+ if (normalizedExpected !== normalizedProvided) {
2602
+ console.log(`\n⚠️ BLOCKED: AI attempted to create separate test file per function!`);
2603
+ console.log(` Expected: ${EXPECTED_TEST_FILE_PATH}`);
2604
+ console.log(` AI tried: ${args.test_file_path}`);
2605
+ console.log(` 🛡️ Enforcing single test file policy...\n`);
2606
+ // Override the test file path to the expected one
2607
+ args.test_file_path = EXPECTED_TEST_FILE_PATH;
2608
+ }
2609
+ }
2474
2610
  result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
2475
2611
  break;
2476
2612
  case 'run_tests':
@@ -2494,6 +2630,9 @@ async function executeTool(toolName, args) {
2494
2630
  case 'insert_at_position':
2495
2631
  result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
2496
2632
  break;
2633
+ case 'write_review':
2634
+ result = await writeReview(args.file_path, args.review_content);
2635
+ break;
2497
2636
  default:
2498
2637
  result = { success: false, error: `Unknown tool: ${toolName}` };
2499
2638
  }
@@ -2750,857 +2889,270 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
2750
2889
  // Main conversation loop
2751
2890
  async function generateTests(sourceFile) {
2752
2891
  console.log(`\n📝 Generating tests for: ${sourceFile}\n`);
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
- }
2892
+ // Analyze file to get all functions (with retry)
2893
+ let result = analyzeFileAST(sourceFile);
2894
+ // Retry once if failed
2895
+ if (!result.success) {
2896
+ console.log('⚠️ AST analysis failed, retrying once...');
2897
+ result = analyzeFileAST(sourceFile);
2898
+ }
2899
+ // If still failed, throw error
2900
+ if (!result.success || !result.analysis || !result.analysis.functions) {
2901
+ throw new Error(`File analysis failed. Unable to extract functions from file. Error: ${result.error || 'unknown'}`);
2902
+ }
2903
+ // Filter to only EXPORTED functions
2904
+ const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
2905
+ const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
2906
+ // Error if no exported functions
2907
+ if (functionNames.length === 0) {
2908
+ throw new Error('No exported functions found in file. Cannot generate tests.');
2909
+ }
2910
+ // Log what we found
2911
+ const totalFunctions = result.analysis.functions.length;
2912
+ const internalFunctions = totalFunctions - exportedFunctions.length;
2913
+ console.log(`✅ Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
2914
+ if (internalFunctions > 0) {
2915
+ console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
2916
+ }
2917
+ // Always use function-by-function generation
2918
+ return await generateTestsForFunctions(sourceFile, functionNames);
2919
+ }
2920
+ // Interactive CLI
2921
+ async function promptUser(question) {
2922
+ const rl = readline.createInterface({
2923
+ input: process.stdin,
2924
+ output: process.stdout
2925
+ });
2926
+ return new Promise(resolve => {
2927
+ rl.question(question, answer => {
2928
+ rl.close();
2929
+ resolve(answer);
2930
+ });
2931
+ });
2932
+ }
2933
+ // Get all directories recursively
2934
+ async function listDirectories(dir, dirList = []) {
2935
+ const items = await fs.readdir(dir);
2936
+ for (const item of items) {
2937
+ const itemPath = path.join(dir, item);
2938
+ const stat = await fs.stat(itemPath);
2939
+ if (stat.isDirectory() && !CONFIG.excludeDirs.includes(item)) {
2940
+ dirList.push(itemPath);
2941
+ await listDirectories(itemPath, dirList);
2810
2942
  }
2811
2943
  }
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
2944
+ return dirList;
2945
+ }
2946
+ // Folder-wise test generation
2947
+ async function generateTestsForFolder() {
2948
+ console.log('\n📂 Folder-wise Test Generation\n');
2949
+ // Get all directories
2950
+ const directories = await listDirectories('.');
2951
+ if (directories.length === 0) {
2952
+ console.log('No directories found!');
2953
+ return;
2954
+ }
2955
+ console.log('Select a folder to generate tests for all files:\n');
2956
+ directories.forEach((dir, index) => {
2957
+ console.log(`${index + 1}. ${dir}`);
2958
+ });
2959
+ const choice = await promptUser('\nEnter folder number: ');
2960
+ const selectedDir = directories[parseInt(choice) - 1];
2961
+ if (!selectedDir) {
2962
+ console.log('Invalid selection!');
2963
+ return;
2964
+ }
2965
+ // Get all files in the selected directory (recursive)
2966
+ const files = await listFilesRecursive(selectedDir);
2967
+ if (files.length === 0) {
2968
+ console.log(`No source files found in ${selectedDir}!`);
2969
+ return;
2970
+ }
2971
+ console.log(`\n📝 Found ${files.length} files to process in ${selectedDir}\n`);
2972
+ // Process each file
2973
+ for (let i = 0; i < files.length; i++) {
2974
+ const file = files[i];
2975
+ const testFilePath = getTestFilePath(file);
2976
+ console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);
2977
+ // Check if test file already exists
2978
+ if (fsSync.existsSync(testFilePath)) {
2979
+ const answer = await promptUser(` Test file already exists: ${testFilePath}\n Regenerate? (y/n): `);
2980
+ if (answer.toLowerCase() !== 'y') {
2981
+ console.log(' Skipped.');
2982
+ continue;
2983
+ }
2984
+ }
2985
+ try {
2986
+ await generateTests(file);
2987
+ console.log(` ✅ Completed: ${testFilePath}`);
2988
+ }
2989
+ catch (error) {
2990
+ console.error(` ❌ Failed: ${error.message}`);
2818
2991
  }
2819
- console.log(`⚠️ Could not check file size: ${error}. Proceeding with regular generation.`);
2820
- // Falls through to regular file-wise generation below
2821
2992
  }
2822
- const testFilePath = getTestFilePath(sourceFile);
2993
+ console.log(`\n✨ Folder processing complete! Processed ${files.length} files.`);
2994
+ }
2995
+ // Function-wise test generation
2996
+ /**
2997
+ * Generate tests for a single function
2998
+ * @returns true if tests passed, false if legitimate failure reported
2999
+ */
3000
+ async function generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists) {
3001
+ // Set the expected test file path globally to prevent AI from creating per-function files
3002
+ EXPECTED_TEST_FILE_PATH = testFilePath;
2823
3003
  const messages = [
2824
3004
  {
2825
3005
  role: 'user',
2826
- content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests including edge cases for a TypeScript file.
3006
+ content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
3007
+ [Critical] Be prompt and efficient in your response. Make sure the test case file is typed and complete.
2827
3008
 
2828
- Source file: ${sourceFile}
2829
- Test file path: ${testFilePath}
3009
+ ## CONTEXT
3010
+ Test file: ${testFilePath} | Exists: ${testFileExists}
2830
3011
 
2831
- IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
3012
+ ⚠️ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
2832
3013
 
2833
- Your task (you MUST complete ALL steps):
2834
- 1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
2835
- - This provides metadata about all code structures without loading full file content
2836
- - CRITICAL: You have only 50 iterations to complete this task, so make sure you are using the tools efficiently.
2837
- - Do not over explore, use the tools to get the information you need and start generating tests.
2838
- 2. Use get_imports_ast tool to understand all dependencies
2839
- 3. For each dependency, use find_file(filePath) to locate the file and calculate_relative_path to get correct import paths for the test file
2840
- 4. For complex functions, use get_function_ast tool to get detailed information
2841
- - Returns complete function code WITH JSDoc comments
2842
- - Includes calledFunctions and calledMethods lists showing what the function calls
2843
- - Use this to fetch related helper functions if needed
2844
- - [CRITICAL]: If a function calls other functions from other files, use find_file + get_function_ast tools to locate them and check if they need to mocked, since they can be making api calls to external services.
2845
- 5. Use get_function_ast to get detailed information about the functions.
2846
- 6. For large test files (>5K lines), use get_file_preamble to see existing imports/mocks/setup blocks
2847
- - Automatically included when reading large test files
2848
- - Use before adding new test cases to avoid duplicate mocks/imports
2849
- - Particularly useful when updating existing test files with upsert_function_tests
2850
- - Captures complete multi-line mocks including complex jest.mock() statements
2851
- 7. For classes, use get_class_methods tool to extract all methods
2852
- 8. Use get_type_definitions tool to understand TypeScript types and interfaces
2853
- 9. Generate comprehensive Jest unit tests with:
2854
- - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors. Ensure tests fully mock the config module with all expected properties.
2855
- - If required:
2856
- - Mock database modules like '../database' or '../database/index' with virtual:true.
2857
- - Mock models, and any modules that access config or the database with virtual:true.
2858
- - Mock config properly with virtual:true.
2859
- - Mock isEmpty from lodash to return the expected values with virtual:true.
2860
- - Axios should be mocked with virtual:true.
2861
- - Use jest.mock() calls at the TOP of the file before any imports
2862
- - [CRITICAL]: Virtual modules should only be used for db/config/models/services/index/axios/routes files. You should not use virtual:true for any other files or helpers that exist in the source code. The actual helpers should never be mocked with virtual:true.
2863
- 9. REQUIRED: Write tests using upsert_function_tests tool for EACH function with REAL test code (NOT placeholders!)
2864
- - Call upsert_function_tests once for EACH exported function
2865
- - Ensure comprehensive mocks are included in the first function's test to set up the file
2866
- - DO NOT use ANY placeholder comments like:
2867
- * "// Mock setup", "// Assertions", "// Call function"
2868
- * "// Further tests...", "// Additional tests..."
2869
- * "// Similarly, write tests for..."
2870
- * "// Add more tests...", "// TODO", "// ..."
2871
- - Write ACTUAL working test code with real mocks, real assertions, real function calls
2872
- - Every test MUST have [MANDATORY]:
2873
- * Real setup code (mock functions, create test data)
2874
- * Real execution (call the function being tested)
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
2882
- - Write tests for EVERY exported function (minimum 3-5 tests per function)
2883
- - If source has 4 functions, test file MUST have 4 describe blocks with actual tests
2884
- - Example of COMPLETE test structure:
2885
- * Setup: Create mocks and test data
2886
- * Execute: Call the function being tested
2887
- * Assert: Use expect() to verify results
2888
- 10. REQUIRED: Run the tests using run_tests tool
2889
- 11. REQUIRED: If tests fail with import errors:
2890
- - Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
2891
- - Use calculate_relative_path tool to get correct import path
2892
- - ✅ PRIMARY METHOD: Use search_replace_block to fix imports:
2893
- * Include 3-5 lines of context around the import to change
2894
- * Example: search_replace_block({
2895
- search: "import { oldImport } from './old-path';\nimport { other } from './other';",
2896
- replace: "import { oldImport, newImport } from './correct-path';\nimport { other } from './other';"
2897
- })
2898
- - 📌 ALTERNATIVE: Use insert_at_position for adding new imports:
2899
- * insert_at_position({ position: 'after_imports', content: "import { newImport } from './path';" })
2900
- - ⚠️ AVOID: Line-based tools (deprecated, fragile)
2901
- 12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
2902
-
2903
- FIXABLE ERRORS (you should fix these):
2904
- - Wrong import paths
2905
- - Missing mocks
2906
- - Incorrect mock implementations
2907
- - Wrong assertions or test logic
2908
- - TypeScript compilation errors (syntax errors, bracket mismatches)
2909
- - Missing test setup/teardown
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
- })
2921
-
2922
- LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
2923
- - Function returns wrong type (e.g., undefined instead of object)
2924
- - Missing null/undefined checks in source code
2925
- - Logic errors in source code
2926
- - Unhandled promise rejections in source code
2927
-
2928
- 13. If errors are FIXABLE (AFTER test file is written):
2929
- - ✅ PRIMARY METHOD: Use search_replace_block (RECOMMENDED):
2930
- * Find the problematic code section
2931
- * Include 3-5 lines of context before/after to make search unique
2932
- * Replace with corrected version
2933
- * Example: search_replace_block({
2934
- file_path: "test.ts",
2935
- search: "const mock = jest.fn();\ntest('old test', () => {\n mock();",
2936
- replace: "const mock = jest.fn().mockResolvedValue({ data: 'test' });\ntest('fixed test', () => {\n mock();"
2937
- })
2938
- * Handles whitespace/indentation differences automatically!
2939
- - 📌 ALTERNATIVE: Use insert_at_position for adding mocks/imports at top:
2940
- * insert_at_position({ position: 'after_imports', content: "jest.mock('../database');" })
2941
- - ⚠️ AVOID: Line-based tools (deprecated) - they are fragile and prone to errors
2942
- - Then retry running tests
2943
- 14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
2944
- - Provide failing test names, reason, and source code issue description
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.
2947
- 15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
2948
- 16. REQUIRED: Ensure all functions are tested in the test file.
2949
- 17. CRITICAL: config and database modules must be mocked
3014
+ ---
2950
3015
 
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.
3016
+ ## EXECUTION PLAN
2960
3017
 
2961
- CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
3018
+ **Phase 1: Deep Analysis**
3019
+ \\\`\\\`\\\`
3020
+ 1. analyze_file_ast(${sourceFile}) → function metadata.
3021
+ 2. get_function_ast(${sourceFile},{functionName}) → implementation + dependencies
3022
+ 3. For each dependency:
3023
+ - Same file: get_function_ast(${sourceFile},{functionName})
3024
+ - 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
3025
+ 4. get_imports_ast → all dependencies
3026
+ 5. calculate_relative_path for each import
3027
+ 6. get_file_preamble → imports and mocks already declared in the file
3028
+ \\\`\\\`\\\`
2962
3029
 
2963
- 19. [ALWAYS FOLLOW] Write Jest test cases following these MANDATORY patterns to prevent mock pollution:
3030
+ **Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
3031
+ *Before writing tests, map the logic requirements for external calls.*
3032
+ 1. Identify every external call (e.g., \`analyticsHelper.postEvent\`).
3033
+ 2. Trace backwards: What \`if\`, \`switch\`, or \`try/catch\` block guards this call?
3034
+ 3. Identify the dependency that controls that guard.
3035
+ 4. Plan the Mock Return: Determine exactly what value the dependency must return to enter that block.
2964
3036
 
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.
3037
+ **Phase 2: Test Generation**
2970
3038
 
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
3039
+ Mock Pattern (CRITICAL - Top of file):
3040
+ \\\`\\\`\\\`typescript
3041
+ // ===== MOCKS (BEFORE IMPORTS) =====
3042
+ jest.mock('config', () => ({
3043
+ get: (key: string) => ({
3044
+ AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
3045
+ USER_DEL_SECRET: 'secret'
3046
+ })
3047
+ }), { virtual: true });
2976
3048
 
2977
- SECTION 3: BeforeEach Pattern (MANDATORY)
2978
- Pattern Structure:
3049
+ // virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
2979
3050
 
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
3051
+ jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
2985
3052
 
2986
- Example Pattern:
3053
+ // ===== IMPORTS =====
3054
+ import { functionName } from '../controller';
3055
+ import { dependencyMethod } from '../helpers/dependency';
2987
3056
 
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
3057
+ // ===== TYPED MOCKS =====
3058
+ const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
2995
3059
 
2996
- SECTION 4: What to NEVER Do
3060
+ \\\`\\\`\\\`
2997
3061
 
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
3062
+ Requirements (5+ tests minimum):
3063
+ - Happy path
3064
+ - 🔸 Edge cases (null, undefined, empty)
3065
+ - Error conditions
3066
+ - ⏱️ Async behavior
3067
+ - 🔍 API null/undefined handling
3004
3068
 
3005
- SECTION 5: Key Principles
3069
+ /**
3070
+ * Phase 3: Anti-Pollution Pattern (MANDATORY)
3071
+ */
3006
3072
 
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
3073
+ \\\`\\\`\\\`typescript
3074
+ // ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
3075
+ afterEach(() => {
3076
+ jest.restoreAllMocks(); // Automatically restores ALL spies
3077
+ });
3012
3078
 
3013
- SECTION 6: Verification Steps
3079
+ // ===== TESTS =====
3080
+ describe('functionName', () => {
3081
+ beforeEach(() => {
3082
+ jest.resetAllMocks(); // Resets ALL mocks (call history + implementations)
3083
+
3084
+ // Set fresh defaults for THIS describe block only
3085
+ mockDep1.mockResolvedValue({ status: 'success' });
3086
+ mockDep2.mockReturnValue(true);
3087
+ });
3014
3088
 
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
3089
+ test('happy path', async () => {
3090
+ mockDep1.mockResolvedValueOnce({ id: 123 }); // Override for this test only
3091
+
3092
+ const result = await functionName();
3093
+
3094
+ expect(result).toEqual({ id: 123 });
3095
+ expect(mockDep1).toHaveBeenCalledWith(expect.objectContaining({ param: 'value' }));
3096
+ });
3019
3097
 
3020
- SECTION 7: Critical Reminders
3098
+ test('error case', async () => {
3099
+ mockDep1.mockRejectedValueOnce(new Error('fail'));
3100
+ await expect(functionName()).rejects.toThrow('fail');
3101
+ });
3102
+ });
3021
3103
 
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
3104
+ // ===== INTERNAL SPIES (When testing same-file function calls) =====
3105
+ describe('functionWithInternalCalls', () => {
3106
+ let internalFnSpy: jest.SpyInstance;
3027
3107
 
3028
- Apply these patterns to ALL test files to ensure zero mock pollution between test suites and individual test cases.
3029
-
3030
- START NOW by calling the analyze_file_ast tool with the source file path.`
3031
- }
3032
- ];
3033
- let iterations = 0;
3034
- const maxIterations = 100;
3035
- let testFileWritten = false;
3036
- let allToolResults = [];
3037
- let legitimateFailureReported = false;
3038
- let lastTestError = '';
3039
- let sameErrorCount = 0;
3040
- while (iterations < maxIterations) {
3041
- iterations++;
3042
- if (iterations === 1) {
3043
- console.log(`\n🤖 AI is analyzing your code...`);
3044
- }
3045
- else if (iterations % 5 === 0) {
3046
- console.log(`\n🤖 AI is still working (step ${iterations})...`);
3047
- }
3048
- const response = await callAI(messages, TOOLS);
3049
- if (response.content) {
3050
- const content = response.content; // Store for TypeScript
3051
- // Only show AI message if it's making excuses (for debugging), otherwise skip
3052
- // Detect if AI is making excuses instead of using tools
3053
- const excusePatterns = [
3054
- /unable to proceed/i,
3055
- /cannot directly/i,
3056
- /constrained by/i,
3057
- /simulated environment/i,
3058
- /limited to providing/i,
3059
- /beyond my capabilities/i,
3060
- /can't execute/i
3061
- ];
3062
- const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
3063
- if (isMakingExcuses) {
3064
- console.log('\n⚠️ AI is making excuses! Forcing it to use tools...');
3065
- // Don't add the excuse to conversation, override with command
3066
- messages.push({
3067
- role: 'user',
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.'
3069
- });
3070
- continue;
3071
- }
3072
- messages.push({ role: 'assistant', content });
3073
- }
3074
- if (!response.toolCalls || response.toolCalls.length === 0) {
3075
- // Don't stop unless tests actually passed or legitimate failure reported
3076
- const lastTestRun = allToolResults[allToolResults.length - 1];
3077
- const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
3078
- if (legitimateFailureReported) {
3079
- console.log('\n✅ Test generation complete (with legitimate failures reported)');
3080
- break;
3081
- }
3082
- if (testFileWritten && testsActuallyPassed) {
3083
- console.log('\n✅ Test generation complete!');
3084
- break;
3085
- }
3086
- // If no tools called, prompt to continue with specific action
3087
- console.log('\n⚠️ No tool calls. Prompting AI to continue...');
3088
- if (!testFileWritten) {
3089
- messages.push({
3090
- role: 'user',
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.'
3092
- });
3093
- }
3094
- else {
3095
- messages.push({
3096
- role: 'user',
3097
- content: `STOP talking and USE TOOLS!
3098
-
3099
- If tests are failing:
3100
- - FIXABLE errors (imports, mocks, assertions):
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.
3105
- - LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
3106
-
3107
- Example: search_replace_block({ search: "old code with context...", replace: "new fixed code..." })`
3108
- });
3109
- }
3110
- continue;
3111
- }
3112
- // Execute all tool calls
3113
- const toolResults = [];
3114
- for (const toolCall of response.toolCalls) {
3115
- const result = await executeTool(toolCall.name, toolCall.input);
3116
- const toolResult = {
3117
- id: toolCall.id,
3118
- name: toolCall.name,
3119
- result
3120
- };
3121
- toolResults.push(toolResult);
3122
- allToolResults.push(toolResult);
3123
- // Track if legitimate failure was reported
3124
- if (toolCall.name === 'report_legitimate_failure' && result.success) {
3125
- legitimateFailureReported = true;
3126
- console.log('\n✅ Legitimate failure acknowledged. Stopping test fixes.');
3127
- console.log(` Recommendation: ${result.recommendation}`);
3128
- }
3129
- // Track if test file was written
3130
- if (toolCall.name === 'upsert_function_tests') {
3131
- if (result.success) {
3132
- testFileWritten = true;
3133
- console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
3134
- }
3135
- }
3136
- // Detect syntax errors from validation
3137
- if (result.syntaxError && result.location) {
3138
- console.log(`\n❌ Syntax error introduced at line ${result.location.line}!`);
3139
- messages.push({
3140
- role: 'user',
3141
- content: `🚨 SYNTAX ERROR DETECTED at line ${result.location.line}:${result.location.column}
3142
-
3143
- ${result.error}
3144
-
3145
- 💡 ${result.suggestion}
3146
-
3147
- Your last modification created invalid syntax and was ROLLED BACK automatically.
3148
-
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
3156
-
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
- })
3163
-
3164
- Start NOW by reading the file around line ${result.location.line}!`
3165
- });
3166
- }
3167
- // Detect repeated errors (suggests legitimate failure)
3168
- if (toolCall.name === 'run_tests' && !result.success) {
3169
- const errorOutput = result.output || result.error || '';
3170
- const currentError = errorOutput.substring(0, 300); // First 300 chars as signature
3171
- if (currentError === lastTestError) {
3172
- sameErrorCount++;
3173
- console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
3174
- if (sameErrorCount >= 3) {
3175
- console.log('\n🚨 Same error repeated 3+ times! ');
3176
- messages.push({
3177
- role: 'user',
3178
- content: `The same test error has occurred ${sameErrorCount} times in a row!
3179
-
3180
-
3181
- Analyze the error and determine:
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.
3184
- 2. Or is this a LEGITIMATE source code bug?
3185
-
3186
- If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
3187
- If FIXABLE: Make one more attempt to fix it.`
3188
- });
3189
- }
3190
- }
3191
- else {
3192
- lastTestError = currentError;
3193
- sameErrorCount = 1;
3194
- }
3195
- }
3196
- // Detect import path errors
3197
- if (toolCall.name === 'run_tests' && !result.success) {
3198
- const errorOutput = result.output || result.error || '';
3199
- // Check for module not found errors
3200
- const moduleNotFoundMatch = errorOutput.match(/Cannot find module ['"]([^'"]+)['"]/);
3201
- const tsModuleErrorMatch = errorOutput.match(/TS2307.*Cannot find module ['"]([^'"]+)['"]/);
3202
- if (moduleNotFoundMatch || tsModuleErrorMatch) {
3203
- const missingModule = moduleNotFoundMatch?.[1] || tsModuleErrorMatch?.[1];
3204
- console.log(`\n🔍 Import error detected: Cannot find module "${missingModule}"`);
3205
- // Extract filename from the path
3206
- const filename = missingModule?.split('/').pop();
3207
- messages.push({
3208
- role: 'user',
3209
- content: `Import path error detected! Module not found: "${missingModule}"
3210
-
3211
- ✅ FIX WITH SEARCH-REPLACE:
3212
-
3213
- Step 1: find_file tool to search for "${filename}" in the repository
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!
3219
-
3220
- Example workflow:
3221
- 1. find_file({ filename: "${filename}.ts" })
3222
- 2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found 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
- })
3228
-
3229
- Start NOW with find_file!`
3230
- });
3231
- }
3232
- // Check for database initialization errors
3233
- const isDatabaseError = /Cannot read properties of undefined.*reading|database|config|SSL|CA|HOST/i.test(errorOutput);
3234
- if (isDatabaseError) {
3235
- console.log('\n🔍 Database initialization error detected! Need to add mocks...');
3236
- messages.push({
3237
- role: 'user',
3238
- content: `The test is failing because the source file imports modules that initialize database connections.
3239
-
3240
- ✅ FIX WITH SEARCH-REPLACE:
3241
-
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
- })
3248
-
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 }"
3254
- })
3255
-
3256
- ⚠️ Mocks MUST be at the TOP before any imports!
3257
-
3258
- Start NOW with insert_at_position!`
3259
- });
3260
- }
3261
- }
3262
- }
3263
- // Add tool results to conversation based on provider
3264
- if (CONFIG.aiProvider === 'claude') {
3265
- messages.push({
3266
- role: 'assistant',
3267
- content: response.toolCalls.map(tc => ({
3268
- type: 'tool_use',
3269
- id: tc.id,
3270
- name: tc.name,
3271
- input: tc.input
3272
- }))
3273
- });
3274
- messages.push({
3275
- role: 'user',
3276
- content: toolResults.map(tr => ({
3277
- type: 'tool_result',
3278
- tool_use_id: tr.id,
3279
- content: JSON.stringify(tr.result)
3280
- }))
3281
- });
3282
- }
3283
- else if (CONFIG.aiProvider === 'openai') {
3284
- messages.push({
3285
- role: 'assistant',
3286
- tool_calls: response.toolCalls.map(tc => ({
3287
- id: tc.id,
3288
- type: 'function',
3289
- function: {
3290
- name: tc.name,
3291
- arguments: JSON.stringify(tc.input)
3292
- }
3293
- }))
3294
- });
3295
- for (const tr of toolResults) {
3296
- messages.push({
3297
- role: 'tool',
3298
- tool_call_id: tr.id,
3299
- content: JSON.stringify(tr.result)
3300
- });
3301
- }
3302
- }
3303
- else {
3304
- // Gemini - use proper function call format
3305
- for (const toolCall of response.toolCalls) {
3306
- // Add model's function call
3307
- messages.push({
3308
- role: 'model',
3309
- functionCall: {
3310
- name: toolCall.name,
3311
- args: toolCall.input
3312
- }
3313
- });
3314
- // Add user's function response
3315
- const result = toolResults.find(tr => tr.name === toolCall.name);
3316
- messages.push({
3317
- role: 'user',
3318
- functionResponse: {
3319
- name: toolCall.name,
3320
- response: result?.result
3321
- }
3322
- });
3323
- }
3324
- }
3325
- // Check if legitimate failure was reported
3326
- if (legitimateFailureReported) {
3327
- console.log('\n✅ Stopping iteration: Legitimate failure reported.');
3328
- break;
3329
- }
3330
- // Check if tests were run and passed
3331
- const testRun = toolResults.find(tr => tr.name === 'run_tests');
3332
- if (testRun?.result.passed) {
3333
- console.log('\n🎉 All tests passed!');
3334
- break;
3335
- }
3336
- }
3337
- if (iterations >= maxIterations) {
3338
- console.log('\n⚠️ Reached maximum iterations. Tests may not be complete.');
3339
- }
3340
- if (!testFileWritten) {
3341
- console.log('\n❌ WARNING: Test file was never written! The AI may not have used the tools correctly.');
3342
- console.log(' Try running again or check your API key and connectivity.');
3343
- }
3344
- else if (legitimateFailureReported) {
3345
- console.log('\n📋 Test file created with legitimate failures documented.');
3346
- console.log(' These failures indicate bugs in the source code that need to be fixed.');
3347
- }
3348
- return testFilePath;
3349
- }
3350
- // Interactive CLI
3351
- async function promptUser(question) {
3352
- const rl = readline.createInterface({
3353
- input: process.stdin,
3354
- output: process.stdout
3355
- });
3356
- return new Promise(resolve => {
3357
- rl.question(question, answer => {
3358
- rl.close();
3359
- resolve(answer);
3360
- });
3361
- });
3362
- }
3363
- // Get all directories recursively
3364
- async function listDirectories(dir, dirList = []) {
3365
- const items = await fs.readdir(dir);
3366
- for (const item of items) {
3367
- const itemPath = path.join(dir, item);
3368
- const stat = await fs.stat(itemPath);
3369
- if (stat.isDirectory() && !CONFIG.excludeDirs.includes(item)) {
3370
- dirList.push(itemPath);
3371
- await listDirectories(itemPath, dirList);
3372
- }
3373
- }
3374
- return dirList;
3375
- }
3376
- // Folder-wise test generation
3377
- async function generateTestsForFolder() {
3378
- console.log('\n📂 Folder-wise Test Generation\n');
3379
- // Get all directories
3380
- const directories = await listDirectories('.');
3381
- if (directories.length === 0) {
3382
- console.log('No directories found!');
3383
- return;
3384
- }
3385
- console.log('Select a folder to generate tests for all files:\n');
3386
- directories.forEach((dir, index) => {
3387
- console.log(`${index + 1}. ${dir}`);
3388
- });
3389
- const choice = await promptUser('\nEnter folder number: ');
3390
- const selectedDir = directories[parseInt(choice) - 1];
3391
- if (!selectedDir) {
3392
- console.log('Invalid selection!');
3393
- return;
3394
- }
3395
- // Get all files in the selected directory (recursive)
3396
- const files = await listFilesRecursive(selectedDir);
3397
- if (files.length === 0) {
3398
- console.log(`No source files found in ${selectedDir}!`);
3399
- return;
3400
- }
3401
- console.log(`\n📝 Found ${files.length} files to process in ${selectedDir}\n`);
3402
- // Process each file
3403
- for (let i = 0; i < files.length; i++) {
3404
- const file = files[i];
3405
- const testFilePath = getTestFilePath(file);
3406
- console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);
3407
- // Check if test file already exists
3408
- if (fsSync.existsSync(testFilePath)) {
3409
- const answer = await promptUser(` Test file already exists: ${testFilePath}\n Regenerate? (y/n): `);
3410
- if (answer.toLowerCase() !== 'y') {
3411
- console.log(' Skipped.');
3412
- continue;
3413
- }
3414
- }
3415
- try {
3416
- await generateTests(file);
3417
- console.log(` ✅ Completed: ${testFilePath}`);
3418
- }
3419
- catch (error) {
3420
- console.error(` ❌ Failed: ${error.message}`);
3421
- }
3422
- }
3423
- console.log(`\n✨ Folder processing complete! Processed ${files.length} files.`);
3424
- }
3425
- // Function-wise test generation
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) {
3431
- const messages = [
3432
- {
3433
- role: 'user',
3434
- content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
3435
-
3436
- ## CONTEXT
3437
- Test file: ${testFilePath} | Exists: ${testFileExists}
3438
-
3439
- ---
3440
-
3441
- ## EXECUTION PLAN
3442
-
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
- \\\`\\\`\\\`
3454
-
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.
3461
-
3462
- **Phase 2: Test Generation**
3463
-
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
- });
3108
+ beforeEach(() => {
3109
+ jest.resetAllMocks();
3110
+
3111
+ // ✅ EXCEPTION: require() needed here for spying on same module
3112
+ const controller = require('../controller');
3113
+ internalFnSpy = jest.spyOn(controller, 'internalFunction').mockResolvedValue(undefined);
3114
+ });
3115
+
3116
+ // No manual restore needed - global afterEach handles it
3565
3117
 
3566
3118
  test('calls internal function', async () => {
3567
3119
  await functionWithInternalCalls();
3568
3120
  expect(internalFnSpy).toHaveBeenCalled();
3569
3121
  });
3570
3122
  });
3123
+ \\\`\\\`\\\`
3571
3124
 
3572
- ### CRITICAL RULES:
3125
+ ### CRITICAL RULES (Prevent Mock Pollution):
3573
3126
  **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
3127
+ 1. \`jest.resetAllMocks()\` as FIRST line in every \`beforeEach()\` (not clearAllMocks)
3128
+ 2. Global \`afterEach(() => jest.restoreAllMocks())\` near top of test file
3129
+ 3. Set mock defaults in each \`describe\` block's \`beforeEach()\` independently
3130
+ 4. Override with \`mockResolvedValueOnce/mockReturnValueOnce\` in individual tests
3131
+ 5. Type all mocks: \`const mockFn = fn as jest.MockedFunction<typeof fn>\`
3132
+ 6. All \`jest.mock()\` at top before imports (use calculate_relative_path for paths)
3133
+ 7. Check for existing mocks with \`get_file_preamble\` tool before adding duplicates
3582
3134
 
3583
3135
  **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()\`
3136
+ 1. Use \`jest.clearAllMocks()\` (only clears history, not implementations) Use \`resetAllMocks()\`
3137
+ 2. Manually \`.mockReset()\` individual mocks \`resetAllMocks()\` handles all
3138
+ 3. Share mock state between \`describe\` blocks Each block sets its own defaults
3139
+ 4. Use \`require()\` except when creating spies on same module being tested
3140
+ 5. Use \`virtual:true\` for regular files → Only for: config, db, models, services (modules not in filesystem)
3141
+ 6. Forget global \`afterEach(() => jest.restoreAllMocks())\` Causes spy pollution
3142
+
3590
3143
 
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
3144
 
3597
3145
  **Phase 4: Write Tests**
3146
+ ⚠️ CRITICAL REQUIREMENT: Use EXACTLY this test file path: "${testFilePath}"
3147
+ DO NOT modify the path. DO NOT create ${functionName}.test.ts or any other variation.
3148
+
3598
3149
  → upsert_function_tests({
3599
- test_file_path: "${testFilePath}",
3150
+ test_file_path: "${testFilePath}", // ⚠️ USE THIS EXACT PATH - DO NOT CHANGE!
3600
3151
  function_name: "${functionName}",
3601
3152
  new_test_content: "describe('${functionName}', () => {...})"
3602
3153
  })
3603
3154
  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.
3155
+ All functions from the same source file MUST share the same test file.
3604
3156
 
3605
3157
 
3606
3158
 
@@ -3655,6 +3207,7 @@ This will automatically replace the existing test cases for the function with th
3655
3207
  - Ensure test independence (no pollution)
3656
3208
  - Fix test bugs, report source bugs
3657
3209
  - [CRITICAL] Each test suite should be completely self-contained and not depend on or affect any other test suite's state.
3210
+ - Test file exists: ${testFileExists} - if the test file exist, alway check the mock and imports already present in the test file, using get_file_preamble tool. Make sure you do not duplicate mocks and mocks and imports are added at correct position.
3658
3211
 
3659
3212
  **START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file.`
3660
3213
  }
@@ -3675,8 +3228,8 @@ This will automatically replace the existing test cases for the function with th
3675
3228
  else if (iterations % 5 === 0) {
3676
3229
  console.log(`\n🤖 AI is still working (step ${iterations})...`);
3677
3230
  }
3678
- const response = await callAI(messages, TOOLS);
3679
- console.log('response from AI', JSON.stringify(response, null, 2));
3231
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3232
+ // console.log('response from AI', JSON.stringify(response, null, 2));
3680
3233
  if (response.content) {
3681
3234
  const content = response.content;
3682
3235
  // Only show AI message if it's making excuses (for debugging), otherwise skip
@@ -3802,7 +3355,193 @@ If this is still fixable: Make focused attempt to fix it.`
3802
3355
  }
3803
3356
  }
3804
3357
  }
3805
- // Add tool results to conversation based on provider
3358
+ // Add tool results to conversation based on provider
3359
+ if (CONFIG.aiProvider === 'claude') {
3360
+ messages.push({
3361
+ role: 'assistant',
3362
+ content: response.toolCalls.map(tc => ({
3363
+ type: 'tool_use',
3364
+ id: tc.id,
3365
+ name: tc.name,
3366
+ input: tc.input
3367
+ }))
3368
+ });
3369
+ messages.push({
3370
+ role: 'user',
3371
+ content: toolResults.map(tr => ({
3372
+ type: 'tool_result',
3373
+ tool_use_id: tr.id,
3374
+ content: JSON.stringify(tr.result)
3375
+ }))
3376
+ });
3377
+ }
3378
+ else if (CONFIG.aiProvider === 'openai') {
3379
+ messages.push({
3380
+ role: 'assistant',
3381
+ tool_calls: response.toolCalls.map(tc => ({
3382
+ id: tc.id,
3383
+ type: 'function',
3384
+ function: {
3385
+ name: tc.name,
3386
+ arguments: JSON.stringify(tc.input)
3387
+ }
3388
+ }))
3389
+ });
3390
+ for (const tr of toolResults) {
3391
+ messages.push({
3392
+ role: 'tool',
3393
+ tool_call_id: tr.id,
3394
+ content: JSON.stringify(tr.result)
3395
+ });
3396
+ }
3397
+ }
3398
+ else {
3399
+ for (const toolCall of response.toolCalls) {
3400
+ messages.push({
3401
+ role: 'model',
3402
+ functionCall: {
3403
+ name: toolCall.name,
3404
+ args: toolCall.input
3405
+ }
3406
+ });
3407
+ const result = toolResults.find(tr => tr.name === toolCall.name);
3408
+ messages.push({
3409
+ role: 'user',
3410
+ functionResponse: {
3411
+ name: toolCall.name,
3412
+ response: result?.result
3413
+ }
3414
+ });
3415
+ }
3416
+ }
3417
+ // Check if legitimate failure was reported
3418
+ if (legitimateFailureReported) {
3419
+ console.log('\n✅ Stopping iteration: Legitimate failure reported.');
3420
+ break;
3421
+ }
3422
+ // Check if tests passed
3423
+ const testRun = toolResults.find(tr => tr.name === 'run_tests');
3424
+ if (testRun?.result.passed) {
3425
+ console.log('\n🎉 All tests passed!');
3426
+ break;
3427
+ }
3428
+ }
3429
+ if (iterations >= maxIterations) {
3430
+ console.log('\n⚠️ Reached maximum iterations. Tests may not be complete.');
3431
+ }
3432
+ if (legitimateFailureReported) {
3433
+ console.log('\n📋 Test file updated with legitimate failures documented.');
3434
+ console.log(' These failures indicate bugs in the source code that need to be fixed.');
3435
+ }
3436
+ // Clear the expected test file path (cleanup)
3437
+ EXPECTED_TEST_FILE_PATH = null;
3438
+ // Return true if tests passed, false if legitimate failure reported
3439
+ // Get the LAST test run result (not the first) to check final status
3440
+ const testRuns = allToolResults.filter(tr => tr.name === 'run_tests');
3441
+ const lastTestRun = testRuns.length > 0 ? testRuns[testRuns.length - 1] : null;
3442
+ return !legitimateFailureReported && (lastTestRun?.result?.passed || false);
3443
+ }
3444
+ /**
3445
+ * Smart validation that fixes failing tests
3446
+ * - Runs full test suite
3447
+ * - For failures, attempts to fix all failing tests using AI
3448
+ */
3449
+ async function smartValidateTestSuite(sourceFile, testFilePath, functionNames) {
3450
+ console.log(`\n${'='.repeat(80)}`);
3451
+ console.log(`🔍 VALIDATION: Running full test suite (${functionNames.length} function(s))`);
3452
+ console.log(`${'='.repeat(80)}\n`);
3453
+ // Run tests for entire file (no function filter)
3454
+ const fullSuiteResult = runTests(testFilePath);
3455
+ if (fullSuiteResult.passed) {
3456
+ console.log(`\n✅ Full test suite passed! All ${functionNames.length} function(s) working together correctly.`);
3457
+ return;
3458
+ }
3459
+ console.log(`\n⚠️ Full test suite has failures. Attempting to fix failing tests...\n`);
3460
+ // Parse failing test names from Jest output
3461
+ const failingTests = parseFailingTestNames(fullSuiteResult.output);
3462
+ if (failingTests.length === 0) {
3463
+ console.log('⚠️ Could not parse specific failing test names. Skipping detailed analysis.');
3464
+ return;
3465
+ }
3466
+ console.log(`Found ${failingTests.length} failing test(s): ${failingTests.join(', ')}\n`);
3467
+ // Attempt to fix all failing tests
3468
+ await fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteResult.output);
3469
+ }
3470
+ /**
3471
+ * Fix failing tests using AI
3472
+ * Attempts to fix all test issues including pollution, imports, mocks, etc.
3473
+ */
3474
+ async function fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteOutput) {
3475
+ const messages = [
3476
+ {
3477
+ role: 'user',
3478
+ content: `You are fixing FAILING TESTS in the test suite.
3479
+
3480
+ Source file: ${sourceFile}
3481
+ Test file: ${testFilePath}
3482
+ Functions tested: ${functionNames.join(', ')}
3483
+
3484
+ FAILING TESTS:
3485
+ ${failingTests.map(t => `- ${t}`).join('\n')}
3486
+
3487
+ Full suite output:
3488
+ ${fullSuiteOutput}
3489
+
3490
+ YOUR TASK - Fix all failing tests:
3491
+
3492
+ COMMON ISSUES TO FIX:
3493
+ - Missing jest.resetAllMocks() in beforeEach (should be first line)
3494
+ - Missing jest.restoreAllMocks() in global afterEach
3495
+ - Mock state bleeding between describe blocks
3496
+ - Module-level state issues (add jest.resetModules() if needed)
3497
+ - Shared variable contamination
3498
+ - beforeEach not resetting mocks properly
3499
+ - afterEach not cleaning up spies
3500
+ - Missing or incorrect imports
3501
+ - Mock implementation issues
3502
+ - Incorrect test assertions
3503
+ - Test logic errors
3504
+
3505
+ TOOLS TO USE:
3506
+ 1. get_file_preamble - See current setup
3507
+ 2. search_replace_block - Fix specific sections (preferred)
3508
+ 3. insert_at_position - Add missing global afterEach
3509
+ 4. run_tests - Verify fixes
3510
+
3511
+ START by calling get_file_preamble to see the current test structure.`
3512
+ }
3513
+ ];
3514
+ let iterations = 0;
3515
+ const maxIterations = 100; // Limit iterations for test fixes
3516
+ while (iterations < maxIterations) {
3517
+ iterations++;
3518
+ console.log(`\n🔧 Test fix attempt ${iterations}/${maxIterations}...`);
3519
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3520
+ if (response.content) {
3521
+ messages.push({ role: 'assistant', content: response.content });
3522
+ }
3523
+ if (!response.toolCalls || response.toolCalls.length === 0) {
3524
+ // AI stopped - check if tests pass now
3525
+ const finalTest = runTests(testFilePath);
3526
+ if (finalTest.passed) {
3527
+ console.log('\n✅ Tests fixed! Full test suite now passes.');
3528
+ return;
3529
+ }
3530
+ console.log('\n⚠️ AI stopped but tests still failing.');
3531
+ break;
3532
+ }
3533
+ // Execute tool calls
3534
+ const toolResults = [];
3535
+ for (const toolCall of response.toolCalls) {
3536
+ const result = await executeTool(toolCall.name, toolCall.input);
3537
+ toolResults.push({ id: toolCall.id, name: toolCall.name, result });
3538
+ // Check if tests passed
3539
+ if (toolCall.name === 'run_tests' && result.passed) {
3540
+ console.log('\n✅ Tests fixed! Full test suite now passes.');
3541
+ return;
3542
+ }
3543
+ }
3544
+ // Add tool results to conversation
3806
3545
  if (CONFIG.aiProvider === 'claude') {
3807
3546
  messages.push({
3808
3547
  role: 'assistant',
@@ -3828,10 +3567,7 @@ If this is still fixable: Make focused attempt to fix it.`
3828
3567
  tool_calls: response.toolCalls.map(tc => ({
3829
3568
  id: tc.id,
3830
3569
  type: 'function',
3831
- function: {
3832
- name: tc.name,
3833
- arguments: JSON.stringify(tc.input)
3834
- }
3570
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
3835
3571
  }))
3836
3572
  });
3837
3573
  for (const tr of toolResults) {
@@ -3861,30 +3597,8 @@ If this is still fixable: Make focused attempt to fix it.`
3861
3597
  });
3862
3598
  }
3863
3599
  }
3864
- // Check if legitimate failure was reported
3865
- if (legitimateFailureReported) {
3866
- console.log('\n✅ Stopping iteration: Legitimate failure reported.');
3867
- break;
3868
- }
3869
- // Check if tests passed
3870
- const testRun = toolResults.find(tr => tr.name === 'run_tests');
3871
- if (testRun?.result.passed) {
3872
- console.log('\n🎉 All tests passed!');
3873
- break;
3874
- }
3875
- }
3876
- if (iterations >= maxIterations) {
3877
- console.log('\n⚠️ Reached maximum iterations. Tests may not be complete.');
3878
- }
3879
- if (legitimateFailureReported) {
3880
- console.log('\n📋 Test file updated with legitimate failures documented.');
3881
- console.log(' These failures indicate bugs in the source code that need to be fixed.');
3882
3600
  }
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);
3601
+ console.log('\n⚠️ Could not automatically fix all failing tests. Manual review may be needed.');
3888
3602
  }
3889
3603
  /** [Not useful, introduce side-effect]
3890
3604
  * Validate and fix the complete test file after all functions are processed
@@ -3969,7 +3683,7 @@ START by calling get_file_preamble to understand current file structure.`
3969
3683
  while (iterations < maxIterations) {
3970
3684
  iterations++;
3971
3685
  console.log(`\n🔧 Fixing attempt ${iterations}/${maxIterations}...`);
3972
- const response = await callAI(messages, TOOLS);
3686
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3973
3687
  if (response.content) {
3974
3688
  messages.push({ role: 'assistant', content: response.content });
3975
3689
  }
@@ -4076,6 +3790,8 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
4076
3790
  console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
4077
3791
  const testFilePath = getTestFilePath(sourceFile);
4078
3792
  let testFileExists = fsSync.existsSync(testFilePath);
3793
+ // Read validation interval from config
3794
+ const validationInterval = CONFIG.validationInterval;
4079
3795
  // Process each function one at a time
4080
3796
  for (let i = 0; i < functionNames.length; i++) {
4081
3797
  const functionName = functionNames[i];
@@ -4091,13 +3807,22 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
4091
3807
  else {
4092
3808
  console.log(`\n⚠️ Function '${functionName}' completed with issues. Continuing to next function...`);
4093
3809
  }
3810
+ // Periodic validation checkpoint (only if validation is enabled in config)
3811
+ if (validationInterval !== undefined && validationInterval !== null) {
3812
+ const isPeriodicCheckpoint = validationInterval > 0 && (i + 1) % validationInterval === 0;
3813
+ const isFinalFunction = i === functionNames.length - 1;
3814
+ if (isPeriodicCheckpoint || isFinalFunction) {
3815
+ console.log(`\n${'─'.repeat(80)}`);
3816
+ console.log(`📊 CHECKPOINT ${i + 1}/${functionNames.length}: Running full suite validation...`);
3817
+ console.log(`${'─'.repeat(80)}`);
3818
+ await smartValidateTestSuite(sourceFile, testFilePath, functionNames.slice(0, i + 1));
3819
+ }
3820
+ }
4094
3821
  await new Promise(resolve => setTimeout(resolve, 5000));
4095
3822
  }
4096
3823
  console.log(`\n${'='.repeat(80)}`);
4097
3824
  console.log(`✅ All ${functionNames.length} function(s) processed!`);
4098
3825
  console.log(`${'='.repeat(80)}\n`);
4099
- // FINAL VALIDATION: Run complete test suite and fix file-level issues
4100
- // await validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames);
4101
3826
  return testFilePath;
4102
3827
  }
4103
3828
  async function generateTestsForFunction() {
@@ -4130,7 +3855,7 @@ async function generateTestsForFunction() {
4130
3855
  console.log('No exported functions found in the file!');
4131
3856
  return;
4132
3857
  }
4133
- console.log(`\nFound ${functions.length} exported functions:\n`);
3858
+ // console.log(`\nFound ${functions.length} exported functions:\n`);
4134
3859
  functions.forEach((func, index) => {
4135
3860
  console.log(`${index + 1}. ${func.name} (${func.type}, ${func.async ? 'async' : 'sync'})`);
4136
3861
  });
@@ -4165,7 +3890,7 @@ async function getGitDiff() {
4165
3890
  // const unstagedDiff = execSync('git diff', { encoding: 'utf-8', stdio: 'pipe' }).toString();
4166
3891
  // Combine both diffs
4167
3892
  const fullDiff = stagedDiff;
4168
- console.log('Full diff is', fullDiff);
3893
+ // console.log('Full diff is', fullDiff);
4169
3894
  if (!fullDiff.trim()) {
4170
3895
  return { fullDiff: '', files: [] };
4171
3896
  }
@@ -4219,27 +3944,27 @@ async function getChangedFunctionsFromDiff(filePath, diff) {
4219
3944
  return [];
4220
3945
  }
4221
3946
  // First, get all exported functions from the file
4222
- console.log(` 📁 Analyzing file: ${filePath}`);
3947
+ // console.log(` 📁 Analyzing file: ${filePath}`);
4223
3948
  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));
3949
+ // console.log(` 📊 AST Analysis result:`, JSON.stringify({
3950
+ // success: result.success,
3951
+ // totalFunctions: result.analysis?.functions?.length || 0,
3952
+ // error: result.error || 'none'
3953
+ // }, null, 2));
4229
3954
  if (!result.success || !result.analysis || !result.analysis.functions) {
4230
3955
  console.log(` ⚠️ Failed to analyze file: ${result.error || 'unknown error'}`);
4231
3956
  return [];
4232
3957
  }
4233
3958
  // Filter to only exported functions
4234
3959
  const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
4235
- console.log(` 📦 Found ${exportedFunctions.length} exported function(s):`, exportedFunctions.map((f) => f.name).join(', '));
3960
+ // console.log(` 📦 Found ${exportedFunctions.length} exported function(s):`, exportedFunctions.map((f: any) => f.name).join(', '));
4236
3961
  if (exportedFunctions.length === 0) {
4237
3962
  console.log(` ⚠️ No exported functions found in file`);
4238
3963
  console.log(` 💡 Tip: Make sure the file has exported functions (export const/function/async)`);
4239
3964
  return [];
4240
3965
  }
4241
3966
  const exportedFunctionNames = exportedFunctions.map((f) => f.name);
4242
- console.log(` 📦 Exported functions in file: ${exportedFunctionNames.join(', ')}`);
3967
+ // console.log(` 📦 Exported functions in file: ${exportedFunctionNames.join(', ')}`);
4243
3968
  // Ask AI to identify which functions appear in the diff
4244
3969
  const prompt = `You are analyzing a git diff to identify which exported functions have been changed or added.
4245
3970
 
@@ -4264,7 +3989,7 @@ Return ONLY the JSON array, nothing else.`;
4264
3989
  content: prompt
4265
3990
  }
4266
3991
  ];
4267
- console.log(` 🤖 Calling AI to analyze diff...`);
3992
+ // console.log(` 🤖 Calling AI to analyze diff...`);
4268
3993
  // Call AI without tools - just need text response
4269
3994
  const response = await callAI(messages, [], CONFIG.aiProvider);
4270
3995
  if (!response.content) {
@@ -4273,7 +3998,7 @@ Return ONLY the JSON array, nothing else.`;
4273
3998
  }
4274
3999
  // Parse AI response to extract function names
4275
4000
  const content = response.content.trim();
4276
- console.log(` 🤖 AI response: ${content}`);
4001
+ // console.log(` 🤖 AI response: ${content}`);
4277
4002
  // Try to extract JSON array from response
4278
4003
  const jsonMatch = content.match(/\[.*\]/s);
4279
4004
  if (jsonMatch) {
@@ -4282,7 +4007,7 @@ Return ONLY the JSON array, nothing else.`;
4282
4007
  if (Array.isArray(functionNames)) {
4283
4008
  // Filter to only include functions that actually exist in the file
4284
4009
  const filtered = functionNames.filter(name => exportedFunctionNames.includes(name));
4285
- console.log(` ✅ Extracted ${filtered.length} function(s) from AI response: ${filtered.join(', ')}`);
4010
+ // console.log(` ✅ Extracted ${filtered.length} function(s) from AI response: ${filtered.join(', ')}`);
4286
4011
  return filtered;
4287
4012
  }
4288
4013
  }
@@ -4307,10 +4032,7 @@ async function autoGenerateTests() {
4307
4032
  console.log('🔍 Scanning git changes...\n');
4308
4033
  try {
4309
4034
  // Get all changes from git diff
4310
- console.log('Getting git diff...');
4311
4035
  const { fullDiff, files } = await getGitDiff();
4312
- console.log('Full diff is', fullDiff);
4313
- console.log('Files are', files);
4314
4036
  if (files.length === 0) {
4315
4037
  console.log('✅ No changes detected in source files.');
4316
4038
  console.log(' (Only staged and unstaged changes are checked)');
@@ -4329,10 +4051,10 @@ async function autoGenerateTests() {
4329
4051
  continue;
4330
4052
  }
4331
4053
  console.log(`\n🔄 Processing: ${filePath}`);
4332
- console.log(` Analyzing diff with AI...`);
4054
+ console.log(` 🤖 Analyzing diff with AI...`);
4333
4055
  // Use AI to extract changed function names from diff
4334
4056
  const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4335
- console.log('Changed functions are', changedFunctions);
4057
+ // console.log('Changed functions are', changedFunctions);
4336
4058
  if (changedFunctions.length === 0) {
4337
4059
  console.log(` ⏭️ No exported functions changed`);
4338
4060
  continue;
@@ -4372,11 +4094,387 @@ async function autoGenerateTests() {
4372
4094
  throw error;
4373
4095
  }
4374
4096
  }
4097
+ /**
4098
+ * Review code changes for quality, bugs, performance, and security issues
4099
+ */
4100
+ async function reviewChangedFiles() {
4101
+ console.log('🔍 Scanning git changes for review...\n');
4102
+ try {
4103
+ // Get all changes from git diff
4104
+ const { fullDiff, files } = await getGitDiff();
4105
+ if (files.length === 0) {
4106
+ console.log('✅ No changes detected in source files.');
4107
+ console.log(' (Only staged and unstaged changes are checked)');
4108
+ return;
4109
+ }
4110
+ console.log(`📝 Found changes in ${files.length} file(s) to review\n`);
4111
+ let processedFiles = 0;
4112
+ let errorFiles = 0;
4113
+ // Process each changed file
4114
+ for (const fileInfo of files) {
4115
+ const { filePath, diff } = fileInfo;
4116
+ // Check if file exists
4117
+ if (!fsSync.existsSync(filePath)) {
4118
+ console.log(`⏭️ Skipping ${filePath} (file not found)`);
4119
+ continue;
4120
+ }
4121
+ console.log(`\n🔄 Reviewing: ${filePath}`);
4122
+ console.log(` 🤖 Analyzing changes with AI...`);
4123
+ // Use AI to extract changed function names from diff
4124
+ const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4125
+ if (changedFunctions.length === 0) {
4126
+ console.log(` ⏭️ No exported functions changed`);
4127
+ continue;
4128
+ }
4129
+ console.log(` 📦 Changed functions: ${changedFunctions.join(', ')}`);
4130
+ try {
4131
+ // Generate code review for the file
4132
+ await generateCodeReview(filePath, diff, changedFunctions);
4133
+ processedFiles++;
4134
+ console.log(` ✅ Review completed`);
4135
+ }
4136
+ catch (error) {
4137
+ errorFiles++;
4138
+ console.error(` ❌ Error during review: ${error.message}`);
4139
+ // Continue with next file
4140
+ }
4141
+ }
4142
+ // Summary
4143
+ console.log('\n' + '='.repeat(60));
4144
+ console.log('📊 Code Review Summary');
4145
+ console.log('='.repeat(60));
4146
+ console.log(`✅ Successfully reviewed: ${processedFiles} file(s)`);
4147
+ if (errorFiles > 0) {
4148
+ console.log(`⚠️ Failed: ${errorFiles} file(s)`);
4149
+ }
4150
+ console.log(`📁 Reviews saved to: reviews/ directory`);
4151
+ console.log('='.repeat(60));
4152
+ console.log('\n✨ Done!');
4153
+ }
4154
+ catch (error) {
4155
+ if (error.message === 'Not a git repository') {
4156
+ console.error('❌ Error: Not a git repository');
4157
+ console.error(' Review mode requires git to detect changes.');
4158
+ process.exit(1);
4159
+ }
4160
+ throw error;
4161
+ }
4162
+ }
4163
+ /**
4164
+ * Generate comprehensive code review for a specific file
4165
+ */
4166
+ async function generateCodeReview(filePath, diff, changedFunctions) {
4167
+ // Prepare review file path
4168
+ const fileName = path.basename(filePath);
4169
+ const reviewFileName = fileName.replace(/\.(ts|tsx|js|jsx)$/, '.review.md');
4170
+ const reviewFilePath = path.join('reviews', reviewFileName);
4171
+ // Create comprehensive review prompt
4172
+ const reviewPrompt = `You are a senior software engineer conducting a thorough code review. Review the changed functions in ${filePath} for code quality, potential bugs, performance issues, and security vulnerabilities.
4173
+
4174
+ ## CONTEXT
4175
+ File: ${filePath}
4176
+ Changed Functions: ${changedFunctions.join(', ')}
4177
+
4178
+ ## GIT DIFF
4179
+ \`\`\`diff
4180
+ ${diff}
4181
+ \`\`\`
4182
+
4183
+ ## YOUR TASK
4184
+ Conduct a comprehensive code review using the available tools to understand the full context. You MUST use tools to analyze the code thoroughly before providing your review.
4185
+
4186
+ ## EXECUTION PLAN
4187
+
4188
+ **Phase 1: Deep Code Analysis (MANDATORY)**
4189
+ 1. analyze_file_ast(${filePath}) → Get file structure and all functions
4190
+ 2. For each changed function (${changedFunctions.join(', ')}):
4191
+ - get_function_ast(${filePath}, {functionName}) → Get implementation details
4192
+ 3. get_imports_ast(${filePath}) → Understand dependencies
4193
+ 4. get_type_definitions(${filePath}) → Review type safety
4194
+ 5. For key dependencies, use find_file() and get_function_ast() to understand their behavior
4195
+
4196
+ **Phase 2: Review Analysis**
4197
+
4198
+ Analyze each changed function for:
4199
+
4200
+ ### 1. CODE QUALITY
4201
+ - Naming conventions (functions, variables, types)
4202
+ - Code complexity and readability
4203
+ - Code duplication
4204
+ - Best practices adherence
4205
+ - TypeScript/JavaScript patterns
4206
+ - Error handling completeness
4207
+ - Logging and debugging support
4208
+
4209
+ ### 2. POTENTIAL BUGS
4210
+ - Logic errors
4211
+ - Edge cases not handled (null, undefined, empty arrays, etc.)
4212
+ - Type mismatches
4213
+ - Async/await issues
4214
+ - Race conditions
4215
+ - Off-by-one errors
4216
+ - Incorrect conditionals
4217
+
4218
+ ### 3. PERFORMANCE ISSUES
4219
+ - Inefficient algorithms (O(n²) when O(n) possible)
4220
+ - Unnecessary iterations or computations
4221
+ - Memory leaks (closures, event listeners)
4222
+ - Missing caching opportunities
4223
+ - Inefficient data structures
4224
+ - Unnecessary re-renders (React)
4225
+
4226
+ ### 4. SECURITY VULNERABILITIES
4227
+ - Input validation missing
4228
+ - SQL injection risks
4229
+ - XSS vulnerabilities
4230
+ - Authentication/authorization issues
4231
+ - Sensitive data exposure
4232
+ - Insecure dependencies
4233
+ - Missing rate limiting
4234
+
4235
+ ## OUTPUT FORMAT
4236
+
4237
+ **Phase 3: Generate Review**
4238
+
4239
+ Use the write_review tool to create a comprehensive markdown review file.
4240
+
4241
+ The review MUST include:
4242
+
4243
+ 1. **Summary**: Brief overview of changes and overall assessment
4244
+ 2. **Findings by Category**: Group findings by:
4245
+ - 🔴 Critical (must fix immediately)
4246
+ - 🟠 High (should fix soon)
4247
+ - 🟡 Medium (consider fixing)
4248
+ - 🟢 Low (nice to have)
4249
+
4250
+ For each finding:
4251
+ - Category (Code Quality/Bugs/Performance/Security)
4252
+ - Severity (Critical/High/Medium/Low)
4253
+ - Location (function name, approximate line)
4254
+ - Issue description
4255
+ - Code snippet showing the problem
4256
+ - Recommended fix with code example
4257
+ - Rationale
4258
+
4259
+ 3. **Positive Aspects**: What was done well
4260
+ 4. **Recommendations**: General suggestions for improvement
4261
+
4262
+ ## MARKDOWN TEMPLATE
4263
+
4264
+ \`\`\`markdown
4265
+ # Code Review: {filename}
4266
+
4267
+ **Date**: {current_date}
4268
+ **Reviewer**: AI Code Review System
4269
+ **Changed Functions**: {list of functions}
4270
+
4271
+ ---
4272
+
4273
+ ## Summary
4274
+
4275
+ [Brief overview of changes and overall code quality assessment]
4276
+
4277
+ ---
4278
+
4279
+ ## Findings
4280
+
4281
+ ### 🔴 Critical Issues
4282
+
4283
+ #### [Category] - [Issue Title]
4284
+ **Severity**: Critical
4285
+ **Function**: \`functionName\`
4286
+ **Location**: Line ~XX
4287
+
4288
+ **Issue**:
4289
+ [Description of the problem]
4290
+
4291
+ **Current Code**:
4292
+ \`\`\`typescript
4293
+ // problematic code snippet
4294
+ \`\`\`
4295
+
4296
+ **Recommended Fix**:
4297
+ \`\`\`typescript
4298
+ // improved code snippet
4299
+ \`\`\`
4300
+
4301
+ **Rationale**:
4302
+ [Why this is important and how the fix helps]
4303
+
4304
+ ---
4305
+
4306
+ ### 🟠 High Priority Issues
4307
+
4308
+ [Same format as above]
4309
+
4310
+ ---
4311
+
4312
+ ### 🟡 Medium Priority Issues
4313
+
4314
+ [Same format as above]
4315
+
4316
+ ---
4317
+
4318
+ ### 🟢 Low Priority Issues
4319
+
4320
+ [Same format as above]
4321
+
4322
+ ---
4323
+
4324
+ ## ✅ Positive Aspects
4325
+
4326
+ - [What was done well]
4327
+ - [Good practices observed]
4328
+
4329
+ ---
4330
+
4331
+ ## 💡 General Recommendations
4332
+
4333
+ 1. [Recommendation 1]
4334
+ 2. [Recommendation 2]
4335
+
4336
+ ---
4337
+
4338
+ ## Conclusion
4339
+
4340
+ [Final thoughts and overall assessment]
4341
+ \`\`\`
4342
+
4343
+ ## CRITICAL REMINDERS
4344
+
4345
+ - ALWAYS use tools to analyze code before reviewing
4346
+ - Be thorough but constructive
4347
+ - Provide specific, actionable feedback
4348
+ - Include code examples in your suggestions
4349
+ - Consider the context and project constraints
4350
+ - Focus on changed functions but consider their impact on the whole file
4351
+ - Use write_review tool to save your review to: ${reviewFilePath}
4352
+
4353
+ **START**: Begin by calling analyze_file_ast(${filePath}) to understand the file structure.`;
4354
+ const messages = [
4355
+ {
4356
+ role: 'user',
4357
+ content: reviewPrompt
4358
+ }
4359
+ ];
4360
+ let iterations = 0;
4361
+ const maxIterations = 50;
4362
+ let reviewWritten = false;
4363
+ while (iterations < maxIterations) {
4364
+ iterations++;
4365
+ if (iterations === 1) {
4366
+ console.log(`\n🤖 AI is analyzing code for review...`);
4367
+ }
4368
+ else if (iterations % 5 === 0) {
4369
+ console.log(`\n🤖 AI is still working on review (step ${iterations})...`);
4370
+ }
4371
+ const response = await callAI(messages, TOOLS, CONFIG.aiProvider);
4372
+ if (response.content) {
4373
+ const content = response.content;
4374
+ messages.push({ role: 'assistant', content });
4375
+ // Check if review is complete
4376
+ if (content.toLowerCase().includes('review complete') ||
4377
+ content.toLowerCase().includes('review has been written')) {
4378
+ if (reviewWritten) {
4379
+ console.log('\n✅ Code review complete!');
4380
+ break;
4381
+ }
4382
+ }
4383
+ }
4384
+ if (!response.toolCalls || response.toolCalls.length === 0) {
4385
+ if (reviewWritten) {
4386
+ console.log('\n✅ Code review complete!');
4387
+ break;
4388
+ }
4389
+ console.log('\n⚠️ No tool calls. Prompting AI to continue...');
4390
+ messages.push({
4391
+ role: 'user',
4392
+ content: `Please use the write_review tool NOW to save your code review to ${reviewFilePath}. Include all findings with severity levels, code examples, and recommendations.`
4393
+ });
4394
+ continue;
4395
+ }
4396
+ // Execute tool calls
4397
+ const toolResults = [];
4398
+ for (const toolCall of response.toolCalls) {
4399
+ const toolResult = await executeTool(toolCall.name, toolCall.input);
4400
+ toolResults.push({ id: toolCall.id, name: toolCall.name, result: toolResult });
4401
+ // Track if review was written
4402
+ if (toolCall.name === 'write_review' && toolResult.success) {
4403
+ reviewWritten = true;
4404
+ }
4405
+ }
4406
+ // Add tool calls and results to messages (format differs by provider)
4407
+ if (CONFIG.aiProvider === 'openai') {
4408
+ // OpenAI: First add assistant message with tool_calls, then add tool results
4409
+ messages.push({
4410
+ role: 'assistant',
4411
+ tool_calls: response.toolCalls.map(tc => ({
4412
+ id: tc.id,
4413
+ type: 'function',
4414
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
4415
+ }))
4416
+ });
4417
+ for (const tr of toolResults) {
4418
+ messages.push({
4419
+ role: 'tool',
4420
+ tool_call_id: tr.id,
4421
+ content: JSON.stringify(tr.result)
4422
+ });
4423
+ }
4424
+ }
4425
+ else if (CONFIG.aiProvider === 'gemini') {
4426
+ // Gemini: Add function calls and responses
4427
+ for (const toolCall of response.toolCalls) {
4428
+ messages.push({
4429
+ role: 'model',
4430
+ functionCall: { name: toolCall.name, args: toolCall.input }
4431
+ });
4432
+ const result = toolResults.find(tr => tr.name === toolCall.name);
4433
+ messages.push({
4434
+ role: 'user',
4435
+ functionResponse: { name: toolCall.name, response: result?.result }
4436
+ });
4437
+ }
4438
+ }
4439
+ else {
4440
+ // Claude: Add tool uses and results
4441
+ messages.push({
4442
+ role: 'assistant',
4443
+ content: response.toolCalls.map(tc => ({
4444
+ type: 'tool_use',
4445
+ id: tc.id,
4446
+ name: tc.name,
4447
+ input: tc.input
4448
+ }))
4449
+ });
4450
+ messages.push({
4451
+ role: 'user',
4452
+ content: toolResults.map(tr => ({
4453
+ type: 'tool_result',
4454
+ tool_use_id: tr.id,
4455
+ content: JSON.stringify(tr.result)
4456
+ }))
4457
+ });
4458
+ }
4459
+ }
4460
+ if (!reviewWritten) {
4461
+ console.log('\n⚠️ Could not complete code review. Manual review may be needed.');
4462
+ }
4463
+ }
4375
4464
  async function main() {
4376
4465
  console.log('🧪 AI-Powered Unit Test Generator with AST Analysis\n');
4377
- // Check for auto mode from CLI arguments EARLY (before any prompts)
4466
+ // Parse command from CLI arguments EARLY (before any prompts)
4378
4467
  const args = process.argv.slice(2);
4379
- const isAutoMode = args.includes('auto');
4468
+ const command = args[0]; // First argument is the command: 'auto', 'test', 'review', or undefined
4469
+ // Validate command if provided
4470
+ if (command && !['auto', 'test', 'review'].includes(command)) {
4471
+ console.error('❌ Invalid command. Usage:\n');
4472
+ console.error(' testgen auto - Review changes and generate tests');
4473
+ console.error(' testgen test - Generate tests only');
4474
+ console.error(' testgen review - Review changes only');
4475
+ console.error(' testgen - Interactive mode\n');
4476
+ process.exit(1);
4477
+ }
4380
4478
  // Load configuration from codeguard.json
4381
4479
  try {
4382
4480
  CONFIG = (0, config_1.loadConfig)();
@@ -4399,9 +4497,12 @@ async function main() {
4399
4497
  console.error('npm install @babel/parser @babel/traverse ts-node\n');
4400
4498
  process.exit(1);
4401
4499
  }
4402
- // If auto mode, skip indexing setup and proceed directly
4403
- if (isAutoMode) {
4404
- console.log('🤖 Auto Mode: Detecting changes via git diff\n');
4500
+ // If command mode (auto, test, review), skip indexing setup and proceed directly
4501
+ if (command === 'auto' || command === 'test' || command === 'review') {
4502
+ const modeLabel = command === 'auto' ? 'Auto Mode (Review + Test)' :
4503
+ command === 'test' ? 'Test Generation Mode' :
4504
+ 'Code Review Mode';
4505
+ console.log(`🤖 ${modeLabel}: Detecting changes via git diff\n`);
4405
4506
  console.log(`✅ Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
4406
4507
  // Initialize indexer if it exists, but don't prompt
4407
4508
  globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
@@ -4412,7 +4513,21 @@ async function main() {
4412
4513
  else {
4413
4514
  globalIndexer = null;
4414
4515
  }
4415
- await autoGenerateTests();
4516
+ // Execute based on command
4517
+ if (command === 'auto') {
4518
+ // Run review first, then tests
4519
+ await reviewChangedFiles();
4520
+ console.log('\n' + '='.repeat(60) + '\n');
4521
+ await autoGenerateTests();
4522
+ }
4523
+ else if (command === 'test') {
4524
+ // Only generate tests
4525
+ await autoGenerateTests();
4526
+ }
4527
+ else if (command === 'review') {
4528
+ // Only review changes
4529
+ await reviewChangedFiles();
4530
+ }
4416
4531
  return;
4417
4532
  }
4418
4533
  // Optional: Codebase Indexing