codeguard-testgen 1.0.9 → 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/README.md +332 -40
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +965 -847
- package/dist/index.js.map +1 -1
- package/dist/typeValidator.d.ts +25 -0
- package/dist/typeValidator.d.ts.map +1 -0
- package/dist/typeValidator.js +180 -0
- package/dist/typeValidator.js.map +1 -0
- package/package.json +1 -1
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
|
-
//
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
console.log(
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2829
|
-
Test file
|
|
3009
|
+
## CONTEXT
|
|
3010
|
+
Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
2830
3011
|
|
|
2831
|
-
|
|
3012
|
+
⚠️ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
|
|
2832
3013
|
|
|
2833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
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
|
-
|
|
2978
|
-
Pattern Structure:
|
|
3049
|
+
// virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
|
|
2979
3050
|
|
|
2980
|
-
|
|
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
|
-
|
|
3053
|
+
// ===== IMPORTS =====
|
|
3054
|
+
import { functionName } from '../controller';
|
|
3055
|
+
import { dependencyMethod } from '../helpers/dependency';
|
|
2987
3056
|
|
|
2988
|
-
|
|
2989
|
-
|
|
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
|
-
|
|
3060
|
+
\\\`\\\`\\\`
|
|
2997
3061
|
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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
|
-
|
|
3069
|
+
/**
|
|
3070
|
+
* Phase 3: Anti-Pollution Pattern (MANDATORY)
|
|
3071
|
+
*/
|
|
3006
3072
|
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3073
|
+
\\\`\\\`\\\`typescript
|
|
3074
|
+
// ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
|
|
3075
|
+
afterEach(() => {
|
|
3076
|
+
jest.restoreAllMocks(); // Automatically restores ALL spies
|
|
3077
|
+
});
|
|
3012
3078
|
|
|
3013
|
-
|
|
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
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
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
|
-
|
|
3098
|
+
test('error case', async () => {
|
|
3099
|
+
mockDep1.mockRejectedValueOnce(new Error('fail'));
|
|
3100
|
+
await expect(functionName()).rejects.toThrow('fail');
|
|
3101
|
+
});
|
|
3102
|
+
});
|
|
3021
3103
|
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
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.
|
|
3575
|
-
2.
|
|
3576
|
-
3.
|
|
3577
|
-
4.
|
|
3578
|
-
5.
|
|
3579
|
-
6.
|
|
3580
|
-
7.
|
|
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.
|
|
3585
|
-
2.
|
|
3586
|
-
3.
|
|
3587
|
-
4.
|
|
3588
|
-
5.
|
|
3589
|
-
6.
|
|
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
|
}
|
|
@@ -3667,7 +3220,7 @@ This will automatically replace the existing test cases for the function with th
|
|
|
3667
3220
|
let lastTestError = '';
|
|
3668
3221
|
let sameErrorCount = 0;
|
|
3669
3222
|
while (iterations < maxIterations) {
|
|
3670
|
-
|
|
3223
|
+
console.log('USING CLAUDE PROMPT original 16');
|
|
3671
3224
|
iterations++;
|
|
3672
3225
|
if (iterations === 1) {
|
|
3673
3226
|
console.log(`\n🤖 AI is analyzing selected functions...`);
|
|
@@ -3675,7 +3228,7 @@ 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,
|
|
3231
|
+
const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
|
|
3679
3232
|
// console.log('response from AI', JSON.stringify(response, null, 2));
|
|
3680
3233
|
if (response.content) {
|
|
3681
3234
|
const content = response.content;
|
|
@@ -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
|
-
|
|
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,
|
|
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() {
|
|
@@ -4369,11 +4094,387 @@ async function autoGenerateTests() {
|
|
|
4369
4094
|
throw error;
|
|
4370
4095
|
}
|
|
4371
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
|
+
}
|
|
4372
4464
|
async function main() {
|
|
4373
4465
|
console.log('🧪 AI-Powered Unit Test Generator with AST Analysis\n');
|
|
4374
|
-
//
|
|
4466
|
+
// Parse command from CLI arguments EARLY (before any prompts)
|
|
4375
4467
|
const args = process.argv.slice(2);
|
|
4376
|
-
const
|
|
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
|
+
}
|
|
4377
4478
|
// Load configuration from codeguard.json
|
|
4378
4479
|
try {
|
|
4379
4480
|
CONFIG = (0, config_1.loadConfig)();
|
|
@@ -4396,9 +4497,12 @@ async function main() {
|
|
|
4396
4497
|
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
4397
4498
|
process.exit(1);
|
|
4398
4499
|
}
|
|
4399
|
-
// If
|
|
4400
|
-
if (
|
|
4401
|
-
|
|
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`);
|
|
4402
4506
|
console.log(`✅ Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
4403
4507
|
// Initialize indexer if it exists, but don't prompt
|
|
4404
4508
|
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
@@ -4409,7 +4513,21 @@ async function main() {
|
|
|
4409
4513
|
else {
|
|
4410
4514
|
globalIndexer = null;
|
|
4411
4515
|
}
|
|
4412
|
-
|
|
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
|
+
}
|
|
4413
4531
|
return;
|
|
4414
4532
|
}
|
|
4415
4533
|
// Optional: Codebase Indexing
|