codeguard-testgen 1.0.10 → 1.0.11
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 +297 -2
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +68 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1425 -507
- package/dist/index.js.map +1 -1
- package/dist/reviewEngine.d.ts +38 -0
- package/dist/reviewEngine.d.ts.map +1 -0
- package/dist/reviewEngine.js +571 -0
- package/dist/reviewEngine.js.map +1 -0
- package/dist/toolExecutors.d.ts +82 -0
- package/dist/toolExecutors.d.ts.map +1 -0
- package/dist/toolExecutors.js +354 -0
- package/dist/toolExecutors.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ const fsSync = require("fs");
|
|
|
39
39
|
const path = require("path");
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
41
|
const readline = require("readline");
|
|
42
|
+
const ora = require('ora');
|
|
42
43
|
// AST parsers
|
|
43
44
|
const babelParser = require("@babel/parser");
|
|
44
45
|
const traverse = require('@babel/traverse').default;
|
|
@@ -53,6 +54,7 @@ const fuzzyMatcher_1 = require("./fuzzyMatcher");
|
|
|
53
54
|
let CONFIG;
|
|
54
55
|
// Global indexer instance (optional - only initialized if user chooses to index)
|
|
55
56
|
let globalIndexer = null;
|
|
57
|
+
let globalSpinner = null; // Shared spinner for all operations
|
|
56
58
|
// Global variable to track expected test file path (to prevent AI from creating per-function files)
|
|
57
59
|
let EXPECTED_TEST_FILE_PATH = null;
|
|
58
60
|
// AI Provider configurations - models will be set from CONFIG
|
|
@@ -124,13 +126,17 @@ const TOOLS = [
|
|
|
124
126
|
},
|
|
125
127
|
{
|
|
126
128
|
name: 'analyze_file_ast',
|
|
127
|
-
description: 'Parse file using AST and extract detailed information about
|
|
129
|
+
description: 'Parse file using AST and extract detailed information about functions, classes, types, and exports. Optionally filter to specific function for token efficiency in function-wise test generation.',
|
|
128
130
|
input_schema: {
|
|
129
131
|
type: 'object',
|
|
130
132
|
properties: {
|
|
131
133
|
file_path: {
|
|
132
134
|
type: 'string',
|
|
133
135
|
description: 'The relative path to the source file.'
|
|
136
|
+
},
|
|
137
|
+
function_name: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Optional: Filter results to only this function. Use in function-wise test generation to reduce tokens (returns metadata for 1 function instead of all). Omit to get all functions (file-wise mode).'
|
|
134
140
|
}
|
|
135
141
|
},
|
|
136
142
|
required: ['file_path']
|
|
@@ -184,7 +190,7 @@ const TOOLS = [
|
|
|
184
190
|
},
|
|
185
191
|
{
|
|
186
192
|
name: 'get_file_preamble',
|
|
187
|
-
description: 'Extract top-level code (imports,
|
|
193
|
+
description: 'Extract top-level code (imports, vi.mock calls, setup blocks, module-level variables) from a file. Captures complete multi-line statements. Perfect for understanding large test files without reading entire content.',
|
|
188
194
|
input_schema: {
|
|
189
195
|
type: 'object',
|
|
190
196
|
properties: {
|
|
@@ -256,7 +262,7 @@ const TOOLS = [
|
|
|
256
262
|
},
|
|
257
263
|
{
|
|
258
264
|
name: 'run_tests',
|
|
259
|
-
description: 'Run
|
|
265
|
+
description: 'Run Vitest tests for a specific test file. In function-wise mode, only runs tests for specified functions to avoid interference from other failing tests.',
|
|
260
266
|
input_schema: {
|
|
261
267
|
type: 'object',
|
|
262
268
|
properties: {
|
|
@@ -267,7 +273,7 @@ const TOOLS = [
|
|
|
267
273
|
function_names: {
|
|
268
274
|
type: 'array',
|
|
269
275
|
items: { type: 'string' },
|
|
270
|
-
description: 'Optional: Array of function names to test. When provided, only runs tests matching these function names using
|
|
276
|
+
description: 'Optional: Array of function names to test. When provided, only runs tests matching these function names using Vitest -t flag. Use this in function-wise mode to isolate specific function tests.'
|
|
271
277
|
}
|
|
272
278
|
},
|
|
273
279
|
required: ['test_file_path']
|
|
@@ -303,7 +309,7 @@ const TOOLS = [
|
|
|
303
309
|
},
|
|
304
310
|
{
|
|
305
311
|
name: 'calculate_relative_path',
|
|
306
|
-
description: 'Calculate the correct relative import path from one file to
|
|
312
|
+
description: 'Calculate the correct relative import path from one file to one or more target files. Accepts multiple to_file paths for efficiency - get all relative paths in a single call!',
|
|
307
313
|
input_schema: {
|
|
308
314
|
type: 'object',
|
|
309
315
|
properties: {
|
|
@@ -313,39 +319,12 @@ const TOOLS = [
|
|
|
313
319
|
},
|
|
314
320
|
to_file: {
|
|
315
321
|
type: 'string',
|
|
316
|
-
description: '
|
|
322
|
+
description: 'One or more files to import. Can be a single file path or multiple comma-separated paths (e.g., "src/services/aws.ts, src/models/doctors.ts")'
|
|
317
323
|
}
|
|
318
324
|
},
|
|
319
325
|
required: ['from_file', 'to_file']
|
|
320
326
|
}
|
|
321
327
|
},
|
|
322
|
-
{
|
|
323
|
-
name: 'report_legitimate_failure',
|
|
324
|
-
description: 'Report that test failures are legitimate due to bugs in the source code, not issues with the test itself. Use this when tests are correctly written but fail because the source code has bugs.',
|
|
325
|
-
input_schema: {
|
|
326
|
-
type: 'object',
|
|
327
|
-
properties: {
|
|
328
|
-
test_file_path: {
|
|
329
|
-
type: 'string',
|
|
330
|
-
description: 'The path to the test file'
|
|
331
|
-
},
|
|
332
|
-
failing_tests: {
|
|
333
|
-
type: 'array',
|
|
334
|
-
items: { type: 'string' },
|
|
335
|
-
description: 'List of test names that are legitimately failing'
|
|
336
|
-
},
|
|
337
|
-
reason: {
|
|
338
|
-
type: 'string',
|
|
339
|
-
description: 'Explanation of why the failures are legitimate (e.g., "Function returns undefined instead of expected object", "Missing null check causes TypeError")'
|
|
340
|
-
},
|
|
341
|
-
source_code_issue: {
|
|
342
|
-
type: 'string',
|
|
343
|
-
description: 'Description of the bug in the source code that causes the failure'
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
required: ['test_file_path', 'failing_tests', 'reason', 'source_code_issue']
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
328
|
{
|
|
350
329
|
name: 'search_replace_block',
|
|
351
330
|
description: '🚀 RECOMMENDED: Search for a code block and replace it with new content. Uses intelligent fuzzy matching to handle whitespace/indentation differences. 10x more reliable than line-based editing! Include 3-5 lines of context before/after the target to make the search unique.',
|
|
@@ -418,10 +397,42 @@ const TOOLS = [
|
|
|
418
397
|
required: ['file_path', 'review_content']
|
|
419
398
|
}
|
|
420
399
|
},
|
|
400
|
+
{
|
|
401
|
+
name: 'search_codebase',
|
|
402
|
+
description: 'Search the entire codebase for a pattern using grep. Useful for finding where a function is used, where routes are registered, or finding usage examples. Returns matching lines with file paths and line numbers. IMPORTANT: Use focused searches to avoid token limits - be specific with patterns and use file_extension filter.',
|
|
403
|
+
input_schema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
pattern: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'The search pattern (can be a string or regex). Examples: "getUsers", "router.get.*getUsers" (be specific!)'
|
|
409
|
+
},
|
|
410
|
+
file_extension: {
|
|
411
|
+
type: 'string',
|
|
412
|
+
description: 'RECOMMENDED: Filter by file extension (e.g., ".ts", ".js") to reduce results'
|
|
413
|
+
},
|
|
414
|
+
max_results: {
|
|
415
|
+
type: 'number',
|
|
416
|
+
description: 'Maximum number of matching lines to return (default: 20, max: 50). Use low values to avoid token limits.'
|
|
417
|
+
},
|
|
418
|
+
files_only: {
|
|
419
|
+
type: 'boolean',
|
|
420
|
+
description: 'If true, return only file paths without content. Use this first to see how many files match, then search specific files.'
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
required: ['pattern']
|
|
424
|
+
}
|
|
425
|
+
},
|
|
421
426
|
];
|
|
422
427
|
exports.TOOLS = TOOLS;
|
|
423
428
|
// Filtered tools for test generation (excludes write_review)
|
|
424
429
|
const TOOLS_FOR_TEST_GENERATION = TOOLS.filter(tool => tool.name !== 'write_review');
|
|
430
|
+
const TOOLS_FOR_CODE_REVIEW = TOOLS.filter((tool) => tool.name !== "run_tests" &&
|
|
431
|
+
tool.name !== "upsert_function_tests" &&
|
|
432
|
+
tool.name !== "insert_at_position" &&
|
|
433
|
+
tool.name !== "search_replace_block" &&
|
|
434
|
+
tool.name !== "calculate_relative_path" &&
|
|
435
|
+
tool.name !== "resolve_import_path");
|
|
425
436
|
// AST Parsing utilities
|
|
426
437
|
function parseFileToAST(filePath, content) {
|
|
427
438
|
const ext = path.extname(filePath);
|
|
@@ -446,7 +457,7 @@ function parseFileToAST(filePath, content) {
|
|
|
446
457
|
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
447
458
|
}
|
|
448
459
|
}
|
|
449
|
-
function analyzeFileAST(filePath) {
|
|
460
|
+
function analyzeFileAST(filePath, functionName) {
|
|
450
461
|
try {
|
|
451
462
|
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
452
463
|
const ast = parseFileToAST(filePath, content);
|
|
@@ -459,7 +470,8 @@ function analyzeFileAST(filePath) {
|
|
|
459
470
|
exports: [],
|
|
460
471
|
imports: [],
|
|
461
472
|
types: [],
|
|
462
|
-
constants: []
|
|
473
|
+
constants: [],
|
|
474
|
+
routeRegistrations: []
|
|
463
475
|
};
|
|
464
476
|
traverse(ast, {
|
|
465
477
|
// Function declarations
|
|
@@ -570,6 +582,49 @@ function analyzeFileAST(filePath) {
|
|
|
570
582
|
});
|
|
571
583
|
}
|
|
572
584
|
});
|
|
585
|
+
// Detect route registrations
|
|
586
|
+
analysis.routeRegistrations = detectRouteRegistrations(content, analysis.imports);
|
|
587
|
+
// Filter to specific function if requested (token optimization)
|
|
588
|
+
if (functionName) {
|
|
589
|
+
const targetFunction = analysis.functions.find(f => f.name === functionName);
|
|
590
|
+
if (!targetFunction) {
|
|
591
|
+
// Provide helpful error with available functions
|
|
592
|
+
const available = analysis.functions.map(f => f.name).slice(0, 10).join(', ');
|
|
593
|
+
const more = analysis.functions.length > 10 ? ` (and ${analysis.functions.length - 10} more)` : '';
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
error: `Function '${functionName}' not found in ${filePath}. Available functions: ${available}${more}`
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// Find related class if function is a method
|
|
600
|
+
let relatedClass = null;
|
|
601
|
+
for (const cls of analysis.classes) {
|
|
602
|
+
if (cls.methods?.some((m) => m.name === functionName)) {
|
|
603
|
+
relatedClass = cls;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
analysis: {
|
|
610
|
+
functions: [targetFunction],
|
|
611
|
+
classes: relatedClass ? [relatedClass] : [],
|
|
612
|
+
exports: analysis.exports.filter(e => e.name === functionName || e.local === functionName),
|
|
613
|
+
imports: analysis.imports, // Keep all imports for dependency context
|
|
614
|
+
types: [], // Types filtered out for token efficiency
|
|
615
|
+
constants: [], // Constants filtered out for token efficiency
|
|
616
|
+
routeRegistrations: analysis.routeRegistrations.filter(r => r.handler === functionName)
|
|
617
|
+
},
|
|
618
|
+
summary: {
|
|
619
|
+
functionCount: 1,
|
|
620
|
+
classCount: relatedClass ? 1 : 0,
|
|
621
|
+
exportCount: analysis.exports.filter(e => e.name === functionName || e.local === functionName).length,
|
|
622
|
+
typeCount: 0,
|
|
623
|
+
filtered: true,
|
|
624
|
+
targetFunction: functionName
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
573
628
|
return {
|
|
574
629
|
success: true,
|
|
575
630
|
analysis,
|
|
@@ -585,6 +640,60 @@ function analyzeFileAST(filePath) {
|
|
|
585
640
|
return { success: false, error: error.message };
|
|
586
641
|
}
|
|
587
642
|
}
|
|
643
|
+
// Fast route detection using regex patterns
|
|
644
|
+
function detectRouteRegistrations(code, imports) {
|
|
645
|
+
const registrations = [];
|
|
646
|
+
// Express: router.get('/users', getUsers) or app.post('/users', createUser)
|
|
647
|
+
const expressPattern = /\b(app|router)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
|
|
648
|
+
for (const match of code.matchAll(expressPattern)) {
|
|
649
|
+
registrations.push({
|
|
650
|
+
framework: 'express',
|
|
651
|
+
method: match[2].toUpperCase(),
|
|
652
|
+
path: match[3],
|
|
653
|
+
handler: match[4],
|
|
654
|
+
handlerImportedFrom: findImportSource(match[4], imports),
|
|
655
|
+
line: getLineNumber(code, match.index || 0)
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
// Fastify: fastify.get('/users', getUsers)
|
|
659
|
+
const fastifyPattern = /\bfastify\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
|
|
660
|
+
for (const match of code.matchAll(fastifyPattern)) {
|
|
661
|
+
registrations.push({
|
|
662
|
+
framework: 'fastify',
|
|
663
|
+
method: match[1].toUpperCase(),
|
|
664
|
+
path: match[2],
|
|
665
|
+
handler: match[3],
|
|
666
|
+
handlerImportedFrom: findImportSource(match[3], imports),
|
|
667
|
+
line: getLineNumber(code, match.index || 0)
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
// Koa: router.get('/users', getUsers)
|
|
671
|
+
const koaPattern = /\brouter\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
|
|
672
|
+
for (const match of code.matchAll(koaPattern)) {
|
|
673
|
+
registrations.push({
|
|
674
|
+
framework: 'koa',
|
|
675
|
+
method: match[1].toUpperCase(),
|
|
676
|
+
path: match[2],
|
|
677
|
+
handler: match[3],
|
|
678
|
+
handlerImportedFrom: findImportSource(match[3], imports),
|
|
679
|
+
line: getLineNumber(code, match.index || 0)
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return registrations;
|
|
683
|
+
}
|
|
684
|
+
function findImportSource(functionName, imports) {
|
|
685
|
+
for (const imp of imports) {
|
|
686
|
+
if (imp.imported && Array.isArray(imp.imported)) {
|
|
687
|
+
if (imp.imported.some((i) => i.local === functionName || i.imported === functionName)) {
|
|
688
|
+
return imp.source;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
function getLineNumber(code, index) {
|
|
695
|
+
return code.substring(0, index).split('\n').length;
|
|
696
|
+
}
|
|
588
697
|
function extractParamInfo(param) {
|
|
589
698
|
if (param.type === 'Identifier') {
|
|
590
699
|
return {
|
|
@@ -692,7 +801,7 @@ function getFunctionAST(filePath, functionName) {
|
|
|
692
801
|
}
|
|
693
802
|
// Fallback: If we have an indexer, search for the function in the index
|
|
694
803
|
if (globalIndexer) {
|
|
695
|
-
console.log(` 🔍 Function not found in ${filePath}, searching index...`);
|
|
804
|
+
// console.log(` 🔍 Function not found in ${filePath}, searching index...`);
|
|
696
805
|
// Search through all indexed files
|
|
697
806
|
const indexData = globalIndexer.index;
|
|
698
807
|
if (indexData && indexData.files) {
|
|
@@ -702,7 +811,7 @@ function getFunctionAST(filePath, functionName) {
|
|
|
702
811
|
const hasFunction = analysis.functions?.some((fn) => fn.name === functionName);
|
|
703
812
|
const hasMethod = analysis.classes?.some((cls) => cls.methods?.some((method) => method.name === functionName));
|
|
704
813
|
if (hasFunction || hasMethod) {
|
|
705
|
-
console.log(` ✅ Found ${functionName} in ${indexedFilePath}`);
|
|
814
|
+
// console.log(` ✅ Found ${functionName} in ${indexedFilePath}`);
|
|
706
815
|
const result = searchInFile(indexedFilePath);
|
|
707
816
|
if (result) {
|
|
708
817
|
return result;
|
|
@@ -843,7 +952,7 @@ function getImportsAST(filePath) {
|
|
|
843
952
|
catch (error) {
|
|
844
953
|
// File not found - try to find similar file in index
|
|
845
954
|
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
846
|
-
console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
955
|
+
// console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
847
956
|
const indexData = globalIndexer.index;
|
|
848
957
|
if (indexData && indexData.files) {
|
|
849
958
|
const searchName = path.basename(filePath);
|
|
@@ -953,7 +1062,7 @@ function getTypeDefinitions(filePath) {
|
|
|
953
1062
|
catch (error) {
|
|
954
1063
|
// File not found - try to find similar file in index
|
|
955
1064
|
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
956
|
-
console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
1065
|
+
// console.log(` 🔍 File not found at ${filePath}, searching index for similar files...`);
|
|
957
1066
|
const indexData = globalIndexer.index;
|
|
958
1067
|
if (indexData && indexData.files) {
|
|
959
1068
|
const searchName = path.basename(filePath);
|
|
@@ -1038,13 +1147,13 @@ function getFilePreamble(filePath) {
|
|
|
1038
1147
|
});
|
|
1039
1148
|
});
|
|
1040
1149
|
}
|
|
1041
|
-
// Expression statements (could be jest.mock or setup blocks)
|
|
1150
|
+
// Expression statements (could be vi.mock/jest.mock or setup blocks)
|
|
1042
1151
|
else if (statement.type === 'ExpressionStatement' && statement.expression?.type === 'CallExpression') {
|
|
1043
1152
|
const callExpr = statement.expression;
|
|
1044
1153
|
const callee = callExpr.callee;
|
|
1045
|
-
// Check for jest.mock()
|
|
1154
|
+
// Check for vi.mock() or jest.mock()
|
|
1046
1155
|
if (callee.type === 'MemberExpression' &&
|
|
1047
|
-
callee.object?.name === 'jest' &&
|
|
1156
|
+
(callee.object?.name === 'vi' || callee.object?.name === 'jest') &&
|
|
1048
1157
|
callee.property?.name === 'mock') {
|
|
1049
1158
|
const moduleName = callExpr.arguments[0]?.value || 'unknown';
|
|
1050
1159
|
let isVirtual = false;
|
|
@@ -1098,7 +1207,7 @@ function getFilePreamble(filePath) {
|
|
|
1098
1207
|
...topLevelVariables.map(v => ({ ...v, category: 'variable' }))
|
|
1099
1208
|
].sort((a, b) => a.startLine - b.startLine);
|
|
1100
1209
|
const fullCode = allItems.map(item => `// Lines ${item.startLine}-${item.endLine} (${item.category})\n${item.code}`).join('\n\n');
|
|
1101
|
-
console.log(` ✅ Found file at ${targetPath}`);
|
|
1210
|
+
// console.log(` ✅ Found file at ${targetPath}`);
|
|
1102
1211
|
// console.log(` fullCode: ${fullCode}`);
|
|
1103
1212
|
// console.log(` summary: ${JSON.stringify({
|
|
1104
1213
|
// importCount: imports.length,
|
|
@@ -1120,7 +1229,6 @@ function getFilePreamble(filePath) {
|
|
|
1120
1229
|
setupBlocks,
|
|
1121
1230
|
topLevelVariables
|
|
1122
1231
|
},
|
|
1123
|
-
fullCode,
|
|
1124
1232
|
summary: {
|
|
1125
1233
|
importCount: imports.length,
|
|
1126
1234
|
mockCount: mocks.length,
|
|
@@ -1141,7 +1249,7 @@ function getFilePreamble(filePath) {
|
|
|
1141
1249
|
}
|
|
1142
1250
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1143
1251
|
if (globalIndexer) {
|
|
1144
|
-
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1252
|
+
// console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1145
1253
|
const indexData = globalIndexer.index;
|
|
1146
1254
|
if (indexData && indexData.files) {
|
|
1147
1255
|
const searchName = path.basename(filePath);
|
|
@@ -1174,10 +1282,10 @@ function getFilePreamble(filePath) {
|
|
|
1174
1282
|
// Sort by score (highest first)
|
|
1175
1283
|
scored.sort((a, b) => b.score - a.score);
|
|
1176
1284
|
correctPath = scored[0].path;
|
|
1177
|
-
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1285
|
+
// console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1178
1286
|
}
|
|
1179
1287
|
else {
|
|
1180
|
-
console.log(` ✅ Found file at ${correctPath}`);
|
|
1288
|
+
// console.log(` ✅ Found file at ${correctPath}`);
|
|
1181
1289
|
}
|
|
1182
1290
|
const retryResult = extractPreamble(correctPath);
|
|
1183
1291
|
if (retryResult) {
|
|
@@ -1259,7 +1367,7 @@ function getClassMethods(filePath, className) {
|
|
|
1259
1367
|
}
|
|
1260
1368
|
// Fallback: If we have an indexer, search for the class in the index
|
|
1261
1369
|
if (globalIndexer) {
|
|
1262
|
-
console.log(` 🔍 Class not found in ${filePath}, searching index...`);
|
|
1370
|
+
// console.log(` 🔍 Class not found in ${filePath}, searching index...`);
|
|
1263
1371
|
// Search through all indexed files
|
|
1264
1372
|
const indexData = globalIndexer.index;
|
|
1265
1373
|
if (indexData && indexData.files) {
|
|
@@ -1268,7 +1376,7 @@ function getClassMethods(filePath, className) {
|
|
|
1268
1376
|
// Check if this file contains the class
|
|
1269
1377
|
const hasClass = analysis.classes?.some((cls) => cls.name === className);
|
|
1270
1378
|
if (hasClass) {
|
|
1271
|
-
console.log(` ✅ Found ${className} in ${indexedFilePath}`);
|
|
1379
|
+
// console.log(` ✅ Found ${className} in ${indexedFilePath}`);
|
|
1272
1380
|
const result = searchInFile(indexedFilePath);
|
|
1273
1381
|
if (result) {
|
|
1274
1382
|
return result;
|
|
@@ -1289,7 +1397,7 @@ function getClassMethods(filePath, className) {
|
|
|
1289
1397
|
}
|
|
1290
1398
|
// Other tool implementations
|
|
1291
1399
|
async function readFile(filePath) {
|
|
1292
|
-
const MAX_LINES =
|
|
1400
|
+
const MAX_LINES = 1000;
|
|
1293
1401
|
// Helper to read and validate file
|
|
1294
1402
|
const tryReadFile = async (targetPath) => {
|
|
1295
1403
|
try {
|
|
@@ -1302,14 +1410,14 @@ async function readFile(filePath) {
|
|
|
1302
1410
|
if (globalIndexer) {
|
|
1303
1411
|
const cached = globalIndexer.getFileAnalysis(targetPath);
|
|
1304
1412
|
if (cached) {
|
|
1305
|
-
console.log(` 📦 File too large (${lines.length} lines), returning cached analysis instead`);
|
|
1413
|
+
// console.log(` 📦 File too large (${lines.length} lines), returning cached analysis instead`);
|
|
1306
1414
|
// For test files, also include preamble automatically
|
|
1307
1415
|
let preamble = undefined;
|
|
1308
1416
|
if (isTestFile) {
|
|
1309
1417
|
const preambleResult = getFilePreamble(targetPath);
|
|
1310
1418
|
if (preambleResult.success) {
|
|
1311
1419
|
preamble = preambleResult;
|
|
1312
|
-
console.log(` 📋 Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
|
|
1420
|
+
// console.log(` 📋 Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
|
|
1313
1421
|
}
|
|
1314
1422
|
}
|
|
1315
1423
|
return {
|
|
@@ -1347,7 +1455,7 @@ async function readFile(filePath) {
|
|
|
1347
1455
|
}
|
|
1348
1456
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1349
1457
|
if (globalIndexer) {
|
|
1350
|
-
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1458
|
+
// console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1351
1459
|
const indexData = globalIndexer.index;
|
|
1352
1460
|
if (indexData && indexData.files) {
|
|
1353
1461
|
const searchName = path.basename(filePath);
|
|
@@ -1380,10 +1488,10 @@ async function readFile(filePath) {
|
|
|
1380
1488
|
// Sort by score (highest first)
|
|
1381
1489
|
scored.sort((a, b) => b.score - a.score);
|
|
1382
1490
|
correctPath = scored[0].path;
|
|
1383
|
-
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1491
|
+
// console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1384
1492
|
}
|
|
1385
1493
|
else {
|
|
1386
|
-
console.log(` ✅ Found file at ${correctPath}`);
|
|
1494
|
+
// console.log(` ✅ Found file at ${correctPath}`);
|
|
1387
1495
|
}
|
|
1388
1496
|
// Try to read from correct path
|
|
1389
1497
|
const retryResult = await tryReadFile(correctPath);
|
|
@@ -1478,7 +1586,7 @@ async function readFileLines(filePath, startLine, endLine) {
|
|
|
1478
1586
|
}
|
|
1479
1587
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1480
1588
|
if (globalIndexer) {
|
|
1481
|
-
console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1589
|
+
// console.log(` 🔍 File not found at ${filePath}, searching index...`);
|
|
1482
1590
|
const indexData = globalIndexer.index;
|
|
1483
1591
|
if (indexData && indexData.files) {
|
|
1484
1592
|
const searchName = path.basename(filePath);
|
|
@@ -1511,10 +1619,10 @@ async function readFileLines(filePath, startLine, endLine) {
|
|
|
1511
1619
|
// Sort by score (highest first)
|
|
1512
1620
|
scored.sort((a, b) => b.score - a.score);
|
|
1513
1621
|
correctPath = scored[0].path;
|
|
1514
|
-
console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1622
|
+
// console.log(` 💡 Multiple matches found, choosing best match: ${correctPath}`);
|
|
1515
1623
|
}
|
|
1516
1624
|
else {
|
|
1517
|
-
console.log(` ✅ Found file at ${correctPath}`);
|
|
1625
|
+
// console.log(` ✅ Found file at ${correctPath}`);
|
|
1518
1626
|
}
|
|
1519
1627
|
// Try to read from correct path
|
|
1520
1628
|
const retryResult = await tryReadFileLines(correctPath);
|
|
@@ -1912,7 +2020,55 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1912
2020
|
return { success: false, error: error.message };
|
|
1913
2021
|
}
|
|
1914
2022
|
}
|
|
1915
|
-
|
|
2023
|
+
// Strip ANSI color codes from output
|
|
2024
|
+
function stripAnsi(str) {
|
|
2025
|
+
return str.replace(/\u001b\[[0-9;]*m/g, '').replace(/\\u001b\[[0-9;]*m/g, '');
|
|
2026
|
+
}
|
|
2027
|
+
function runTestsVitest(testFilePath, functionNames) {
|
|
2028
|
+
try {
|
|
2029
|
+
// Build Vitest command using npm run test with dot reporter and no color for clean output
|
|
2030
|
+
let command = `npm run test ${testFilePath} -- --reporter=dot --no-color`;
|
|
2031
|
+
// If function names provided, use Vitest -t flag to run only those tests
|
|
2032
|
+
if (functionNames && functionNames.length > 0) {
|
|
2033
|
+
const escapedNames = functionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
2034
|
+
const pattern = escapedNames.join('|');
|
|
2035
|
+
command += ` -t "${pattern}"`;
|
|
2036
|
+
}
|
|
2037
|
+
const output = (0, child_process_1.execSync)(command, {
|
|
2038
|
+
encoding: 'utf-8',
|
|
2039
|
+
stdio: 'pipe',
|
|
2040
|
+
timeout: 60000, // 60 seconds - Vitest needs time to start
|
|
2041
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
2042
|
+
});
|
|
2043
|
+
// console.log('RUN TESTS OUTPUT', stripAnsi(output));
|
|
2044
|
+
// console.log('RUN TESTS COMMAND', command);
|
|
2045
|
+
return {
|
|
2046
|
+
success: true,
|
|
2047
|
+
output: stripAnsi(output),
|
|
2048
|
+
passed: true,
|
|
2049
|
+
command
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
catch (error) {
|
|
2053
|
+
// execSync error has stdout/stderr as strings when encoding is set
|
|
2054
|
+
let output = '';
|
|
2055
|
+
if (error.stdout)
|
|
2056
|
+
output += error.stdout;
|
|
2057
|
+
if (error.stderr)
|
|
2058
|
+
output += error.stderr;
|
|
2059
|
+
// Fallback: extract from error message if output is empty
|
|
2060
|
+
if (!output && error.message) {
|
|
2061
|
+
output = error.message;
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
success: false,
|
|
2065
|
+
output: stripAnsi(output) || 'Test failed with no output captured',
|
|
2066
|
+
passed: false,
|
|
2067
|
+
error: error.message
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function runTestsJest(testFilePath, functionNames) {
|
|
1916
2072
|
try {
|
|
1917
2073
|
// Build Jest command with optional function name filter
|
|
1918
2074
|
let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
|
|
@@ -1954,10 +2110,9 @@ function runTests(testFilePath, functionNames) {
|
|
|
1954
2110
|
*/
|
|
1955
2111
|
function runTestsIsolated(testFilePath, specificTestNames) {
|
|
1956
2112
|
try {
|
|
1957
|
-
// Build
|
|
1958
|
-
let command = `
|
|
2113
|
+
// Build Vitest command using npm run test with dot reporter and no color for clean output
|
|
2114
|
+
let command = `npm run test ${testFilePath} -- --reporter=dot --no-color`;
|
|
1959
2115
|
if (specificTestNames && specificTestNames.length > 0) {
|
|
1960
|
-
// Escape special regex characters in test names
|
|
1961
2116
|
const escapedNames = specificTestNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
1962
2117
|
const pattern = escapedNames.join('|');
|
|
1963
2118
|
command += ` -t "${pattern}"`;
|
|
@@ -1965,29 +2120,73 @@ function runTestsIsolated(testFilePath, specificTestNames) {
|
|
|
1965
2120
|
const output = (0, child_process_1.execSync)(command, {
|
|
1966
2121
|
encoding: 'utf-8',
|
|
1967
2122
|
stdio: 'pipe',
|
|
1968
|
-
timeout:
|
|
2123
|
+
timeout: 60000, // 60 seconds
|
|
2124
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
1969
2125
|
});
|
|
1970
2126
|
return {
|
|
1971
2127
|
success: true,
|
|
1972
|
-
output,
|
|
2128
|
+
output: stripAnsi(output),
|
|
1973
2129
|
passed: true,
|
|
1974
2130
|
command
|
|
1975
2131
|
};
|
|
1976
2132
|
}
|
|
1977
2133
|
catch (error) {
|
|
2134
|
+
// execSync error has stdout/stderr as strings when encoding is set
|
|
2135
|
+
let output = '';
|
|
2136
|
+
if (error.stdout)
|
|
2137
|
+
output += error.stdout;
|
|
2138
|
+
if (error.stderr)
|
|
2139
|
+
output += error.stderr;
|
|
2140
|
+
// Fallback: extract from error message if output is empty
|
|
2141
|
+
if (!output && error.message) {
|
|
2142
|
+
output = error.message;
|
|
2143
|
+
}
|
|
1978
2144
|
return {
|
|
1979
2145
|
success: false,
|
|
1980
|
-
output:
|
|
2146
|
+
output: stripAnsi(output) || 'Test failed with no output captured',
|
|
1981
2147
|
passed: false,
|
|
1982
2148
|
error: error.message
|
|
1983
2149
|
};
|
|
1984
2150
|
}
|
|
1985
2151
|
}
|
|
1986
2152
|
/**
|
|
1987
|
-
* Parse
|
|
2153
|
+
* Parse Vitest output to extract names of failing tests
|
|
1988
2154
|
* Returns array of test names that failed
|
|
1989
2155
|
*/
|
|
1990
|
-
function
|
|
2156
|
+
function parseFailingTestNamesVitest(vitestOutput) {
|
|
2157
|
+
const failingTests = [];
|
|
2158
|
+
// Vitest output patterns for failing tests:
|
|
2159
|
+
// ● describe block › test name
|
|
2160
|
+
// or: FAIL path/to/test.ts
|
|
2161
|
+
// ✕ test name (XXms)
|
|
2162
|
+
const lines = vitestOutput.split('\n');
|
|
2163
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2164
|
+
const line = lines[i];
|
|
2165
|
+
// Pattern 1: ● describe block › test name
|
|
2166
|
+
const bulletMatch = line.match(/^\s*●\s+(.+?)\s+›\s+(.+?)$/);
|
|
2167
|
+
if (bulletMatch) {
|
|
2168
|
+
const testName = bulletMatch[2].trim();
|
|
2169
|
+
failingTests.push(testName);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
// Pattern 2: ✕ test name
|
|
2173
|
+
const xMatch = line.match(/^\s*✕\s+(.+?)(?:\s+\(\d+m?s\))?$/);
|
|
2174
|
+
if (xMatch) {
|
|
2175
|
+
const testName = xMatch[1].trim();
|
|
2176
|
+
failingTests.push(testName);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
// Pattern 3: FAIL in summary
|
|
2180
|
+
const failMatch = line.match(/^\s*✓?\s*(.+?)\s+\(\d+m?s\)$/);
|
|
2181
|
+
if (failMatch && line.includes('✕')) {
|
|
2182
|
+
const testName = failMatch[1].trim();
|
|
2183
|
+
failingTests.push(testName);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
// Remove duplicates
|
|
2187
|
+
return [...new Set(failingTests)];
|
|
2188
|
+
}
|
|
2189
|
+
function parseFailingTestNamesJest(jestOutput) {
|
|
1991
2190
|
const failingTests = [];
|
|
1992
2191
|
// Jest output patterns for failing tests:
|
|
1993
2192
|
// ● describe block › test name
|
|
@@ -2100,25 +2299,138 @@ function findFile(filename) {
|
|
|
2100
2299
|
return { success: false, error: error.message };
|
|
2101
2300
|
}
|
|
2102
2301
|
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Calculate relative import path from one file to one or more target files.
|
|
2304
|
+
* Handles multiple paths for efficiency - AI can get all relative paths in a single call.
|
|
2305
|
+
*
|
|
2306
|
+
* @param fromFile - The file containing the import
|
|
2307
|
+
* @param toFile - Single path or comma-separated list of paths to import
|
|
2308
|
+
* @returns Object with relative paths for all target files
|
|
2309
|
+
*/
|
|
2103
2310
|
function calculateRelativePath(fromFile, toFile) {
|
|
2104
2311
|
try {
|
|
2312
|
+
// Normalize and validate the from_file path
|
|
2313
|
+
const normalizedFromFile = normalizePath(fromFile);
|
|
2314
|
+
if (!normalizedFromFile) {
|
|
2315
|
+
return { success: false, error: `Invalid from_file path: "${fromFile}"` };
|
|
2316
|
+
}
|
|
2317
|
+
// Parse multiple to_file paths (comma-separated)
|
|
2318
|
+
const toFiles = parseMultiplePaths(toFile);
|
|
2319
|
+
if (toFiles.length === 0) {
|
|
2320
|
+
return { success: false, error: `No valid to_file paths provided: "${toFile}"` };
|
|
2321
|
+
}
|
|
2322
|
+
// Calculate relative path for each target file
|
|
2323
|
+
const results = [];
|
|
2324
|
+
const errors = [];
|
|
2325
|
+
for (const targetFile of toFiles) {
|
|
2326
|
+
const result = calculateSingleRelativePath(normalizedFromFile, targetFile);
|
|
2327
|
+
if (result.success) {
|
|
2328
|
+
results.push(result);
|
|
2329
|
+
}
|
|
2330
|
+
else {
|
|
2331
|
+
errors.push({ file: targetFile, error: result.error });
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
// Return appropriate response based on number of files
|
|
2335
|
+
if (toFiles.length === 1) {
|
|
2336
|
+
// Single file: return flat result for backward compatibility
|
|
2337
|
+
return results.length > 0 ? results[0] : errors[0];
|
|
2338
|
+
}
|
|
2339
|
+
else {
|
|
2340
|
+
// Multiple files: return array of results
|
|
2341
|
+
return {
|
|
2342
|
+
success: errors.length === 0,
|
|
2343
|
+
from: normalizedFromFile,
|
|
2344
|
+
totalFiles: toFiles.length,
|
|
2345
|
+
successCount: results.length,
|
|
2346
|
+
errorCount: errors.length,
|
|
2347
|
+
results,
|
|
2348
|
+
errors: errors.length > 0 ? errors : undefined
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
catch (error) {
|
|
2353
|
+
return { success: false, error: error.message };
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Parse comma-separated or space-separated file paths into an array.
|
|
2358
|
+
* Handles various input formats and trims whitespace.
|
|
2359
|
+
*/
|
|
2360
|
+
function parseMultiplePaths(input) {
|
|
2361
|
+
if (!input || typeof input !== 'string') {
|
|
2362
|
+
return [];
|
|
2363
|
+
}
|
|
2364
|
+
// Split by comma (primary delimiter)
|
|
2365
|
+
const paths = input
|
|
2366
|
+
.split(',')
|
|
2367
|
+
.map(p => p.trim())
|
|
2368
|
+
.filter(p => p.length > 0)
|
|
2369
|
+
.map(p => normalizePath(p))
|
|
2370
|
+
.filter((p) => p !== null);
|
|
2371
|
+
return paths;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Normalize a file path - handle various edge cases.
|
|
2375
|
+
*/
|
|
2376
|
+
function normalizePath(filePath) {
|
|
2377
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
let normalized = filePath.trim();
|
|
2381
|
+
// Remove surrounding quotes if present
|
|
2382
|
+
if ((normalized.startsWith('"') && normalized.endsWith('"')) ||
|
|
2383
|
+
(normalized.startsWith("'") && normalized.endsWith("'"))) {
|
|
2384
|
+
normalized = normalized.slice(1, -1);
|
|
2385
|
+
}
|
|
2386
|
+
// Convert backslashes to forward slashes
|
|
2387
|
+
normalized = normalized.replace(/\\/g, '/');
|
|
2388
|
+
// Remove leading ./ if present (will be added back correctly)
|
|
2389
|
+
if (normalized.startsWith('./')) {
|
|
2390
|
+
normalized = normalized.slice(2);
|
|
2391
|
+
}
|
|
2392
|
+
// Remove duplicate slashes
|
|
2393
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
2394
|
+
// Remove trailing slash
|
|
2395
|
+
if (normalized.endsWith('/')) {
|
|
2396
|
+
normalized = normalized.slice(0, -1);
|
|
2397
|
+
}
|
|
2398
|
+
// Validate it looks like a file path (has at least one character and doesn't start with special chars)
|
|
2399
|
+
if (normalized.length === 0 || /^[\s@#$%^&*]/.test(normalized)) {
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
return normalized;
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Calculate relative path from one file to a single target file.
|
|
2406
|
+
* Core path calculation logic with robust handling.
|
|
2407
|
+
*/
|
|
2408
|
+
function calculateSingleRelativePath(fromFile, toFile) {
|
|
2409
|
+
try {
|
|
2410
|
+
// Get the directory of the source file
|
|
2105
2411
|
const fromDir = path.dirname(fromFile);
|
|
2412
|
+
// Calculate relative path
|
|
2106
2413
|
let relativePath = path.relative(fromDir, toFile);
|
|
2414
|
+
// Handle empty path (same directory)
|
|
2415
|
+
if (!relativePath) {
|
|
2416
|
+
const basename = path.basename(toFile);
|
|
2417
|
+
relativePath = './' + basename;
|
|
2418
|
+
}
|
|
2107
2419
|
// Remove .ts, .tsx, .js, .jsx extensions for imports
|
|
2108
2420
|
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
2421
|
+
// Also handle /index suffix (import from './folder' instead of './folder/index')
|
|
2422
|
+
relativePath = relativePath.replace(/\/index$/, '');
|
|
2109
2423
|
// Ensure it starts with ./ or ../
|
|
2110
|
-
if (!relativePath.startsWith('.')) {
|
|
2424
|
+
if (!relativePath.startsWith('.') && !relativePath.startsWith('..')) {
|
|
2111
2425
|
relativePath = './' + relativePath;
|
|
2112
2426
|
}
|
|
2113
2427
|
// Convert backslashes to forward slashes (Windows compatibility)
|
|
2114
2428
|
relativePath = relativePath.replace(/\\/g, '/');
|
|
2115
|
-
//
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
// importStatement: `import { ... } from '${relativePath}';`
|
|
2121
|
-
// })})`)
|
|
2429
|
+
// Handle edge case where path ends up as just '.'
|
|
2430
|
+
if (relativePath === '.') {
|
|
2431
|
+
const basename = path.basename(toFile).replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
2432
|
+
relativePath = './' + basename;
|
|
2433
|
+
}
|
|
2122
2434
|
return {
|
|
2123
2435
|
success: true,
|
|
2124
2436
|
from: fromFile,
|
|
@@ -2128,26 +2440,26 @@ function calculateRelativePath(fromFile, toFile) {
|
|
|
2128
2440
|
};
|
|
2129
2441
|
}
|
|
2130
2442
|
catch (error) {
|
|
2131
|
-
return { success: false, error: error.message };
|
|
2443
|
+
return { success: false, file: toFile, error: error.message };
|
|
2132
2444
|
}
|
|
2133
2445
|
}
|
|
2134
|
-
function reportLegitimateFailure(testFilePath, failingTests, reason, sourceCodeIssue) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2446
|
+
// function reportLegitimateFailure(testFilePath: string, failingTests: string[], reason: string, sourceCodeIssue: string): any {
|
|
2447
|
+
// console.log('\n⚠️ LEGITIMATE TEST FAILURE REPORTED');
|
|
2448
|
+
// console.log(` Test file: ${testFilePath}`);
|
|
2449
|
+
// console.log(` Failing tests: ${failingTests.join(', ')}`);
|
|
2450
|
+
// console.log(` Reason: ${reason}`);
|
|
2451
|
+
// console.log(` Source code issue: ${sourceCodeIssue}`);
|
|
2452
|
+
// return {
|
|
2453
|
+
// success: true,
|
|
2454
|
+
// acknowledged: true,
|
|
2455
|
+
// message: 'Legitimate failure reported. Tests have been written correctly but source code has bugs.',
|
|
2456
|
+
// testFilePath,
|
|
2457
|
+
// failingTests,
|
|
2458
|
+
// reason,
|
|
2459
|
+
// sourceCodeIssue,
|
|
2460
|
+
// recommendation: 'Fix the source code to resolve these test failures.'
|
|
2461
|
+
// };
|
|
2462
|
+
// }
|
|
2151
2463
|
async function deleteLines(filePath, startLine, endLine) {
|
|
2152
2464
|
try {
|
|
2153
2465
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
@@ -2509,10 +2821,10 @@ const TOOL_MESSAGES = {
|
|
|
2509
2821
|
'list_directory': '📂 Exploring directory structure',
|
|
2510
2822
|
'find_file': '🔍 Locating file in repository',
|
|
2511
2823
|
'calculate_relative_path': '🧭 Calculating import path',
|
|
2512
|
-
'report_legitimate_failure': '⚠️ Reporting legitimate test failures',
|
|
2513
2824
|
'search_replace_block': '🔍🔄 Searching and replacing code block',
|
|
2514
2825
|
'insert_at_position': '➕ Inserting content at position',
|
|
2515
2826
|
'write_review': '📝 Writing code review',
|
|
2827
|
+
'search_codebase': '🔎 Searching codebase',
|
|
2516
2828
|
};
|
|
2517
2829
|
// Tool execution router
|
|
2518
2830
|
async function executeTool(toolName, args) {
|
|
@@ -2532,7 +2844,16 @@ async function executeTool(toolName, args) {
|
|
|
2532
2844
|
else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
|
|
2533
2845
|
friendlyMessage = `📖 Reading lines ${args.start_line}-${args.end_line}`;
|
|
2534
2846
|
}
|
|
2535
|
-
|
|
2847
|
+
// Use global spinner for seamless in-place updates
|
|
2848
|
+
if (!globalSpinner) {
|
|
2849
|
+
globalSpinner = ora(friendlyMessage).start();
|
|
2850
|
+
}
|
|
2851
|
+
else if (globalSpinner.isSpinning) {
|
|
2852
|
+
globalSpinner.text = friendlyMessage;
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
globalSpinner = ora(friendlyMessage).start();
|
|
2856
|
+
}
|
|
2536
2857
|
let result;
|
|
2537
2858
|
try {
|
|
2538
2859
|
switch (toolName) {
|
|
@@ -2543,21 +2864,21 @@ async function executeTool(toolName, args) {
|
|
|
2543
2864
|
result = await readFileLines(args.file_path, args.start_line, args.end_line);
|
|
2544
2865
|
break;
|
|
2545
2866
|
case 'analyze_file_ast':
|
|
2546
|
-
// Try cache first if indexer is available
|
|
2547
|
-
if (globalIndexer) {
|
|
2867
|
+
// Try cache first if indexer is available (only for non-filtered requests)
|
|
2868
|
+
if (globalIndexer && !args.function_name) {
|
|
2548
2869
|
// Check if file has been modified since caching
|
|
2549
2870
|
if (globalIndexer.isFileStale(args.file_path)) {
|
|
2550
|
-
|
|
2551
|
-
result = analyzeFileAST(args.file_path);
|
|
2552
|
-
// Update cache with new analysis
|
|
2553
|
-
if (result.success) {
|
|
2871
|
+
globalSpinner.text = '🔄 File modified, re-analyzing...';
|
|
2872
|
+
result = analyzeFileAST(args.file_path, args.function_name);
|
|
2873
|
+
// Update cache with new analysis (only if not filtered)
|
|
2874
|
+
if (result.success && !args.function_name) {
|
|
2554
2875
|
await globalIndexer.updateIndex([args.file_path], analyzeFileAST);
|
|
2555
2876
|
}
|
|
2556
2877
|
break;
|
|
2557
2878
|
}
|
|
2558
2879
|
const cached = globalIndexer.getFileAnalysis(args.file_path);
|
|
2559
2880
|
if (cached) {
|
|
2560
|
-
|
|
2881
|
+
globalSpinner.text = '📦 Using cached analysis';
|
|
2561
2882
|
result = {
|
|
2562
2883
|
success: true,
|
|
2563
2884
|
analysis: cached,
|
|
@@ -2565,14 +2886,14 @@ async function executeTool(toolName, args) {
|
|
|
2565
2886
|
functionCount: cached.functions.length,
|
|
2566
2887
|
classCount: cached.classes.length,
|
|
2567
2888
|
exportCount: cached.exports.length,
|
|
2568
|
-
typeCount: cached.types.length
|
|
2889
|
+
typeCount: cached.types.length,
|
|
2569
2890
|
}
|
|
2570
2891
|
};
|
|
2571
2892
|
break;
|
|
2572
2893
|
}
|
|
2573
2894
|
}
|
|
2574
|
-
// Fall back to actual analysis
|
|
2575
|
-
result = analyzeFileAST(args.file_path);
|
|
2895
|
+
// Fall back to actual analysis (with optional filtering)
|
|
2896
|
+
result = analyzeFileAST(args.file_path, args.function_name);
|
|
2576
2897
|
break;
|
|
2577
2898
|
case 'get_function_ast':
|
|
2578
2899
|
result = getFunctionAST(args.file_path, args.function_name);
|
|
@@ -2599,10 +2920,10 @@ async function executeTool(toolName, args) {
|
|
|
2599
2920
|
const normalizedExpected = path.normalize(EXPECTED_TEST_FILE_PATH).replace(/\\/g, '/');
|
|
2600
2921
|
const normalizedProvided = path.normalize(args.test_file_path).replace(/\\/g, '/');
|
|
2601
2922
|
if (normalizedExpected !== normalizedProvided) {
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
console.log(
|
|
2605
|
-
|
|
2923
|
+
if (globalSpinner)
|
|
2924
|
+
globalSpinner.stop();
|
|
2925
|
+
console.log(`\n⚠️ BLOCKED: AI attempted separate test file, enforcing single file policy`);
|
|
2926
|
+
globalSpinner = ora(friendlyMessage).start();
|
|
2606
2927
|
// Override the test file path to the expected one
|
|
2607
2928
|
args.test_file_path = EXPECTED_TEST_FILE_PATH;
|
|
2608
2929
|
}
|
|
@@ -2610,7 +2931,12 @@ async function executeTool(toolName, args) {
|
|
|
2610
2931
|
result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
|
|
2611
2932
|
break;
|
|
2612
2933
|
case 'run_tests':
|
|
2613
|
-
|
|
2934
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
2935
|
+
result = runTestsVitest(args.test_file_path, args.function_names);
|
|
2936
|
+
}
|
|
2937
|
+
else {
|
|
2938
|
+
result = runTestsJest(args.test_file_path, args.function_names);
|
|
2939
|
+
}
|
|
2614
2940
|
break;
|
|
2615
2941
|
case 'list_directory':
|
|
2616
2942
|
result = listDirectory(args.directory_path);
|
|
@@ -2621,9 +2947,6 @@ async function executeTool(toolName, args) {
|
|
|
2621
2947
|
case 'calculate_relative_path':
|
|
2622
2948
|
result = calculateRelativePath(args.from_file, args.to_file);
|
|
2623
2949
|
break;
|
|
2624
|
-
case 'report_legitimate_failure':
|
|
2625
|
-
result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
|
|
2626
|
-
break;
|
|
2627
2950
|
case 'search_replace_block':
|
|
2628
2951
|
result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
|
|
2629
2952
|
break;
|
|
@@ -2633,6 +2956,9 @@ async function executeTool(toolName, args) {
|
|
|
2633
2956
|
case 'write_review':
|
|
2634
2957
|
result = await writeReview(args.file_path, args.review_content);
|
|
2635
2958
|
break;
|
|
2959
|
+
case 'search_codebase':
|
|
2960
|
+
result = searchCodebase(args.pattern, args.file_extension, args.max_results, args.files_only);
|
|
2961
|
+
break;
|
|
2636
2962
|
default:
|
|
2637
2963
|
result = { success: false, error: `Unknown tool: ${toolName}` };
|
|
2638
2964
|
}
|
|
@@ -2640,16 +2966,139 @@ async function executeTool(toolName, args) {
|
|
|
2640
2966
|
catch (error) {
|
|
2641
2967
|
result = { success: false, error: error.message, stack: error.stack };
|
|
2642
2968
|
}
|
|
2643
|
-
//
|
|
2969
|
+
// Just keep spinner running - next tool will update the text
|
|
2970
|
+
// No checkmarks for intermediate steps - smoother like Claude CLI
|
|
2644
2971
|
if (result.success) {
|
|
2645
|
-
//
|
|
2646
|
-
|
|
2972
|
+
// Small delay so users see the operation briefly
|
|
2973
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
2647
2974
|
}
|
|
2648
2975
|
else if (result.error) {
|
|
2649
|
-
|
|
2976
|
+
// Show persistent error and reset
|
|
2977
|
+
const errorMsg = result.error.substring(0, 100) + (result.error.length > 100 ? '...' : '');
|
|
2978
|
+
if (globalSpinner) {
|
|
2979
|
+
globalSpinner.fail(errorMsg);
|
|
2980
|
+
globalSpinner = null;
|
|
2981
|
+
}
|
|
2982
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
2650
2983
|
}
|
|
2651
2984
|
return result;
|
|
2652
2985
|
}
|
|
2986
|
+
// Search codebase utility
|
|
2987
|
+
function searchCodebase(pattern, fileExtension, maxResults = 20, filesOnly = false) {
|
|
2988
|
+
try {
|
|
2989
|
+
const { execSync } = require('child_process');
|
|
2990
|
+
// Cap maxResults to prevent token explosion
|
|
2991
|
+
const limit = Math.min(maxResults || 20, 50);
|
|
2992
|
+
// Build grep command
|
|
2993
|
+
let cmd = `grep -rn`;
|
|
2994
|
+
// If files_only, just list files
|
|
2995
|
+
if (filesOnly) {
|
|
2996
|
+
cmd = `grep -rl`; // -l for files with matches only
|
|
2997
|
+
}
|
|
2998
|
+
else {
|
|
2999
|
+
cmd += ` -C 1`; // Only 1 line of context to save tokens
|
|
3000
|
+
}
|
|
3001
|
+
// Add file extension filter if provided
|
|
3002
|
+
if (fileExtension) {
|
|
3003
|
+
const ext = fileExtension.startsWith('.') ? fileExtension : `.${fileExtension}`;
|
|
3004
|
+
cmd += ` --include="*${ext}"`;
|
|
3005
|
+
}
|
|
3006
|
+
// Exclude common directories
|
|
3007
|
+
const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage', '.codeguard-cache', '.jest-cache', '.testgen-cache', '.vitest', 'reviews'];
|
|
3008
|
+
for (const dir of excludeDirs) {
|
|
3009
|
+
cmd += ` --exclude-dir=${dir}`;
|
|
3010
|
+
}
|
|
3011
|
+
// Add pattern and search path
|
|
3012
|
+
cmd += ` "${pattern}" .`;
|
|
3013
|
+
// Limit output lines to prevent token overflow
|
|
3014
|
+
cmd += ` | head -n ${limit * 3}`; // 3 lines per match (context + match)
|
|
3015
|
+
// Execute grep
|
|
3016
|
+
const output = execSync(cmd, {
|
|
3017
|
+
encoding: 'utf-8',
|
|
3018
|
+
cwd: process.cwd(),
|
|
3019
|
+
maxBuffer: 5 * 1024 * 1024, // 1MB max (reduced from 10MB)
|
|
3020
|
+
stdio: ['pipe', 'pipe', 'ignore'] // Ignore stderr
|
|
3021
|
+
});
|
|
3022
|
+
if (filesOnly) {
|
|
3023
|
+
// Return just file list
|
|
3024
|
+
const files = output.trim().split('\n').filter(f => f.length > 0).slice(0, limit);
|
|
3025
|
+
return {
|
|
3026
|
+
success: true,
|
|
3027
|
+
pattern,
|
|
3028
|
+
filesOnly: true,
|
|
3029
|
+
totalFiles: files.length,
|
|
3030
|
+
files: files.map(f => ({ file: f })),
|
|
3031
|
+
message: `Found ${files.length} file(s) matching pattern`,
|
|
3032
|
+
hint: 'Use search_codebase with files_only=false on specific files to see content'
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
// Parse results with token awareness
|
|
3036
|
+
const lines = output.trim().split('\n').slice(0, limit);
|
|
3037
|
+
const results = [];
|
|
3038
|
+
const fileSet = new Set();
|
|
3039
|
+
let currentFile = '';
|
|
3040
|
+
let currentMatches = [];
|
|
3041
|
+
for (const line of lines) {
|
|
3042
|
+
if (line.includes(':')) {
|
|
3043
|
+
const match = line.match(/^([^:]+):(\d+):(.*)/);
|
|
3044
|
+
if (match) {
|
|
3045
|
+
const [, file, lineNum, content] = match;
|
|
3046
|
+
fileSet.add(file);
|
|
3047
|
+
if (file !== currentFile) {
|
|
3048
|
+
if (currentFile && currentMatches.length > 0) {
|
|
3049
|
+
results.push({
|
|
3050
|
+
file: currentFile,
|
|
3051
|
+
matchCount: currentMatches.length,
|
|
3052
|
+
matches: currentMatches.slice(0, 5) // Max 5 matches per file
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
currentFile = file;
|
|
3056
|
+
currentMatches = [];
|
|
3057
|
+
}
|
|
3058
|
+
// Truncate long lines to save tokens
|
|
3059
|
+
const truncatedContent = content.length > 150 ? content.substring(0, 150) + '...' : content;
|
|
3060
|
+
currentMatches.push(`Line ${lineNum}: ${truncatedContent}`);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
// Add last file
|
|
3065
|
+
if (currentFile && currentMatches.length > 0) {
|
|
3066
|
+
results.push({
|
|
3067
|
+
file: currentFile,
|
|
3068
|
+
matchCount: currentMatches.length,
|
|
3069
|
+
matches: currentMatches.slice(0, 5)
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matchCount, 0);
|
|
3073
|
+
return {
|
|
3074
|
+
success: true,
|
|
3075
|
+
pattern,
|
|
3076
|
+
totalFiles: fileSet.size,
|
|
3077
|
+
totalMatches,
|
|
3078
|
+
files: results.slice(0, 10), // Max 10 files in response
|
|
3079
|
+
truncated: results.length > 10 || lines.length >= limit,
|
|
3080
|
+
message: `Found ${fileSet.size} file(s) with ${totalMatches} match(es)${results.length > 10 ? ' (showing first 10 files)' : ''}`,
|
|
3081
|
+
hint: results.length > 10 ? 'Too many results. Try adding file_extension filter or more specific pattern.' : undefined
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
catch (error) {
|
|
3085
|
+
// Grep returns exit code 1 when no matches found
|
|
3086
|
+
if (error.status === 1 || error.message?.includes('No such file')) {
|
|
3087
|
+
return {
|
|
3088
|
+
success: true,
|
|
3089
|
+
pattern,
|
|
3090
|
+
totalFiles: 0,
|
|
3091
|
+
totalMatches: 0,
|
|
3092
|
+
files: [],
|
|
3093
|
+
message: 'No matches found'
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
return {
|
|
3097
|
+
success: false,
|
|
3098
|
+
error: `Search failed: ${error.message}`
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2653
3102
|
// File system utilities
|
|
2654
3103
|
async function listFilesRecursive(dir, fileList = []) {
|
|
2655
3104
|
const files = await fs.readdir(dir);
|
|
@@ -2712,7 +3161,7 @@ function getTestFilePath(sourceFile) {
|
|
|
2712
3161
|
// No subdirectory: testDir/filename.test.ts
|
|
2713
3162
|
testPath = path.join(CONFIG.testDir, testFileName);
|
|
2714
3163
|
}
|
|
2715
|
-
console.log(` 📁 Test file path: ${testPath}`);
|
|
3164
|
+
// console.log(` 📁 Test file path: ${testPath}`);
|
|
2716
3165
|
return testPath;
|
|
2717
3166
|
}
|
|
2718
3167
|
// AI Provider implementations
|
|
@@ -2888,12 +3337,12 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
|
|
|
2888
3337
|
}
|
|
2889
3338
|
// Main conversation loop
|
|
2890
3339
|
async function generateTests(sourceFile) {
|
|
2891
|
-
console.log(`\n📝 Generating tests for: ${sourceFile}\n`);
|
|
3340
|
+
// console.log(`\n📝 Generating tests for: ${sourceFile}\n`);
|
|
2892
3341
|
// Analyze file to get all functions (with retry)
|
|
2893
3342
|
let result = analyzeFileAST(sourceFile);
|
|
2894
3343
|
// Retry once if failed
|
|
2895
3344
|
if (!result.success) {
|
|
2896
|
-
console.log('⚠️ AST analysis failed, retrying once...');
|
|
3345
|
+
// console.log('⚠️ AST analysis failed, retrying once...');
|
|
2897
3346
|
result = analyzeFileAST(sourceFile);
|
|
2898
3347
|
}
|
|
2899
3348
|
// If still failed, throw error
|
|
@@ -2910,9 +3359,9 @@ async function generateTests(sourceFile) {
|
|
|
2910
3359
|
// Log what we found
|
|
2911
3360
|
const totalFunctions = result.analysis.functions.length;
|
|
2912
3361
|
const internalFunctions = totalFunctions - exportedFunctions.length;
|
|
2913
|
-
console.log(`✅ Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
|
|
3362
|
+
// console.log(`✅ Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
|
|
2914
3363
|
if (internalFunctions > 0) {
|
|
2915
|
-
console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
|
|
3364
|
+
// console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
|
|
2916
3365
|
}
|
|
2917
3366
|
// Always use function-by-function generation
|
|
2918
3367
|
return await generateTestsForFunctions(sourceFile, functionNames);
|
|
@@ -3000,74 +3449,289 @@ async function generateTestsForFolder() {
|
|
|
3000
3449
|
async function generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists) {
|
|
3001
3450
|
// Set the expected test file path globally to prevent AI from creating per-function files
|
|
3002
3451
|
EXPECTED_TEST_FILE_PATH = testFilePath;
|
|
3003
|
-
const
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3452
|
+
const testEnv = CONFIG.testEnv;
|
|
3453
|
+
let messages;
|
|
3454
|
+
if (testEnv == "vitest") {
|
|
3455
|
+
messages = [
|
|
3456
|
+
{
|
|
3457
|
+
role: "user",
|
|
3458
|
+
content: `You are a senior developer generating production-ready Vitest unit tests for TypeScript.
|
|
3008
3459
|
|
|
3009
|
-
##
|
|
3010
|
-
|
|
3460
|
+
## TARGET
|
|
3461
|
+
Function: ${functionName} | Source: ${sourceFile}
|
|
3462
|
+
Test File: ${testFilePath} (Exists: ${testFileExists})
|
|
3011
3463
|
|
|
3012
|
-
|
|
3464
|
+
## WORKFLOW
|
|
3013
3465
|
|
|
3014
|
-
|
|
3466
|
+
### 1. ANALYSIS (Execute in order)
|
|
3467
|
+
1. analyze_file_ast(${sourceFile}, "${functionName}") → target function metadata
|
|
3468
|
+
2. get_function_ast(${sourceFile}, "${functionName}") → implementation
|
|
3469
|
+
3. get_imports_ast(${sourceFile}) → trace ALL dependencies
|
|
3470
|
+
4. get_file_preamble(${testFilePath}) → existing mocks/imports (if exists)
|
|
3471
|
+
5. For each dependency: find_file() → get_function_ast() → understand behavior
|
|
3472
|
+
6. calculate_relative_path(from: ${testFilePath}, to: import_path) → all imports
|
|
3015
3473
|
|
|
3016
|
-
|
|
3474
|
+
**Dependency Tracing (CRITICAL):**
|
|
3475
|
+
- Map EVERY function call to its import source (verify with get_imports_ast)
|
|
3476
|
+
- Create vi.mock() for EVERY imported module (even conditional usage)
|
|
3477
|
+
- Export ALL used functions from each vi.mock block
|
|
3478
|
+
- Set return values in beforeEach for ALL mocked functions
|
|
3479
|
+
- Auto-detect required named exports from AST to prevent "X is not defined"
|
|
3017
3480
|
|
|
3018
|
-
|
|
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
|
-
\\\`\\\`\\\`
|
|
3481
|
+
### 2. FILE STRUCTURE (STRICT ORDER)
|
|
3029
3482
|
|
|
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.
|
|
3036
3483
|
|
|
3037
|
-
|
|
3484
|
+
// 1. MOCKS (before ANY imports)
|
|
3485
|
+
vi.mock('module-path');
|
|
3486
|
+
vi.mock('database/index', () => ({ db: { query: vi.fn() } }));
|
|
3487
|
+
vi.mock('env', () => ({ _ENV: { KEY: 'test' } }));
|
|
3038
3488
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
|
|
3045
|
-
USER_DEL_SECRET: 'secret'
|
|
3046
|
-
})
|
|
3047
|
-
}), { virtual: true });
|
|
3489
|
+
// 2. IMPORTS
|
|
3490
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3491
|
+
import type { TypeOnly } from './types';
|
|
3492
|
+
import { ${functionName} } from 'calculated-path';
|
|
3493
|
+
import { dependency } from 'dependency-path';
|
|
3048
3494
|
|
|
3049
|
-
|
|
3495
|
+
// 3. TYPED MOCKS
|
|
3496
|
+
const mockDep = vi.mocked(dependency);
|
|
3050
3497
|
|
|
3051
|
-
|
|
3498
|
+
// 4. TESTS
|
|
3499
|
+
describe('${functionName}', () => {
|
|
3500
|
+
beforeEach(() => {
|
|
3501
|
+
// clearMocks: true in config handles cleanup
|
|
3502
|
+
mockDep.mockResolvedValue(defaultValue);
|
|
3503
|
+
});
|
|
3052
3504
|
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3505
|
+
it('should_behavior_when_condition', async () => {
|
|
3506
|
+
// ARRANGE
|
|
3507
|
+
const input = { id: 1 };
|
|
3508
|
+
mockDep.mockResolvedValueOnce(specificValue);
|
|
3509
|
+
|
|
3510
|
+
// ACT
|
|
3511
|
+
const result = await ${functionName}(input);
|
|
3512
|
+
|
|
3513
|
+
// ASSERT
|
|
3514
|
+
expect(result).toEqual(expected);
|
|
3515
|
+
expect(mockDep).toHaveBeenCalledWith(input);
|
|
3516
|
+
expect(mockDep).toHaveBeenCalledTimes(1);
|
|
3517
|
+
});
|
|
3518
|
+
});
|
|
3056
3519
|
|
|
3057
|
-
// ===== TYPED MOCKS =====
|
|
3058
|
-
const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
|
|
3059
3520
|
|
|
3060
|
-
|
|
3521
|
+
### 3. COVERAGE (Minimum 5 tests)
|
|
3061
3522
|
|
|
3062
|
-
|
|
3063
|
-
-
|
|
3064
|
-
|
|
3523
|
+
1. **Happy Path** (1-2): Valid inputs → expected outputs
|
|
3524
|
+
2. **Edge Cases** (2-3): Empty/null/undefined/0/boundaries/special chars
|
|
3525
|
+
3. **Error Handling** (1-2): Invalid inputs, dependency failures
|
|
3526
|
+
- Sync: expect(() => fn()).toThrow(ErrorClass)
|
|
3527
|
+
- Async: await expect(fn()).rejects.toThrow(ErrorClass)
|
|
3528
|
+
4. **Branch Coverage**: Each conditional path tested
|
|
3529
|
+
5. **Async Behavior**: Promise resolution/rejection (if applicable)
|
|
3530
|
+
|
|
3531
|
+
### 4. MOCK STRATEGY
|
|
3532
|
+
|
|
3533
|
+
**ALWAYS Mock:**
|
|
3534
|
+
- External modules (fs, http, database)
|
|
3535
|
+
- Modules with side effects (logging, analytics)
|
|
3536
|
+
- database/index, env, WinstonLogger
|
|
3537
|
+
|
|
3538
|
+
**Consider NOT Mocking:**
|
|
3539
|
+
- Pure utility functions from same codebase (test integration)
|
|
3540
|
+
- Type imports: import type { X } (NEVER mock)
|
|
3541
|
+
|
|
3542
|
+
**Mock Pattern:**
|
|
3543
|
+
- Module-level: vi.mock('path') → const mockFn = vi.mocked(importedFn)
|
|
3544
|
+
- NEVER: vi.spyOn(exports, 'fn') or global wrappers
|
|
3545
|
+
- Hoist mock functions for use in both factory and tests
|
|
3546
|
+
|
|
3547
|
+
### 5. ASSERTIONS
|
|
3548
|
+
|
|
3549
|
+
**Priority Order:**
|
|
3550
|
+
1. **Exact**: toEqual(), toBe() for primitives
|
|
3551
|
+
2. **Partial**: toMatchObject() for subset matching
|
|
3552
|
+
3. **Structural**: expect.objectContaining(), expect.any(Type)
|
|
3553
|
+
4. **Specific Checks**: toBeDefined(), toBeNull(), toHaveLength(n)
|
|
3554
|
+
5. **Mock Verification**: toHaveBeenCalledWith(), toHaveBeenCalledTimes()
|
|
3555
|
+
|
|
3556
|
+
**NEVER:**
|
|
3557
|
+
- toBeTruthy() for object existence (use toBeDefined())
|
|
3558
|
+
- Snapshots for dates, random values, or as primary assertions
|
|
3559
|
+
|
|
3560
|
+
### 6. CRITICAL RULES
|
|
3561
|
+
|
|
3562
|
+
**MUST:**
|
|
3563
|
+
- ✅ ALL vi.mock() before imports
|
|
3564
|
+
- ✅ Use calculate_relative_path for ALL imports
|
|
3565
|
+
- ✅ 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.
|
|
3566
|
+
- ✅ When editing existing file: UPDATE existing vi.mock, NEVER duplicate
|
|
3567
|
+
- ✅ Test names: should_behavior_when_condition
|
|
3568
|
+
- ✅ AAA pattern with comments
|
|
3569
|
+
- ✅ Each test = one behavior
|
|
3570
|
+
- ✅ Import types separately: import type { Config }
|
|
3571
|
+
- ✅ Use vi.mocked<typeof module>() for full type inference
|
|
3572
|
+
- ✅ Mock internal non-exported functions
|
|
3573
|
+
- ✅ Use vi.useFakeTimers() for time-dependent tests
|
|
3574
|
+
- ✅ Test cases expectations must match source code
|
|
3575
|
+
|
|
3576
|
+
|
|
3577
|
+
**NEVER:**
|
|
3578
|
+
- ❌ Mock after imports
|
|
3579
|
+
- ❌ Shared state between tests
|
|
3580
|
+
- ❌ Multiple behaviors in one test
|
|
3581
|
+
- ❌ Generic test names ("test1", "works")
|
|
3582
|
+
- ❌ Manual cleanup (vi.clearAllMocks in tests - config handles it)
|
|
3583
|
+
- ❌ Environment dependencies without mocks
|
|
3584
|
+
- ❌ Use require() (ES imports only)
|
|
3585
|
+
- ❌ Reference function from wrong module (verify with get_imports_ast)
|
|
3586
|
+
- ❌ Change existing mocks in ways that break other tests\
|
|
3587
|
+
|
|
3588
|
+
|
|
3589
|
+
### 7. EXECUTION
|
|
3590
|
+
|
|
3591
|
+
upsert_function_tests({
|
|
3592
|
+
test_file_path: "${testFilePath}",
|
|
3593
|
+
function_name: "${functionName}",
|
|
3594
|
+
new_test_content: "..." // complete test code
|
|
3595
|
+
});
|
|
3596
|
+
|
|
3597
|
+
run_tests({
|
|
3598
|
+
test_file_path: "${testFilePath}",
|
|
3599
|
+
function_names: ["${functionName}"]
|
|
3600
|
+
});
|
|
3601
|
+
|
|
3602
|
+
|
|
3603
|
+
### 8. FAILURE DEBUGGING
|
|
3604
|
+
|
|
3605
|
+
**Import Errors:**
|
|
3606
|
+
- Recalculate paths with calculate_relative_path
|
|
3607
|
+
- Check barrel exports (index.ts redirects)
|
|
3608
|
+
- Verify source file exports function
|
|
3609
|
+
|
|
3610
|
+
**Mock Errors:**
|
|
3611
|
+
- Add missing vi.mock() at top
|
|
3612
|
+
- Ensure all used functions exported from mock
|
|
3613
|
+
- Verify mock setup in beforeEach
|
|
3614
|
+
|
|
3615
|
+
**Type Errors:**
|
|
3616
|
+
- Import types with import type
|
|
3617
|
+
- Check mock return types match signatures
|
|
3618
|
+
- Use proper generic constraints
|
|
3619
|
+
|
|
3620
|
+
**Assertion Failures:**
|
|
3621
|
+
- Log mock calls: console.log(mockFn.mock.calls)
|
|
3622
|
+
- Check execution path (add temp logs in source)
|
|
3623
|
+
- Verify input data types
|
|
3624
|
+
- Check async/await usage
|
|
3625
|
+
- Validate prerequisite mocks return expected values
|
|
3626
|
+
|
|
3627
|
+
** If fails, categorize:
|
|
3628
|
+
|
|
3629
|
+
**[MUST] FIXABLE** → Fix these:
|
|
3630
|
+
| Error | Fix Method |
|
|
3631
|
+
|-------|-----------|
|
|
3632
|
+
| Wrong imports | find_file(fileName) to get the file path + calculate_relative_path + search_replace_block |
|
|
3633
|
+
| Missing mocks | insert_at_position |
|
|
3634
|
+
| Syntax errors | search_replace_block (3-5 lines context) |
|
|
3635
|
+
| Mock pollution | Fix beforeEach pattern |
|
|
3636
|
+
| "Test suite failed to run" | get_file_preamble + fix imports/mocks |
|
|
3637
|
+
| "Cannot find module" | calculate_relative_path |
|
|
3638
|
+
|
|
3639
|
+
**Process:**
|
|
3640
|
+
1. Read FULL error message
|
|
3641
|
+
2. Identify error type (import/mock/assertion/type)
|
|
3642
|
+
3. Make focused fix using available tools
|
|
3643
|
+
4. Iterate until ALL PASS
|
|
3644
|
+
|
|
3645
|
+
### 9. QUALITY CHECKLIST
|
|
3646
|
+
- [ ] Independent tests (no execution order dependency)
|
|
3647
|
+
- [ ] Fast (<100ms per test, no real I/O)
|
|
3648
|
+
- [ ] Readable (AAA, descriptive names)
|
|
3649
|
+
- [ ] Focused (one behavior per test)
|
|
3650
|
+
- [ ] Deterministic (same input = same output)
|
|
3651
|
+
- [ ] Type-safe (no any, proper generics)
|
|
3652
|
+
- [ ] Complete coverage (all paths tested)
|
|
3653
|
+
- [ ] No duplicate declarations
|
|
3654
|
+
- [ ] Existing tests still pass
|
|
3655
|
+
|
|
3656
|
+
## START
|
|
3657
|
+
\`analyze_file_ast\` → gather context → **write tests immediately** → verify → run → fix → complete.
|
|
3658
|
+
|
|
3659
|
+
**[CRITICAL]** NEVER change existing mocks such that other tests fail. Use test-specific overrides with mockReturnValueOnce.
|
|
3660
|
+
You must be efficient and fast in your approach, do not overthink the problem.`,
|
|
3661
|
+
},
|
|
3662
|
+
];
|
|
3663
|
+
}
|
|
3664
|
+
else {
|
|
3665
|
+
messages = [
|
|
3666
|
+
{
|
|
3667
|
+
role: "user",
|
|
3668
|
+
content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
|
|
3669
|
+
[Critical] Be prompt and efficient in your response. Make sure the test case file is typed and complete. Be as fast as possible in your repsonse.
|
|
3670
|
+
[Critical] You cannot remove or modify the existing mocks and imports in the test file since other test may be using it. You can only add new mocks and imports for the new test cases.
|
|
3671
|
+
|
|
3672
|
+
## CONTEXT
|
|
3673
|
+
Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
3674
|
+
|
|
3675
|
+
⚠️ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
|
|
3676
|
+
|
|
3677
|
+
---
|
|
3678
|
+
|
|
3679
|
+
## EXECUTION PLAN
|
|
3680
|
+
|
|
3681
|
+
**Phase 1: Deep Analysis**
|
|
3682
|
+
\\\`\\\`\\\`
|
|
3683
|
+
1. analyze_file_ast(${sourceFile}) → function metadata.
|
|
3684
|
+
2. get_function_ast(${sourceFile},{functionName}) → implementation + dependencies
|
|
3685
|
+
3. For each dependency:
|
|
3686
|
+
- Same file: get_function_ast(${sourceFile},{functionName})
|
|
3687
|
+
- 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
|
|
3688
|
+
4. get_imports_ast → all dependencies
|
|
3689
|
+
5. calculate_relative_path(from: ${testFilePath}, to: import_path) → all imports, accpets multiple comma separated 'to' paths. Use exact path returned by this tool for all imports.
|
|
3690
|
+
6. get_file_preamble → imports and mocks already declared in the file
|
|
3691
|
+
7. search_codebase → look for relevant context in codebase.
|
|
3692
|
+
\\\`\\\`\\\`
|
|
3693
|
+
|
|
3694
|
+
**Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
|
|
3695
|
+
*Before writing tests, map the logic requirements for external calls.*
|
|
3696
|
+
1. Identify every external call (e.g., \`analyticsHelper.postEvent\`).
|
|
3697
|
+
2. Trace backwards: What \`if\`, \`switch\`, or \`try/catch\` block guards this call?
|
|
3698
|
+
3. Identify the dependency that controls that guard.
|
|
3699
|
+
4. Plan the Mock Return: Determine exactly what value the dependency must return to enter that block.
|
|
3700
|
+
|
|
3701
|
+
**Phase 2: Test Generation**
|
|
3702
|
+
|
|
3703
|
+
Mock Pattern (CRITICAL - Top of file):
|
|
3704
|
+
\\\`\\\`\\\`typescript
|
|
3705
|
+
// ===== MOCKS (BEFORE IMPORTS) =====
|
|
3706
|
+
jest.mock('config', () => ({
|
|
3707
|
+
get: (key: string) => ({
|
|
3708
|
+
AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
|
|
3709
|
+
USER_DEL_SECRET: 'secret'
|
|
3710
|
+
})
|
|
3711
|
+
}), { virtual: true });
|
|
3712
|
+
|
|
3713
|
+
// virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
|
|
3714
|
+
|
|
3715
|
+
jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
|
|
3716
|
+
|
|
3717
|
+
// ===== IMPORTS =====
|
|
3718
|
+
import { functionName } from '../controller';
|
|
3719
|
+
import { dependencyMethod } from '../helpers/dependency';
|
|
3720
|
+
|
|
3721
|
+
// ===== TYPED MOCKS =====
|
|
3722
|
+
const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
|
|
3723
|
+
|
|
3724
|
+
\\\`\\\`\\\`
|
|
3725
|
+
|
|
3726
|
+
Requirements (5+ tests minimum):
|
|
3727
|
+
- ✅ Happy path
|
|
3728
|
+
- 🔸 Edge cases (null, undefined, empty)
|
|
3065
3729
|
- ❌ Error conditions
|
|
3066
3730
|
- ⏱️ Async behavior
|
|
3067
3731
|
- 🔍 API null/undefined handling
|
|
3068
3732
|
|
|
3069
3733
|
/**
|
|
3070
|
-
* Phase 3: Anti-Pollution Pattern (
|
|
3734
|
+
* Phase 3: Anti-Pollution Pattern (MUST FOLLOW EXACTLY THIS PATTERN, NO VARIATIONS)
|
|
3071
3735
|
*/
|
|
3072
3736
|
|
|
3073
3737
|
\\\`\\\`\\\`typescript
|
|
@@ -3188,14 +3852,8 @@ All functions from the same source file MUST share the same test file.
|
|
|
3188
3852
|
| "Test suite failed to run" | get_file_preamble + fix imports/mocks |
|
|
3189
3853
|
| "Cannot find module" | calculate_relative_path |
|
|
3190
3854
|
|
|
3191
|
-
**LEGITIMATE** → Report, don't fix:
|
|
3192
|
-
- Source returns wrong type
|
|
3193
|
-
- Missing null checks in source
|
|
3194
|
-
- Logic errors in source
|
|
3195
|
-
|
|
3196
|
-
⛔ NEVER report "Test suite failed to run" as legitimate
|
|
3197
3855
|
|
|
3198
|
-
3️⃣ Repeat until: ✅
|
|
3856
|
+
3️⃣ Repeat until: ✅ All test cases pass
|
|
3199
3857
|
|
|
3200
3858
|
---
|
|
3201
3859
|
|
|
@@ -3207,11 +3865,14 @@ All functions from the same source file MUST share the same test file.
|
|
|
3207
3865
|
- Ensure test independence (no pollution)
|
|
3208
3866
|
- Fix test bugs, report source bugs
|
|
3209
3867
|
- [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,
|
|
3868
|
+
- Test file exists: ${testFileExists} - if the test file exist, always 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.
|
|
3869
|
+
- Mocking of winston logger or any other external dependeny is critical and mandatory.
|
|
3870
|
+
- Use search_codebase tool to look for relevant context in codebase quickly.
|
|
3211
3871
|
|
|
3212
|
-
**START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file
|
|
3213
|
-
|
|
3214
|
-
|
|
3872
|
+
**START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file. Analyze deeply and write test cases only when you are sure about the function and the dependencies. Make sure the written test cases run and pass on first attempt.`,
|
|
3873
|
+
},
|
|
3874
|
+
];
|
|
3875
|
+
}
|
|
3215
3876
|
let iterations = 0;
|
|
3216
3877
|
const maxIterations = 100;
|
|
3217
3878
|
let testFileWritten = false;
|
|
@@ -3220,16 +3881,25 @@ All functions from the same source file MUST share the same test file.
|
|
|
3220
3881
|
let lastTestError = '';
|
|
3221
3882
|
let sameErrorCount = 0;
|
|
3222
3883
|
while (iterations < maxIterations) {
|
|
3223
|
-
console.log('USING CLAUDE PROMPT original 16');
|
|
3224
3884
|
iterations++;
|
|
3225
3885
|
if (iterations === 1) {
|
|
3226
|
-
|
|
3886
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
3887
|
+
globalSpinner = ora('🤖 AI is analyzing selected functions...').start();
|
|
3888
|
+
}
|
|
3889
|
+
else {
|
|
3890
|
+
globalSpinner.text = '🤖 AI is analyzing selected functions...';
|
|
3891
|
+
}
|
|
3227
3892
|
}
|
|
3228
3893
|
else if (iterations % 5 === 0) {
|
|
3229
|
-
|
|
3894
|
+
if (globalSpinner) {
|
|
3895
|
+
globalSpinner.text = `🤖 AI is still working (step ${iterations})...`;
|
|
3896
|
+
}
|
|
3230
3897
|
}
|
|
3898
|
+
// console.log('messages', messages);
|
|
3899
|
+
// console.log('TOOLS_FOR_TEST_GENERATION', TOOLS_FOR_TEST_GENERATION);
|
|
3231
3900
|
const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
|
|
3232
3901
|
// console.log('response from AI', JSON.stringify(response, null, 2));
|
|
3902
|
+
// console.log('TEst file path', testFilePath);
|
|
3233
3903
|
if (response.content) {
|
|
3234
3904
|
const content = response.content;
|
|
3235
3905
|
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
@@ -3243,7 +3913,7 @@ All functions from the same source file MUST share the same test file.
|
|
|
3243
3913
|
/beyond my capabilities/i,
|
|
3244
3914
|
/can't execute/i
|
|
3245
3915
|
];
|
|
3246
|
-
const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
|
|
3916
|
+
const isMakingExcuses = excusePatterns.some(pattern => typeof content === 'string' && pattern.test(content));
|
|
3247
3917
|
if (isMakingExcuses) {
|
|
3248
3918
|
console.log('\n⚠️ AI is making excuses! Forcing it to use tools...');
|
|
3249
3919
|
messages.push({
|
|
@@ -3293,7 +3963,7 @@ This works for both NEW and EXISTING test files!`
|
|
|
3293
3963
|
4. Then run_tests to verify
|
|
3294
3964
|
|
|
3295
3965
|
📌 ALTERNATIVE: Use insert_at_position for adding imports/mocks
|
|
3296
|
-
- insert_at_position({ position: 'after_imports', content: "
|
|
3966
|
+
- insert_at_position({ position: 'after_imports', content: "vi.mock('../module');" })
|
|
3297
3967
|
|
|
3298
3968
|
⚠️ SECONDARY: Use upsert_function_tests for function-level rewrites
|
|
3299
3969
|
|
|
@@ -3325,16 +3995,13 @@ Start NOW with search_replace_block or insert_at_position!`
|
|
|
3325
3995
|
const currentError = errorOutput.substring(0, 300);
|
|
3326
3996
|
if (currentError === lastTestError) {
|
|
3327
3997
|
sameErrorCount++;
|
|
3328
|
-
console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
|
|
3998
|
+
// console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
|
|
3329
3999
|
if (sameErrorCount >= 3) {
|
|
3330
|
-
console.log('\n🚨 Same error repeated 3+ times! ');
|
|
4000
|
+
// console.log('\n🚨 Same error repeated 3+ times! ');
|
|
3331
4001
|
messages.push({
|
|
3332
4002
|
role: 'user',
|
|
3333
4003
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
If this is a source code bug: Call report_legitimate_failure tool NOW.
|
|
3337
|
-
If this is still fixable: Make focused attempt to fix it.`
|
|
4004
|
+
Make focused attempt to fix the tests using the tools available.`
|
|
3338
4005
|
});
|
|
3339
4006
|
}
|
|
3340
4007
|
}
|
|
@@ -3347,7 +4014,7 @@ If this is still fixable: Make focused attempt to fix it.`
|
|
|
3347
4014
|
if (toolCall.name === 'upsert_function_tests') {
|
|
3348
4015
|
if (result.success) {
|
|
3349
4016
|
testFileWritten = true;
|
|
3350
|
-
console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
4017
|
+
// console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
3351
4018
|
messages.push({
|
|
3352
4019
|
role: 'user',
|
|
3353
4020
|
content: `Test files are written successfully. Please use run_tests tool to verify the tests. If the tests fail, please make focused attempts to fix the tests using the tools available.`
|
|
@@ -3451,16 +4118,32 @@ async function smartValidateTestSuite(sourceFile, testFilePath, functionNames) {
|
|
|
3451
4118
|
console.log(`🔍 VALIDATION: Running full test suite (${functionNames.length} function(s))`);
|
|
3452
4119
|
console.log(`${'='.repeat(80)}\n`);
|
|
3453
4120
|
// Run tests for entire file (no function filter)
|
|
3454
|
-
|
|
4121
|
+
let fullSuiteResult;
|
|
4122
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4123
|
+
fullSuiteResult = runTestsVitest(testFilePath);
|
|
4124
|
+
}
|
|
4125
|
+
else {
|
|
4126
|
+
fullSuiteResult = runTestsJest(testFilePath);
|
|
4127
|
+
}
|
|
3455
4128
|
if (fullSuiteResult.passed) {
|
|
3456
4129
|
console.log(`\n✅ Full test suite passed! All ${functionNames.length} function(s) working together correctly.`);
|
|
3457
4130
|
return;
|
|
3458
4131
|
}
|
|
3459
4132
|
console.log(`\n⚠️ Full test suite has failures. Attempting to fix failing tests...\n`);
|
|
3460
|
-
// Parse failing test names from
|
|
3461
|
-
|
|
4133
|
+
// Parse failing test names from Vitest output
|
|
4134
|
+
let failingTests;
|
|
4135
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4136
|
+
failingTests = parseFailingTestNamesVitest(fullSuiteResult.output);
|
|
4137
|
+
}
|
|
4138
|
+
else {
|
|
4139
|
+
failingTests = parseFailingTestNamesJest(fullSuiteResult.output);
|
|
4140
|
+
}
|
|
4141
|
+
console.log(`\n📊 Debug: Found ${failingTests.length} failing test(s) from output`);
|
|
3462
4142
|
if (failingTests.length === 0) {
|
|
3463
|
-
console.log('⚠️ Could not parse specific failing test names
|
|
4143
|
+
console.log('⚠️ Could not parse specific failing test names from output.');
|
|
4144
|
+
console.log(' Attempting general fix based on full error output...\n');
|
|
4145
|
+
// Fallback: Still attempt to fix using the full output even without parsed test names
|
|
4146
|
+
await fixFailingTests(sourceFile, testFilePath, functionNames, [], fullSuiteResult.output);
|
|
3464
4147
|
return;
|
|
3465
4148
|
}
|
|
3466
4149
|
console.log(`Found ${failingTests.length} failing test(s): ${failingTests.join(', ')}\n`);
|
|
@@ -3475,7 +4158,7 @@ async function fixFailingTests(sourceFile, testFilePath, functionNames, failingT
|
|
|
3475
4158
|
const messages = [
|
|
3476
4159
|
{
|
|
3477
4160
|
role: 'user',
|
|
3478
|
-
content: `You are fixing FAILING TESTS in the test suite.
|
|
4161
|
+
content: `You are fixing FAILING TESTS in the Vitest test suite.
|
|
3479
4162
|
|
|
3480
4163
|
Source file: ${sourceFile}
|
|
3481
4164
|
Test file: ${testFilePath}
|
|
@@ -3490,22 +4173,21 @@ ${fullSuiteOutput}
|
|
|
3490
4173
|
YOUR TASK - Fix all failing tests:
|
|
3491
4174
|
|
|
3492
4175
|
COMMON ISSUES TO FIX:
|
|
3493
|
-
- Missing jest.resetAllMocks() in beforeEach (should be first line)
|
|
3494
|
-
- Missing jest.restoreAllMocks() in global afterEach
|
|
3495
4176
|
- Mock state bleeding between describe blocks
|
|
3496
|
-
-
|
|
3497
|
-
-
|
|
3498
|
-
- beforeEach not
|
|
3499
|
-
- afterEach not cleaning up spies
|
|
4177
|
+
- Missing vitest imports (describe, it, expect, beforeEach, vi)
|
|
4178
|
+
- Incorrect mock typing (use MockedFunction from vitest)
|
|
4179
|
+
- beforeEach not setting up mocks properly
|
|
3500
4180
|
- Missing or incorrect imports
|
|
3501
4181
|
- Mock implementation issues
|
|
3502
4182
|
- Incorrect test assertions
|
|
3503
4183
|
- Test logic errors
|
|
3504
4184
|
|
|
4185
|
+
NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled.
|
|
4186
|
+
|
|
3505
4187
|
TOOLS TO USE:
|
|
3506
4188
|
1. get_file_preamble - See current setup
|
|
3507
4189
|
2. search_replace_block - Fix specific sections (preferred)
|
|
3508
|
-
3. insert_at_position - Add missing
|
|
4190
|
+
3. insert_at_position - Add missing imports/mocks
|
|
3509
4191
|
4. run_tests - Verify fixes
|
|
3510
4192
|
|
|
3511
4193
|
START by calling get_file_preamble to see the current test structure.`
|
|
@@ -3522,7 +4204,13 @@ START by calling get_file_preamble to see the current test structure.`
|
|
|
3522
4204
|
}
|
|
3523
4205
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3524
4206
|
// AI stopped - check if tests pass now
|
|
3525
|
-
|
|
4207
|
+
let finalTest;
|
|
4208
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4209
|
+
finalTest = runTestsVitest(testFilePath);
|
|
4210
|
+
}
|
|
4211
|
+
else {
|
|
4212
|
+
finalTest = runTestsJest(testFilePath);
|
|
4213
|
+
}
|
|
3526
4214
|
if (finalTest.passed) {
|
|
3527
4215
|
console.log('\n✅ Tests fixed! Full test suite now passes.');
|
|
3528
4216
|
return;
|
|
@@ -3609,7 +4297,13 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
|
|
|
3609
4297
|
console.log(`🔍 FINAL VALIDATION: Running complete test suite`);
|
|
3610
4298
|
console.log(`${'='.repeat(80)}\n`);
|
|
3611
4299
|
// Run tests for entire file (no function filter)
|
|
3612
|
-
|
|
4300
|
+
let testResult;
|
|
4301
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4302
|
+
testResult = runTestsVitest(testFilePath);
|
|
4303
|
+
}
|
|
4304
|
+
else {
|
|
4305
|
+
testResult = runTestsJest(testFilePath);
|
|
4306
|
+
}
|
|
3613
4307
|
if (testResult.passed) {
|
|
3614
4308
|
console.log(`\n✅ Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
|
|
3615
4309
|
return;
|
|
@@ -3619,7 +4313,7 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
|
|
|
3619
4313
|
const messages = [
|
|
3620
4314
|
{
|
|
3621
4315
|
role: 'user',
|
|
3622
|
-
content: `You are a senior software engineer fixing file-level integration issues in a
|
|
4316
|
+
content: `You are a senior software engineer fixing file-level integration issues in a Vitest test file.
|
|
3623
4317
|
|
|
3624
4318
|
Source file: ${sourceFile}
|
|
3625
4319
|
Test file: ${testFilePath}
|
|
@@ -3632,7 +4326,7 @@ CONTEXT:
|
|
|
3632
4326
|
* Mock pollution between test suites
|
|
3633
4327
|
* Shared state not being cleaned up
|
|
3634
4328
|
* Import/mock ordering issues
|
|
3635
|
-
* beforeEach
|
|
4329
|
+
* beforeEach setup issues
|
|
3636
4330
|
* Mock implementations interfering with each other
|
|
3637
4331
|
|
|
3638
4332
|
TEST OUTPUT:
|
|
@@ -3643,35 +4337,27 @@ YOUR TASK:
|
|
|
3643
4337
|
2. Identify file-level issues (NOT individual function logic issues)
|
|
3644
4338
|
3. Fix using search_replace_block or insert_at_position tools
|
|
3645
4339
|
4. Run tests again with run_tests tool
|
|
3646
|
-
5. Repeat until
|
|
4340
|
+
5. Repeat until all test cases pass
|
|
3647
4341
|
|
|
3648
4342
|
COMMON FILE-LEVEL ISSUES TO CHECK:
|
|
3649
|
-
- ❌ Missing
|
|
3650
|
-
- ❌
|
|
3651
|
-
- ❌
|
|
3652
|
-
- ❌ Missing virtual:true for config/database/models mocks
|
|
3653
|
-
- ❌ beforeEach hooks not clearing all shared state
|
|
4343
|
+
- ❌ Missing vitest imports (describe, it, expect, beforeEach, vi)
|
|
4344
|
+
- ❌ Module imports inside describe blocks (use await import() in beforeEach)
|
|
4345
|
+
- ❌ beforeEach hooks not setting up mocks properly
|
|
3654
4346
|
- ❌ Test suites depending on execution order
|
|
3655
4347
|
|
|
4348
|
+
NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled for auto-cleanup.
|
|
4349
|
+
|
|
3656
4350
|
FIXABLE ISSUES (you should fix):
|
|
3657
4351
|
- Mock pollution between test suites
|
|
3658
|
-
- Missing cleanup in beforeEach/afterEach
|
|
3659
4352
|
- Incorrect mock setup at file level
|
|
3660
4353
|
- Import ordering issues
|
|
3661
|
-
- Missing
|
|
3662
|
-
|
|
3663
|
-
LEGITIMATE FAILURES (report these):
|
|
3664
|
-
- Source code bugs causing actual logic errors
|
|
3665
|
-
- Missing null checks in source code
|
|
3666
|
-
- Wrong return types from source functions
|
|
3667
|
-
- Use report_legitimate_failure tool for these
|
|
4354
|
+
- Missing vitest imports
|
|
3668
4355
|
|
|
3669
4356
|
CRITICAL RULES:
|
|
3670
4357
|
1. DO NOT change individual test logic (they passed individually!)
|
|
3671
4358
|
2. Focus ONLY on file-level integration issues
|
|
3672
4359
|
3. Use search_replace_block to fix specific sections
|
|
3673
4360
|
4. Preserve all existing test cases
|
|
3674
|
-
5. If failures are due to source code bugs, call report_legitimate_failure and STOP
|
|
3675
4361
|
|
|
3676
4362
|
START by calling get_file_preamble to understand current file structure.`
|
|
3677
4363
|
}
|
|
@@ -3689,7 +4375,13 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
3689
4375
|
}
|
|
3690
4376
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3691
4377
|
// AI stopped without fixing - check if tests pass now
|
|
3692
|
-
|
|
4378
|
+
let finalTest;
|
|
4379
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4380
|
+
finalTest = runTestsVitest(testFilePath);
|
|
4381
|
+
}
|
|
4382
|
+
else {
|
|
4383
|
+
finalTest = runTestsJest(testFilePath);
|
|
4384
|
+
}
|
|
3693
4385
|
if (finalTest.passed) {
|
|
3694
4386
|
console.log('\n✅ Complete test suite now passes!');
|
|
3695
4387
|
return;
|
|
@@ -3697,7 +4389,7 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
3697
4389
|
console.log('\n⚠️ AI stopped but tests still failing. Prompting to continue...');
|
|
3698
4390
|
messages.push({
|
|
3699
4391
|
role: 'user',
|
|
3700
|
-
content: 'Tests are still failing! Use tools to fix
|
|
4392
|
+
content: 'Tests are still failing! Use tools to fix.'
|
|
3701
4393
|
});
|
|
3702
4394
|
continue;
|
|
3703
4395
|
}
|
|
@@ -3787,7 +4479,7 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
3787
4479
|
* Generate tests for multiple functions, one at a time
|
|
3788
4480
|
*/
|
|
3789
4481
|
async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
3790
|
-
console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
|
|
4482
|
+
// console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
|
|
3791
4483
|
const testFilePath = getTestFilePath(sourceFile);
|
|
3792
4484
|
let testFileExists = fsSync.existsSync(testFilePath);
|
|
3793
4485
|
// Read validation interval from config
|
|
@@ -3795,12 +4487,22 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
|
3795
4487
|
// Process each function one at a time
|
|
3796
4488
|
for (let i = 0; i < functionNames.length; i++) {
|
|
3797
4489
|
const functionName = functionNames[i];
|
|
4490
|
+
// Clear spinner before showing section header
|
|
4491
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4492
|
+
globalSpinner.stop();
|
|
4493
|
+
globalSpinner = null;
|
|
4494
|
+
}
|
|
3798
4495
|
console.log(`\n${'='.repeat(80)}`);
|
|
3799
4496
|
console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
|
|
3800
4497
|
console.log(`${'='.repeat(80)}\n`);
|
|
3801
4498
|
const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
|
|
3802
4499
|
// After first function completes, test file will exist for subsequent functions
|
|
3803
4500
|
testFileExists = true;
|
|
4501
|
+
// Clear spinner before showing completion
|
|
4502
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4503
|
+
globalSpinner.stop();
|
|
4504
|
+
globalSpinner = null;
|
|
4505
|
+
}
|
|
3804
4506
|
if (passed) {
|
|
3805
4507
|
console.log(`\n✅ Function '${functionName}' tests completed successfully!`);
|
|
3806
4508
|
}
|
|
@@ -3828,13 +4530,23 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
|
3828
4530
|
async function generateTestsForFunction() {
|
|
3829
4531
|
console.log('\n🎯 Function-wise Test Generation\n');
|
|
3830
4532
|
// List all files
|
|
3831
|
-
|
|
4533
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4534
|
+
globalSpinner = ora('📂 Scanning repository...').start();
|
|
4535
|
+
}
|
|
4536
|
+
else {
|
|
4537
|
+
globalSpinner.text = '📂 Scanning repository...';
|
|
4538
|
+
}
|
|
3832
4539
|
const files = await listFilesRecursive('.');
|
|
3833
4540
|
if (files.length === 0) {
|
|
3834
|
-
|
|
4541
|
+
globalSpinner.fail('No source files found!');
|
|
4542
|
+
globalSpinner = null;
|
|
3835
4543
|
return;
|
|
3836
4544
|
}
|
|
3837
|
-
|
|
4545
|
+
globalSpinner.text = `Found ${files.length} source file(s)`;
|
|
4546
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4547
|
+
globalSpinner.stop();
|
|
4548
|
+
globalSpinner = null;
|
|
4549
|
+
console.log('\nSelect a file:\n');
|
|
3838
4550
|
files.forEach((file, index) => {
|
|
3839
4551
|
console.log(`${index + 1}. ${file}`);
|
|
3840
4552
|
});
|
|
@@ -3870,6 +4582,11 @@ async function generateTestsForFunction() {
|
|
|
3870
4582
|
}
|
|
3871
4583
|
console.log(`\n✅ Selected functions: ${selectedFunctions.join(', ')}\n`);
|
|
3872
4584
|
await generateTestsForFunctions(selectedFile, selectedFunctions);
|
|
4585
|
+
// Clear spinner before final message
|
|
4586
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4587
|
+
globalSpinner.stop();
|
|
4588
|
+
globalSpinner = null;
|
|
4589
|
+
}
|
|
3873
4590
|
console.log('\n✨ Done!');
|
|
3874
4591
|
}
|
|
3875
4592
|
/**
|
|
@@ -3997,7 +4714,7 @@ Return ONLY the JSON array, nothing else.`;
|
|
|
3997
4714
|
return [];
|
|
3998
4715
|
}
|
|
3999
4716
|
// Parse AI response to extract function names
|
|
4000
|
-
const content = response.content.trim();
|
|
4717
|
+
const content = typeof response.content === 'string' ? response.content.trim() : JSON.stringify(response.content || '');
|
|
4001
4718
|
// console.log(` 🤖 AI response: ${content}`);
|
|
4002
4719
|
// Try to extract JSON array from response
|
|
4003
4720
|
const jsonMatch = content.match(/\[.*\]/s);
|
|
@@ -4029,16 +4746,25 @@ Return ONLY the JSON array, nothing else.`;
|
|
|
4029
4746
|
* Auto-generate tests for changed functions detected via git diff
|
|
4030
4747
|
*/
|
|
4031
4748
|
async function autoGenerateTests() {
|
|
4032
|
-
|
|
4749
|
+
if (!globalSpinner) {
|
|
4750
|
+
globalSpinner = ora('🔍 Scanning git changes...').start();
|
|
4751
|
+
}
|
|
4752
|
+
else {
|
|
4753
|
+
globalSpinner.text = '🔍 Scanning git changes...';
|
|
4754
|
+
globalSpinner.start();
|
|
4755
|
+
}
|
|
4033
4756
|
try {
|
|
4034
4757
|
// Get all changes from git diff
|
|
4035
4758
|
const { fullDiff, files } = await getGitDiff();
|
|
4036
4759
|
if (files.length === 0) {
|
|
4760
|
+
globalSpinner.stop();
|
|
4761
|
+
globalSpinner = null;
|
|
4037
4762
|
console.log('✅ No changes detected in source files.');
|
|
4038
4763
|
console.log(' (Only staged and unstaged changes are checked)');
|
|
4039
4764
|
return;
|
|
4040
4765
|
}
|
|
4041
|
-
|
|
4766
|
+
globalSpinner.text = `Found changes in ${files.length} file(s)`;
|
|
4767
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4042
4768
|
let totalFunctions = 0;
|
|
4043
4769
|
let processedFiles = 0;
|
|
4044
4770
|
let errorFiles = 0;
|
|
@@ -4047,25 +4773,41 @@ async function autoGenerateTests() {
|
|
|
4047
4773
|
const { filePath, diff } = fileInfo;
|
|
4048
4774
|
// Check if file exists
|
|
4049
4775
|
if (!fsSync.existsSync(filePath)) {
|
|
4050
|
-
|
|
4776
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4777
|
+
globalSpinner = ora(`⏭️ Skipping ${filePath} (file not found)`).start();
|
|
4778
|
+
}
|
|
4779
|
+
else {
|
|
4780
|
+
globalSpinner.text = `⏭️ Skipping ${filePath} (file not found)`;
|
|
4781
|
+
}
|
|
4782
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
4051
4783
|
continue;
|
|
4052
4784
|
}
|
|
4053
|
-
|
|
4054
|
-
|
|
4785
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4786
|
+
globalSpinner = ora(`🔄 Processing: ${path.basename(filePath)}`).start();
|
|
4787
|
+
}
|
|
4788
|
+
else {
|
|
4789
|
+
globalSpinner.text = `🔄 Processing: ${path.basename(filePath)}`;
|
|
4790
|
+
}
|
|
4055
4791
|
// Use AI to extract changed function names from diff
|
|
4792
|
+
globalSpinner.text = `🤖 Analyzing diff with AI: ${path.basename(filePath)}`;
|
|
4056
4793
|
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4057
4794
|
// console.log('Changed functions are', changedFunctions);
|
|
4058
4795
|
if (changedFunctions.length === 0) {
|
|
4059
|
-
|
|
4796
|
+
globalSpinner.text = `⏭️ No exported functions changed in ${path.basename(filePath)}`;
|
|
4797
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
4060
4798
|
continue;
|
|
4061
4799
|
}
|
|
4062
|
-
|
|
4800
|
+
globalSpinner.text = `Found ${changedFunctions.length} function(s): ${changedFunctions.join(', ')}`;
|
|
4801
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4063
4802
|
try {
|
|
4064
4803
|
// Use existing generateTestsForFunctions
|
|
4065
4804
|
await generateTestsForFunctions(filePath, changedFunctions);
|
|
4066
4805
|
processedFiles++;
|
|
4067
4806
|
totalFunctions += changedFunctions.length;
|
|
4068
|
-
|
|
4807
|
+
if (globalSpinner) {
|
|
4808
|
+
globalSpinner.text = `Tests generated for ${path.basename(filePath)}`;
|
|
4809
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4810
|
+
}
|
|
4069
4811
|
}
|
|
4070
4812
|
catch (error) {
|
|
4071
4813
|
errorFiles++;
|
|
@@ -4076,6 +4818,11 @@ async function autoGenerateTests() {
|
|
|
4076
4818
|
// Summary
|
|
4077
4819
|
console.log('\n' + '='.repeat(60));
|
|
4078
4820
|
console.log('📊 Auto-Generation Summary');
|
|
4821
|
+
// Clear spinner before final summary
|
|
4822
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4823
|
+
globalSpinner.stop();
|
|
4824
|
+
globalSpinner = null;
|
|
4825
|
+
}
|
|
4079
4826
|
console.log('='.repeat(60));
|
|
4080
4827
|
console.log(`✅ Successfully processed: ${processedFiles} file(s)`);
|
|
4081
4828
|
console.log(`📝 Functions tested: ${totalFunctions}`);
|
|
@@ -4098,56 +4845,81 @@ async function autoGenerateTests() {
|
|
|
4098
4845
|
* Review code changes for quality, bugs, performance, and security issues
|
|
4099
4846
|
*/
|
|
4100
4847
|
async function reviewChangedFiles() {
|
|
4101
|
-
|
|
4848
|
+
if (!globalSpinner) {
|
|
4849
|
+
globalSpinner = ora('🔍 Scanning git changes for review...').start();
|
|
4850
|
+
}
|
|
4851
|
+
else {
|
|
4852
|
+
globalSpinner.text = '🔍 Scanning git changes for review...';
|
|
4853
|
+
globalSpinner.start();
|
|
4854
|
+
}
|
|
4102
4855
|
try {
|
|
4103
4856
|
// Get all changes from git diff
|
|
4104
4857
|
const { fullDiff, files } = await getGitDiff();
|
|
4105
4858
|
if (files.length === 0) {
|
|
4859
|
+
globalSpinner.stop();
|
|
4860
|
+
globalSpinner = null;
|
|
4106
4861
|
console.log('✅ No changes detected in source files.');
|
|
4107
4862
|
console.log(' (Only staged and unstaged changes are checked)');
|
|
4108
4863
|
return;
|
|
4109
4864
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4865
|
+
globalSpinner.text = `Found changes in ${files.length} file(s) to review`;
|
|
4866
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4867
|
+
// Collect all changed files and their functions
|
|
4868
|
+
const filesToReview = [];
|
|
4114
4869
|
for (const fileInfo of files) {
|
|
4115
4870
|
const { filePath, diff } = fileInfo;
|
|
4116
4871
|
// Check if file exists
|
|
4117
4872
|
if (!fsSync.existsSync(filePath)) {
|
|
4118
|
-
|
|
4873
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4874
|
+
globalSpinner = ora(`⏭️ Skipping ${filePath} (file not found)`).start();
|
|
4875
|
+
}
|
|
4876
|
+
else {
|
|
4877
|
+
globalSpinner.text = `⏭️ Skipping ${filePath} (file not found)`;
|
|
4878
|
+
}
|
|
4879
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
4119
4880
|
continue;
|
|
4120
4881
|
}
|
|
4121
|
-
|
|
4122
|
-
|
|
4882
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4883
|
+
globalSpinner = ora(`🔄 Analyzing: ${filePath}`).start();
|
|
4884
|
+
}
|
|
4885
|
+
else {
|
|
4886
|
+
globalSpinner.text = `🔄 Analyzing: ${filePath}`;
|
|
4887
|
+
}
|
|
4123
4888
|
// Use AI to extract changed function names from diff
|
|
4124
4889
|
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4125
4890
|
if (changedFunctions.length === 0) {
|
|
4126
|
-
|
|
4891
|
+
globalSpinner.text = `⏭️ No exported functions changed in ${path.basename(filePath)}`;
|
|
4892
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
4127
4893
|
continue;
|
|
4128
4894
|
}
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4895
|
+
globalSpinner.text = `Found ${changedFunctions.length} changed function(s) in ${path.basename(filePath)}`;
|
|
4896
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4897
|
+
filesToReview.push({ filePath, diff, functions: changedFunctions });
|
|
4898
|
+
}
|
|
4899
|
+
if (filesToReview.length === 0) {
|
|
4900
|
+
console.log('\n✅ No functions to review.');
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
// Generate unified review for all changes
|
|
4904
|
+
try {
|
|
4905
|
+
await generateUnifiedCodeReview(filesToReview);
|
|
4906
|
+
// Ensure spinner is cleared before final message
|
|
4907
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4908
|
+
globalSpinner.stop();
|
|
4909
|
+
globalSpinner = null;
|
|
4140
4910
|
}
|
|
4911
|
+
console.log('\n✅ Code review completed!');
|
|
4912
|
+
}
|
|
4913
|
+
catch (error) {
|
|
4914
|
+
console.error(`\n❌ Error during review: ${error.message}`);
|
|
4141
4915
|
}
|
|
4142
4916
|
// Summary
|
|
4143
4917
|
console.log('\n' + '='.repeat(60));
|
|
4144
4918
|
console.log('📊 Code Review Summary');
|
|
4145
4919
|
console.log('='.repeat(60));
|
|
4146
|
-
console.log(`✅
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
}
|
|
4150
|
-
console.log(`📁 Reviews saved to: reviews/ directory`);
|
|
4920
|
+
console.log(`✅ Files reviewed: ${filesToReview.length}`);
|
|
4921
|
+
console.log(`📝 Total functions: ${filesToReview.reduce((sum, f) => sum + f.functions.length, 0)}`);
|
|
4922
|
+
console.log(`📁 Review saved to: reviews/code_review.md`);
|
|
4151
4923
|
console.log('='.repeat(60));
|
|
4152
4924
|
console.log('\n✨ Done!');
|
|
4153
4925
|
}
|
|
@@ -4161,305 +4933,418 @@ async function reviewChangedFiles() {
|
|
|
4161
4933
|
}
|
|
4162
4934
|
}
|
|
4163
4935
|
/**
|
|
4164
|
-
*
|
|
4936
|
+
* Build AI prompt for a specific review step based on its ruleset
|
|
4165
4937
|
*/
|
|
4166
|
-
async function
|
|
4167
|
-
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4170
|
-
const
|
|
4171
|
-
|
|
4172
|
-
|
|
4938
|
+
async function buildStepPrompt(step, filesToReview, stepOutputPath) {
|
|
4939
|
+
const filesContext = filesToReview.map(f => `- ${f.filePath}: ${f.functions.join(', ')}`).join('\n');
|
|
4940
|
+
const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
|
|
4941
|
+
// Read ruleset from markdown file
|
|
4942
|
+
const rulesetPath = path.join(process.cwd(), 'codeguard-ruleset', step.ruleset);
|
|
4943
|
+
let rulesetContent;
|
|
4944
|
+
try {
|
|
4945
|
+
rulesetContent = await fs.readFile(rulesetPath, 'utf-8');
|
|
4946
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4947
|
+
globalSpinner.text = `📖 Loaded ruleset: ${step.ruleset}`;
|
|
4948
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
catch (error) {
|
|
4952
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4953
|
+
globalSpinner.stop();
|
|
4954
|
+
globalSpinner = null;
|
|
4955
|
+
}
|
|
4956
|
+
console.warn(`⚠️ Could not read ruleset file: ${rulesetPath}`);
|
|
4957
|
+
console.warn(` Using default ruleset message. Error: ${error.message}`);
|
|
4958
|
+
rulesetContent = `Please review the code for ${step.name} following industry best practices.`;
|
|
4959
|
+
}
|
|
4960
|
+
return `You are a senior software engineer conducting a focused code review for: **${step.name}**
|
|
4173
4961
|
|
|
4174
4962
|
## CONTEXT
|
|
4175
|
-
|
|
4176
|
-
|
|
4963
|
+
Review Focus: ${step.name}
|
|
4964
|
+
Category: ${step.category}
|
|
4965
|
+
Total files changed: ${filesToReview.length}
|
|
4966
|
+
Total functions changed: ${totalFunctions}
|
|
4177
4967
|
|
|
4178
|
-
|
|
4968
|
+
Changed files and functions:
|
|
4969
|
+
${filesContext}
|
|
4970
|
+
|
|
4971
|
+
## GIT DIFFS
|
|
4972
|
+
|
|
4973
|
+
${filesToReview.map(f => `### ${f.filePath}
|
|
4179
4974
|
\`\`\`diff
|
|
4180
|
-
${diff}
|
|
4975
|
+
${f.diff}
|
|
4181
4976
|
\`\`\`
|
|
4977
|
+
`).join('\n')}
|
|
4182
4978
|
|
|
4183
4979
|
## YOUR TASK
|
|
4184
|
-
Conduct a
|
|
4980
|
+
Conduct a focused code review ONLY for ${step.name}. Use the available tools to analyze the code thoroughly.
|
|
4981
|
+
Must complete and write the review within 1 minutes. Your review should be brief and to the point.
|
|
4185
4982
|
|
|
4186
|
-
##
|
|
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**
|
|
4983
|
+
## ANALYSIS STEPS
|
|
4238
4984
|
|
|
4239
|
-
|
|
4985
|
+
**Phase 1: Code Analysis (MANDATORY)**
|
|
4986
|
+
For each changed file:
|
|
4987
|
+
1. analyze_file_ast(filePath) → Get file structure and all functions
|
|
4988
|
+
- OR use analyze_file_ast(filePath, functionName) for specific function reviews (token-efficient)
|
|
4989
|
+
2. For each changed function:
|
|
4990
|
+
- get_function_ast(filePath, functionName) → Get implementation details
|
|
4991
|
+
3. get_imports_ast(filePath) → Understand dependencies
|
|
4992
|
+
4. get_type_definitions(filePath) → Review type safety
|
|
4993
|
+
5. Use search_codebase() if needed to understand usage patterns
|
|
4994
|
+
6. Use tools to analyze the code thoroughly. -> Where the functions is used and what all dependencies are there that call this function? What can be potential issues with the code?
|
|
4240
4995
|
|
|
4241
|
-
|
|
4996
|
+
**Phase 2: Review Analysis for ${step.name}**
|
|
4997
|
+
|
|
4998
|
+
Review the code against the following ruleset:
|
|
4999
|
+
|
|
5000
|
+
${rulesetContent}
|
|
5001
|
+
|
|
5002
|
+
Also give attention to any industry specific points that are not covered by the ruleset for ${step.name}.
|
|
4242
5003
|
|
|
4243
|
-
|
|
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)
|
|
5004
|
+
**Phase 3: Write Review**
|
|
4249
5005
|
|
|
4250
|
-
|
|
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
|
|
5006
|
+
Use the write_review tool to save findings to: ${stepOutputPath}
|
|
4258
5007
|
|
|
4259
|
-
|
|
4260
|
-
4. **Recommendations**: General suggestions for improvement
|
|
5008
|
+
## OUTPUT FORMAT
|
|
4261
5009
|
|
|
4262
|
-
|
|
5010
|
+
Your review MUST be in markdown format with the following structure:
|
|
4263
5011
|
|
|
4264
5012
|
\`\`\`markdown
|
|
4265
|
-
#
|
|
4266
|
-
|
|
4267
|
-
**Date**: {current_date}
|
|
4268
|
-
**Reviewer**: AI Code Review System
|
|
4269
|
-
**Changed Functions**: {list of functions}
|
|
4270
|
-
|
|
4271
|
-
---
|
|
4272
|
-
|
|
4273
|
-
## Summary
|
|
5013
|
+
# ${step.name}
|
|
4274
5014
|
|
|
4275
|
-
[Brief overview of changes and overall code quality assessment]
|
|
4276
|
-
|
|
4277
|
-
---
|
|
4278
5015
|
|
|
4279
5016
|
## Findings
|
|
4280
5017
|
|
|
4281
5018
|
### 🔴 Critical Issues
|
|
4282
5019
|
|
|
4283
|
-
#### [
|
|
4284
|
-
**
|
|
4285
|
-
**Function**: \`functionName\`
|
|
4286
|
-
**
|
|
5020
|
+
#### [Issue Title]
|
|
5021
|
+
**File**: \`filePath\`
|
|
5022
|
+
**Function**: \`functionName\`
|
|
5023
|
+
**Severity**: Critical
|
|
4287
5024
|
|
|
4288
5025
|
**Issue**:
|
|
4289
|
-
[Description
|
|
5026
|
+
[Description]
|
|
4290
5027
|
|
|
4291
5028
|
**Current Code**:
|
|
4292
5029
|
\`\`\`typescript
|
|
4293
|
-
// problematic code
|
|
5030
|
+
// problematic code
|
|
4294
5031
|
\`\`\`
|
|
4295
5032
|
|
|
4296
|
-
**Recommended
|
|
5033
|
+
**Recommended Code**:
|
|
4297
5034
|
\`\`\`typescript
|
|
4298
|
-
// improved code
|
|
5035
|
+
// improved code
|
|
4299
5036
|
\`\`\`
|
|
4300
5037
|
|
|
4301
5038
|
**Rationale**:
|
|
4302
|
-
[Why this is important
|
|
5039
|
+
[Why this is important]
|
|
4303
5040
|
|
|
4304
5041
|
---
|
|
4305
5042
|
|
|
4306
5043
|
### 🟠 High Priority Issues
|
|
4307
|
-
|
|
4308
5044
|
[Same format as above]
|
|
4309
5045
|
|
|
4310
5046
|
---
|
|
4311
5047
|
|
|
4312
5048
|
### 🟡 Medium Priority Issues
|
|
4313
|
-
|
|
4314
5049
|
[Same format as above]
|
|
4315
5050
|
|
|
4316
5051
|
---
|
|
4317
5052
|
|
|
4318
|
-
|
|
5053
|
+
## Positive Aspects
|
|
5054
|
+
[What was done well in this area]
|
|
4319
5055
|
|
|
4320
|
-
|
|
5056
|
+
## Recommendations
|
|
5057
|
+
[Specific recommendations for ${step.name}]
|
|
5058
|
+
\`\`\`
|
|
4321
5059
|
|
|
4322
|
-
|
|
5060
|
+
## CRITICAL REMINDERS
|
|
5061
|
+
|
|
5062
|
+
- ALWAYS use tools to analyze code before reviewing
|
|
5063
|
+
- Focus ONLY on ${step.name} - do not review other aspects
|
|
5064
|
+
- Be specific and actionable
|
|
5065
|
+
- Include code examples
|
|
5066
|
+
- Use write_review tool to save to: ${stepOutputPath}
|
|
5067
|
+
- Must complete within 30 seconds
|
|
5068
|
+
- Use avaiable tools extensively to analyze the code.
|
|
5069
|
+
- The review should be based on the ruleset, it should be to the point without too much text.
|
|
5070
|
+
- Before writing the review, make sure to analyze the code thoroughly using the tools available, how similar code is used in the codebase.
|
|
5071
|
+
- Keep the review short and to the point, do not write too much text.
|
|
4323
5072
|
|
|
4324
|
-
|
|
5073
|
+
**START**: Begin by calling analyze_file_ast on each changed file.`;
|
|
5074
|
+
}
|
|
5075
|
+
/**
|
|
5076
|
+
* Execute a single review step with AI
|
|
5077
|
+
*/
|
|
5078
|
+
async function executeReviewStep(step, filesToReview, stepOutputPath) {
|
|
5079
|
+
// console.log(`\n🔍 Running ${step.name} review step...`);
|
|
5080
|
+
try {
|
|
5081
|
+
const prompt = await buildStepPrompt(step, filesToReview, stepOutputPath);
|
|
5082
|
+
const messages = [
|
|
5083
|
+
{
|
|
5084
|
+
role: 'user',
|
|
5085
|
+
content: prompt
|
|
5086
|
+
}
|
|
5087
|
+
];
|
|
5088
|
+
let iterations = 0;
|
|
5089
|
+
const maxIterations = 30;
|
|
5090
|
+
let reviewWritten = false;
|
|
5091
|
+
while (iterations < maxIterations) {
|
|
5092
|
+
iterations++;
|
|
5093
|
+
const response = await callAI(messages, TOOLS_FOR_CODE_REVIEW, CONFIG.aiProvider);
|
|
5094
|
+
if (response.content) {
|
|
5095
|
+
const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
|
|
5096
|
+
messages.push({ role: 'assistant', content });
|
|
5097
|
+
// Check if review is complete
|
|
5098
|
+
if (typeof content === 'string' && (content.toLowerCase().includes('review complete') ||
|
|
5099
|
+
content.toLowerCase().includes('review has been written'))) {
|
|
5100
|
+
if (reviewWritten) {
|
|
5101
|
+
break;
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
5106
|
+
if (reviewWritten) {
|
|
5107
|
+
break;
|
|
5108
|
+
}
|
|
5109
|
+
// Prompt AI to continue
|
|
5110
|
+
messages.push({
|
|
5111
|
+
role: 'user',
|
|
5112
|
+
content: `Please use the write_review tool NOW to save your ${step.name} review to ${stepOutputPath}. Include all findings with severity levels and code examples.`
|
|
5113
|
+
});
|
|
5114
|
+
continue;
|
|
5115
|
+
}
|
|
5116
|
+
// Execute tool calls
|
|
5117
|
+
const toolResults = [];
|
|
5118
|
+
for (const toolCall of response.toolCalls) {
|
|
5119
|
+
const toolResult = await executeTool(toolCall.name, toolCall.input);
|
|
5120
|
+
toolResults.push({ id: toolCall.id, name: toolCall.name, result: toolResult });
|
|
5121
|
+
// Track if review was written
|
|
5122
|
+
if (toolCall.name === 'write_review' && toolResult.success) {
|
|
5123
|
+
reviewWritten = true;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
// Add tool calls and results to messages (format differs by provider)
|
|
5127
|
+
if (CONFIG.aiProvider === 'openai') {
|
|
5128
|
+
messages.push({
|
|
5129
|
+
role: 'assistant',
|
|
5130
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
5131
|
+
id: tc.id,
|
|
5132
|
+
type: 'function',
|
|
5133
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
5134
|
+
}))
|
|
5135
|
+
});
|
|
5136
|
+
for (const tr of toolResults) {
|
|
5137
|
+
messages.push({
|
|
5138
|
+
role: 'tool',
|
|
5139
|
+
tool_call_id: tr.id,
|
|
5140
|
+
content: JSON.stringify(tr.result)
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
else if (CONFIG.aiProvider === 'gemini') {
|
|
5145
|
+
for (const toolCall of response.toolCalls) {
|
|
5146
|
+
messages.push({
|
|
5147
|
+
role: 'model',
|
|
5148
|
+
functionCall: { name: toolCall.name, args: toolCall.input }
|
|
5149
|
+
});
|
|
5150
|
+
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
5151
|
+
messages.push({
|
|
5152
|
+
role: 'user',
|
|
5153
|
+
functionResponse: { name: toolCall.name, response: result?.result }
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
else {
|
|
5158
|
+
// Claude
|
|
5159
|
+
messages.push({
|
|
5160
|
+
role: 'assistant',
|
|
5161
|
+
content: response.toolCalls.map(tc => ({
|
|
5162
|
+
type: 'tool_use',
|
|
5163
|
+
id: tc.id,
|
|
5164
|
+
name: tc.name,
|
|
5165
|
+
input: tc.input
|
|
5166
|
+
}))
|
|
5167
|
+
});
|
|
5168
|
+
messages.push({
|
|
5169
|
+
role: 'user',
|
|
5170
|
+
content: toolResults.map(tr => ({
|
|
5171
|
+
type: 'tool_result',
|
|
5172
|
+
tool_use_id: tr.id,
|
|
5173
|
+
content: JSON.stringify(tr.result)
|
|
5174
|
+
}))
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
if (!reviewWritten) {
|
|
5179
|
+
throw new Error(`Could not complete ${step.name} review within iteration limit`);
|
|
5180
|
+
}
|
|
5181
|
+
console.log(` ✅ ${step.name} review completed`);
|
|
5182
|
+
return { success: true, stepId: step.id, stepName: step.name };
|
|
5183
|
+
}
|
|
5184
|
+
catch (error) {
|
|
5185
|
+
console.error(` ❌ ${step.name} review failed: ${error.message}`);
|
|
5186
|
+
return { success: false, error: error.message, stepId: step.id, stepName: step.name };
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
/**
|
|
5190
|
+
* Merge review results from multiple steps into a single unified review
|
|
5191
|
+
*/
|
|
5192
|
+
async function mergeReviewResults(stepResults, filesToReview, finalOutputPath) {
|
|
5193
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
5194
|
+
const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
|
|
5195
|
+
// Start building the merged review
|
|
5196
|
+
let mergedReview = `# Code Review
|
|
4325
5197
|
|
|
4326
|
-
|
|
4327
|
-
|
|
5198
|
+
**Date**: ${timestamp}
|
|
5199
|
+
**Reviewer**: AI Code Review System
|
|
5200
|
+
**Files Changed**: ${filesToReview.length}
|
|
5201
|
+
**Functions Changed**: ${totalFunctions}
|
|
4328
5202
|
|
|
4329
5203
|
---
|
|
4330
5204
|
|
|
4331
|
-
##
|
|
5205
|
+
## Summary
|
|
4332
5206
|
|
|
4333
|
-
|
|
4334
|
-
2. [Recommendation 2]
|
|
5207
|
+
This review covers ${filesToReview.length} file(s) with ${totalFunctions} changed function(s). The review was conducted across multiple aspects: ${stepResults.map(r => r.stepName).join(', ')}.
|
|
4335
5208
|
|
|
4336
5209
|
---
|
|
4337
5210
|
|
|
4338
|
-
##
|
|
5211
|
+
## Files Changed
|
|
4339
5212
|
|
|
4340
|
-
|
|
4341
|
-
|
|
5213
|
+
${filesToReview.map(f => `- **${f.filePath}**
|
|
5214
|
+
- Functions: ${f.functions.join(', ')}`).join('\n')}
|
|
4342
5215
|
|
|
4343
|
-
|
|
5216
|
+
---
|
|
4344
5217
|
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
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;
|
|
5218
|
+
`;
|
|
5219
|
+
// Add each step's findings
|
|
5220
|
+
for (const result of stepResults) {
|
|
5221
|
+
const stepFilePath = path.join('reviews', '.tmp', `step-${result.stepId}.md`);
|
|
5222
|
+
if (result.success) {
|
|
5223
|
+
try {
|
|
5224
|
+
// Read the step review file
|
|
5225
|
+
if (fsSync.existsSync(stepFilePath)) {
|
|
5226
|
+
const stepContent = await fs.readFile(stepFilePath, 'utf-8');
|
|
5227
|
+
// Extract content after the first heading (skip the step title)
|
|
5228
|
+
const lines = stepContent.split('\n');
|
|
5229
|
+
let contentStart = 0;
|
|
5230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5231
|
+
if (lines[i].startsWith('# ')) {
|
|
5232
|
+
contentStart = i + 1;
|
|
5233
|
+
break;
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
const content = lines.slice(contentStart).join('\n').trim();
|
|
5237
|
+
// Add as a section in the merged review
|
|
5238
|
+
mergedReview += `## ${result.stepName}\n\n${content}\n\n---\n\n`;
|
|
5239
|
+
}
|
|
5240
|
+
else {
|
|
5241
|
+
mergedReview += `## ${result.stepName}\n\n⚠️ Review file not found.\n\n---\n\n`;
|
|
4381
5242
|
}
|
|
4382
5243
|
}
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
if (reviewWritten) {
|
|
4386
|
-
console.log('\n✅ Code review complete!');
|
|
4387
|
-
break;
|
|
5244
|
+
catch (error) {
|
|
5245
|
+
mergedReview += `## ${result.stepName}\n\n⚠️ Error reading review: ${error.message}\n\n---\n\n`;
|
|
4388
5246
|
}
|
|
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
5247
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
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
|
-
}
|
|
5248
|
+
else {
|
|
5249
|
+
mergedReview += `## ${result.stepName}\n\n❌ **Review step failed**: ${result.error}\n\n---\n\n`;
|
|
4405
5250
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
5251
|
+
}
|
|
5252
|
+
// Add conclusion
|
|
5253
|
+
mergedReview += `## Conclusion
|
|
5254
|
+
|
|
5255
|
+
This comprehensive review analyzed the code changes from multiple perspectives. Please address the findings based on their severity levels, starting with critical issues.
|
|
5256
|
+
|
|
5257
|
+
The review was conducted using AI-powered analysis with access to the full codebase context.
|
|
5258
|
+
`;
|
|
5259
|
+
// Ensure reviews directory exists
|
|
5260
|
+
const reviewsDir = path.dirname(finalOutputPath);
|
|
5261
|
+
if (!fsSync.existsSync(reviewsDir)) {
|
|
5262
|
+
await fs.mkdir(reviewsDir, { recursive: true });
|
|
5263
|
+
}
|
|
5264
|
+
// Write the merged review
|
|
5265
|
+
await fs.writeFile(finalOutputPath, mergedReview, 'utf-8');
|
|
5266
|
+
console.log(`\n✅ Merged review written to: ${finalOutputPath}`);
|
|
5267
|
+
// Clean up temporary files
|
|
5268
|
+
try {
|
|
5269
|
+
const tmpDir = path.join('reviews', '.tmp');
|
|
5270
|
+
if (fsSync.existsSync(tmpDir)) {
|
|
5271
|
+
const tmpFiles = await fs.readdir(tmpDir);
|
|
5272
|
+
for (const file of tmpFiles) {
|
|
5273
|
+
await fs.unlink(path.join(tmpDir, file));
|
|
4423
5274
|
}
|
|
5275
|
+
await fs.rmdir(tmpDir);
|
|
4424
5276
|
}
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
5277
|
+
}
|
|
5278
|
+
catch (error) {
|
|
5279
|
+
// Ignore cleanup errors
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Generate unified code review for all changed files
|
|
5284
|
+
*/
|
|
5285
|
+
async function generateUnifiedCodeReview(filesToReview) {
|
|
5286
|
+
const reviewFilePath = path.join('reviews', 'code_review.md');
|
|
5287
|
+
// Get enabled review steps from config
|
|
5288
|
+
const enabledSteps = CONFIG.reviewSteps.filter(step => step.enabled);
|
|
5289
|
+
if (enabledSteps.length === 0) {
|
|
5290
|
+
console.log('\n⚠️ No review steps enabled. Please enable at least one step in codeguard.json');
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
console.log(`\n📋 Running ${enabledSteps.length} review step(s): ${enabledSteps.map(s => s.name).join(', ')}`);
|
|
5294
|
+
// Ensure temporary directory exists
|
|
5295
|
+
const tmpDir = path.join('reviews', '.tmp');
|
|
5296
|
+
if (!fsSync.existsSync(tmpDir)) {
|
|
5297
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
5298
|
+
}
|
|
5299
|
+
// Execute review steps
|
|
5300
|
+
let stepResults;
|
|
5301
|
+
if (CONFIG.reviewExecutionMode === 'parallel') {
|
|
5302
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5303
|
+
globalSpinner = ora('🔄 Executing review steps in parallel...').start();
|
|
4438
5304
|
}
|
|
4439
5305
|
else {
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
5306
|
+
globalSpinner.text = '🔄 Executing review steps in parallel...';
|
|
5307
|
+
}
|
|
5308
|
+
// Run all steps in parallel
|
|
5309
|
+
const stepPromises = enabledSteps.map(step => {
|
|
5310
|
+
const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
|
|
5311
|
+
return executeReviewStep(step, filesToReview, stepOutputPath);
|
|
5312
|
+
});
|
|
5313
|
+
stepResults = await Promise.all(stepPromises);
|
|
5314
|
+
}
|
|
5315
|
+
else {
|
|
5316
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5317
|
+
globalSpinner = ora('🔄 Executing review steps sequentially...').start();
|
|
5318
|
+
}
|
|
5319
|
+
else {
|
|
5320
|
+
globalSpinner.text = '🔄 Executing review steps sequentially...';
|
|
5321
|
+
}
|
|
5322
|
+
// Run steps sequentially
|
|
5323
|
+
stepResults = [];
|
|
5324
|
+
for (const step of enabledSteps) {
|
|
5325
|
+
if (globalSpinner) {
|
|
5326
|
+
globalSpinner.text = `🔍 Running ${step.name} review step...`;
|
|
5327
|
+
}
|
|
5328
|
+
const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
|
|
5329
|
+
const result = await executeReviewStep(step, filesToReview, stepOutputPath);
|
|
5330
|
+
stepResults.push(result);
|
|
5331
|
+
if (globalSpinner && result.success) {
|
|
5332
|
+
globalSpinner.text = `${step.name} review completed`;
|
|
5333
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
5334
|
+
}
|
|
4458
5335
|
}
|
|
4459
5336
|
}
|
|
4460
|
-
|
|
4461
|
-
|
|
5337
|
+
// Merge results into final review
|
|
5338
|
+
await mergeReviewResults(stepResults, filesToReview, reviewFilePath);
|
|
5339
|
+
// Clear global spinner before final summary
|
|
5340
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
5341
|
+
globalSpinner.stop();
|
|
5342
|
+
globalSpinner = null;
|
|
4462
5343
|
}
|
|
5344
|
+
// Summary
|
|
5345
|
+
const successCount = stepResults.filter(r => r.success).length;
|
|
5346
|
+
const failCount = stepResults.filter(r => !r.success).length;
|
|
5347
|
+
console.log(`\n✅ Review complete: ${successCount} step(s) succeeded${failCount > 0 ? `, ${failCount} failed` : ''}`);
|
|
4463
5348
|
}
|
|
4464
5349
|
async function main() {
|
|
4465
5350
|
console.log('🧪 AI-Powered Unit Test Generator with AST Analysis\n');
|
|
@@ -4467,11 +5352,12 @@ async function main() {
|
|
|
4467
5352
|
const args = process.argv.slice(2);
|
|
4468
5353
|
const command = args[0]; // First argument is the command: 'auto', 'test', 'review', or undefined
|
|
4469
5354
|
// Validate command if provided
|
|
4470
|
-
if (command && !['auto', 'test', 'review'].includes(command)) {
|
|
5355
|
+
if (command && !['auto', 'test', 'review', 'doc'].includes(command)) {
|
|
4471
5356
|
console.error('❌ Invalid command. Usage:\n');
|
|
4472
5357
|
console.error(' testgen auto - Review changes and generate tests');
|
|
4473
5358
|
console.error(' testgen test - Generate tests only');
|
|
4474
5359
|
console.error(' testgen review - Review changes only');
|
|
5360
|
+
console.error(' testgen doc - Generate API documentation');
|
|
4475
5361
|
console.error(' testgen - Interactive mode\n');
|
|
4476
5362
|
process.exit(1);
|
|
4477
5363
|
}
|
|
@@ -4497,21 +5383,31 @@ async function main() {
|
|
|
4497
5383
|
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
4498
5384
|
process.exit(1);
|
|
4499
5385
|
}
|
|
4500
|
-
// If command mode (auto, test, review),
|
|
4501
|
-
if (command === 'auto' || command === 'test' || command === 'review') {
|
|
5386
|
+
// If command mode (auto, test, review, doc), prepare index (build/update) and proceed directly
|
|
5387
|
+
if (command === 'auto' || command === 'test' || command === 'review' || command === 'doc') {
|
|
4502
5388
|
const modeLabel = command === 'auto' ? 'Auto Mode (Review + Test)' :
|
|
4503
5389
|
command === 'test' ? 'Test Generation Mode' :
|
|
4504
|
-
'Code Review Mode'
|
|
5390
|
+
command === 'review' ? 'Code Review Mode' :
|
|
5391
|
+
CONFIG.repoDoc ? 'Documentation Generation Mode (Full Repository)' : 'Documentation Generation Mode (Changes Only)';
|
|
4505
5392
|
console.log(`🤖 ${modeLabel}: Detecting changes via git diff\n`);
|
|
4506
5393
|
console.log(`✅ Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
4507
|
-
// Initialize indexer
|
|
5394
|
+
// Initialize and prepare codebase indexer (build or update) without prompts
|
|
4508
5395
|
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
4509
5396
|
const hasExistingIndex = globalIndexer.hasIndex();
|
|
4510
5397
|
if (hasExistingIndex) {
|
|
4511
|
-
await globalIndexer.loadIndex();
|
|
5398
|
+
const loaded = await globalIndexer.loadIndex();
|
|
5399
|
+
if (loaded) {
|
|
5400
|
+
const staleFiles = globalIndexer.getStaleFiles();
|
|
5401
|
+
if (staleFiles.length > 0) {
|
|
5402
|
+
console.log(`🔄 Updating ${staleFiles.length} modified file(s) in index...`);
|
|
5403
|
+
await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
4512
5406
|
}
|
|
4513
5407
|
else {
|
|
4514
|
-
|
|
5408
|
+
// Build index once for the whole repo to speed up AST queries
|
|
5409
|
+
console.log('📦 Building codebase index for faster analysis...');
|
|
5410
|
+
await globalIndexer.buildIndex('.', analyzeFileAST);
|
|
4515
5411
|
}
|
|
4516
5412
|
// Execute based on command
|
|
4517
5413
|
if (command === 'auto') {
|
|
@@ -4528,6 +5424,11 @@ async function main() {
|
|
|
4528
5424
|
// Only review changes
|
|
4529
5425
|
await reviewChangedFiles();
|
|
4530
5426
|
}
|
|
5427
|
+
else if (command === 'doc') {
|
|
5428
|
+
// Only generate documentation
|
|
5429
|
+
console.log('Currently in development. Please try again later.');
|
|
5430
|
+
// await generateDocumentationMode();
|
|
5431
|
+
}
|
|
4531
5432
|
return;
|
|
4532
5433
|
}
|
|
4533
5434
|
// Optional: Codebase Indexing
|
|
@@ -4543,16 +5444,18 @@ async function main() {
|
|
|
4543
5444
|
// Check for stale files (modified since last index)
|
|
4544
5445
|
const staleFiles = globalIndexer.getStaleFiles();
|
|
4545
5446
|
if (staleFiles.length > 0) {
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
if (staleFiles.length <= 5) {
|
|
4549
|
-
staleFiles.forEach(f => console.log(` 📝 ${path.basename(f)}`));
|
|
5447
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5448
|
+
globalSpinner = ora(`🔄 Updating ${staleFiles.length} modified file(s)...`).start();
|
|
4550
5449
|
}
|
|
4551
5450
|
else {
|
|
4552
|
-
|
|
5451
|
+
globalSpinner.text = `🔄 Updating ${staleFiles.length} modified file(s)...`;
|
|
4553
5452
|
}
|
|
4554
5453
|
await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
|
|
4555
|
-
|
|
5454
|
+
globalSpinner.text = `Index updated - ${staleFiles.length} file(s) refreshed`;
|
|
5455
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
5456
|
+
globalSpinner.stop();
|
|
5457
|
+
globalSpinner = null;
|
|
5458
|
+
console.log(); // Empty line for spacing
|
|
4556
5459
|
}
|
|
4557
5460
|
else {
|
|
4558
5461
|
console.log('✅ All files up to date!\n');
|
|
@@ -4615,13 +5518,23 @@ async function main() {
|
|
|
4615
5518
|
case '1':
|
|
4616
5519
|
default:
|
|
4617
5520
|
// File-wise mode (original functionality)
|
|
4618
|
-
|
|
5521
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5522
|
+
globalSpinner = ora('📂 Scanning repository...').start();
|
|
5523
|
+
}
|
|
5524
|
+
else {
|
|
5525
|
+
globalSpinner.text = '📂 Scanning repository...';
|
|
5526
|
+
}
|
|
4619
5527
|
const files = await listFilesRecursive('.');
|
|
4620
5528
|
if (files.length === 0) {
|
|
4621
|
-
|
|
5529
|
+
globalSpinner.fail('No source files found!');
|
|
5530
|
+
globalSpinner = null;
|
|
4622
5531
|
return;
|
|
4623
5532
|
}
|
|
4624
|
-
|
|
5533
|
+
globalSpinner.text = `Found ${files.length} source file(s)`;
|
|
5534
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
5535
|
+
globalSpinner.stop();
|
|
5536
|
+
globalSpinner = null;
|
|
5537
|
+
console.log('\nSelect a file to generate tests:\n');
|
|
4625
5538
|
files.forEach((file, index) => {
|
|
4626
5539
|
console.log(`${index + 1}. ${file}`);
|
|
4627
5540
|
});
|
|
@@ -4632,6 +5545,11 @@ async function main() {
|
|
|
4632
5545
|
return;
|
|
4633
5546
|
}
|
|
4634
5547
|
await generateTests(selectedFile);
|
|
5548
|
+
// Clear spinner before final message
|
|
5549
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
5550
|
+
globalSpinner.stop();
|
|
5551
|
+
globalSpinner = null;
|
|
5552
|
+
}
|
|
4635
5553
|
console.log('\n✨ Done!');
|
|
4636
5554
|
break;
|
|
4637
5555
|
}
|