codeguard-testgen 1.0.9 โ 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 +627 -40
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +69 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1898 -862
- 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/dist/typeValidator.d.ts +25 -0
- package/dist/typeValidator.d.ts.map +1 -0
- package/dist/typeValidator.js +180 -0
- package/dist/typeValidator.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,11 +33,13 @@ exports.insertAtPosition = insertAtPosition;
|
|
|
33
33
|
exports.deleteLines = deleteLines;
|
|
34
34
|
exports.insertLines = insertLines;
|
|
35
35
|
exports.replaceLines = replaceLines;
|
|
36
|
+
exports.writeReview = writeReview;
|
|
36
37
|
const fs = require("fs/promises");
|
|
37
38
|
const fsSync = require("fs");
|
|
38
39
|
const path = require("path");
|
|
39
40
|
const child_process_1 = require("child_process");
|
|
40
41
|
const readline = require("readline");
|
|
42
|
+
const ora = require('ora');
|
|
41
43
|
// AST parsers
|
|
42
44
|
const babelParser = require("@babel/parser");
|
|
43
45
|
const traverse = require('@babel/traverse').default;
|
|
@@ -52,6 +54,9 @@ const fuzzyMatcher_1 = require("./fuzzyMatcher");
|
|
|
52
54
|
let CONFIG;
|
|
53
55
|
// Global indexer instance (optional - only initialized if user chooses to index)
|
|
54
56
|
let globalIndexer = null;
|
|
57
|
+
let globalSpinner = null; // Shared spinner for all operations
|
|
58
|
+
// Global variable to track expected test file path (to prevent AI from creating per-function files)
|
|
59
|
+
let EXPECTED_TEST_FILE_PATH = null;
|
|
55
60
|
// AI Provider configurations - models will be set from CONFIG
|
|
56
61
|
function getAIProviders() {
|
|
57
62
|
return {
|
|
@@ -121,13 +126,17 @@ const TOOLS = [
|
|
|
121
126
|
},
|
|
122
127
|
{
|
|
123
128
|
name: 'analyze_file_ast',
|
|
124
|
-
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.',
|
|
125
130
|
input_schema: {
|
|
126
131
|
type: 'object',
|
|
127
132
|
properties: {
|
|
128
133
|
file_path: {
|
|
129
134
|
type: 'string',
|
|
130
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).'
|
|
131
140
|
}
|
|
132
141
|
},
|
|
133
142
|
required: ['file_path']
|
|
@@ -181,7 +190,7 @@ const TOOLS = [
|
|
|
181
190
|
},
|
|
182
191
|
{
|
|
183
192
|
name: 'get_file_preamble',
|
|
184
|
-
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.',
|
|
185
194
|
input_schema: {
|
|
186
195
|
type: 'object',
|
|
187
196
|
properties: {
|
|
@@ -253,7 +262,7 @@ const TOOLS = [
|
|
|
253
262
|
},
|
|
254
263
|
{
|
|
255
264
|
name: 'run_tests',
|
|
256
|
-
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.',
|
|
257
266
|
input_schema: {
|
|
258
267
|
type: 'object',
|
|
259
268
|
properties: {
|
|
@@ -264,7 +273,7 @@ const TOOLS = [
|
|
|
264
273
|
function_names: {
|
|
265
274
|
type: 'array',
|
|
266
275
|
items: { type: 'string' },
|
|
267
|
-
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.'
|
|
268
277
|
}
|
|
269
278
|
},
|
|
270
279
|
required: ['test_file_path']
|
|
@@ -300,7 +309,7 @@ const TOOLS = [
|
|
|
300
309
|
},
|
|
301
310
|
{
|
|
302
311
|
name: 'calculate_relative_path',
|
|
303
|
-
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!',
|
|
304
313
|
input_schema: {
|
|
305
314
|
type: 'object',
|
|
306
315
|
properties: {
|
|
@@ -310,39 +319,12 @@ const TOOLS = [
|
|
|
310
319
|
},
|
|
311
320
|
to_file: {
|
|
312
321
|
type: 'string',
|
|
313
|
-
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")'
|
|
314
323
|
}
|
|
315
324
|
},
|
|
316
325
|
required: ['from_file', 'to_file']
|
|
317
326
|
}
|
|
318
327
|
},
|
|
319
|
-
{
|
|
320
|
-
name: 'report_legitimate_failure',
|
|
321
|
-
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.',
|
|
322
|
-
input_schema: {
|
|
323
|
-
type: 'object',
|
|
324
|
-
properties: {
|
|
325
|
-
test_file_path: {
|
|
326
|
-
type: 'string',
|
|
327
|
-
description: 'The path to the test file'
|
|
328
|
-
},
|
|
329
|
-
failing_tests: {
|
|
330
|
-
type: 'array',
|
|
331
|
-
items: { type: 'string' },
|
|
332
|
-
description: 'List of test names that are legitimately failing'
|
|
333
|
-
},
|
|
334
|
-
reason: {
|
|
335
|
-
type: 'string',
|
|
336
|
-
description: 'Explanation of why the failures are legitimate (e.g., "Function returns undefined instead of expected object", "Missing null check causes TypeError")'
|
|
337
|
-
},
|
|
338
|
-
source_code_issue: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
description: 'Description of the bug in the source code that causes the failure'
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
required: ['test_file_path', 'failing_tests', 'reason', 'source_code_issue']
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
328
|
{
|
|
347
329
|
name: 'search_replace_block',
|
|
348
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.',
|
|
@@ -397,8 +379,60 @@ const TOOLS = [
|
|
|
397
379
|
required: ['file_path', 'content']
|
|
398
380
|
}
|
|
399
381
|
},
|
|
382
|
+
{
|
|
383
|
+
name: 'write_review',
|
|
384
|
+
description: 'Write code review findings to a markdown file in the reviews/ directory. Use this to output your comprehensive code review.',
|
|
385
|
+
input_schema: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
file_path: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'The path to the review file (e.g., "reviews/index.review.md"). Should be in reviews/ directory with .review.md extension.'
|
|
391
|
+
},
|
|
392
|
+
review_content: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'The complete markdown content of the code review including summary, findings by category (Code Quality, Bugs, Performance, Security), severity levels, and recommendations.'
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
required: ['file_path', 'review_content']
|
|
398
|
+
}
|
|
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
|
+
},
|
|
400
426
|
];
|
|
401
427
|
exports.TOOLS = TOOLS;
|
|
428
|
+
// Filtered tools for test generation (excludes write_review)
|
|
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");
|
|
402
436
|
// AST Parsing utilities
|
|
403
437
|
function parseFileToAST(filePath, content) {
|
|
404
438
|
const ext = path.extname(filePath);
|
|
@@ -423,7 +457,7 @@ function parseFileToAST(filePath, content) {
|
|
|
423
457
|
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
424
458
|
}
|
|
425
459
|
}
|
|
426
|
-
function analyzeFileAST(filePath) {
|
|
460
|
+
function analyzeFileAST(filePath, functionName) {
|
|
427
461
|
try {
|
|
428
462
|
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
429
463
|
const ast = parseFileToAST(filePath, content);
|
|
@@ -436,7 +470,8 @@ function analyzeFileAST(filePath) {
|
|
|
436
470
|
exports: [],
|
|
437
471
|
imports: [],
|
|
438
472
|
types: [],
|
|
439
|
-
constants: []
|
|
473
|
+
constants: [],
|
|
474
|
+
routeRegistrations: []
|
|
440
475
|
};
|
|
441
476
|
traverse(ast, {
|
|
442
477
|
// Function declarations
|
|
@@ -547,6 +582,49 @@ function analyzeFileAST(filePath) {
|
|
|
547
582
|
});
|
|
548
583
|
}
|
|
549
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
|
+
}
|
|
550
628
|
return {
|
|
551
629
|
success: true,
|
|
552
630
|
analysis,
|
|
@@ -562,6 +640,60 @@ function analyzeFileAST(filePath) {
|
|
|
562
640
|
return { success: false, error: error.message };
|
|
563
641
|
}
|
|
564
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
|
+
}
|
|
565
697
|
function extractParamInfo(param) {
|
|
566
698
|
if (param.type === 'Identifier') {
|
|
567
699
|
return {
|
|
@@ -669,7 +801,7 @@ function getFunctionAST(filePath, functionName) {
|
|
|
669
801
|
}
|
|
670
802
|
// Fallback: If we have an indexer, search for the function in the index
|
|
671
803
|
if (globalIndexer) {
|
|
672
|
-
console.log(` ๐ Function not found in ${filePath}, searching index...`);
|
|
804
|
+
// console.log(` ๐ Function not found in ${filePath}, searching index...`);
|
|
673
805
|
// Search through all indexed files
|
|
674
806
|
const indexData = globalIndexer.index;
|
|
675
807
|
if (indexData && indexData.files) {
|
|
@@ -679,7 +811,7 @@ function getFunctionAST(filePath, functionName) {
|
|
|
679
811
|
const hasFunction = analysis.functions?.some((fn) => fn.name === functionName);
|
|
680
812
|
const hasMethod = analysis.classes?.some((cls) => cls.methods?.some((method) => method.name === functionName));
|
|
681
813
|
if (hasFunction || hasMethod) {
|
|
682
|
-
console.log(` โ
Found ${functionName} in ${indexedFilePath}`);
|
|
814
|
+
// console.log(` โ
Found ${functionName} in ${indexedFilePath}`);
|
|
683
815
|
const result = searchInFile(indexedFilePath);
|
|
684
816
|
if (result) {
|
|
685
817
|
return result;
|
|
@@ -820,7 +952,7 @@ function getImportsAST(filePath) {
|
|
|
820
952
|
catch (error) {
|
|
821
953
|
// File not found - try to find similar file in index
|
|
822
954
|
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
823
|
-
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...`);
|
|
824
956
|
const indexData = globalIndexer.index;
|
|
825
957
|
if (indexData && indexData.files) {
|
|
826
958
|
const searchName = path.basename(filePath);
|
|
@@ -930,7 +1062,7 @@ function getTypeDefinitions(filePath) {
|
|
|
930
1062
|
catch (error) {
|
|
931
1063
|
// File not found - try to find similar file in index
|
|
932
1064
|
if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
|
|
933
|
-
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...`);
|
|
934
1066
|
const indexData = globalIndexer.index;
|
|
935
1067
|
if (indexData && indexData.files) {
|
|
936
1068
|
const searchName = path.basename(filePath);
|
|
@@ -1015,13 +1147,13 @@ function getFilePreamble(filePath) {
|
|
|
1015
1147
|
});
|
|
1016
1148
|
});
|
|
1017
1149
|
}
|
|
1018
|
-
// Expression statements (could be jest.mock or setup blocks)
|
|
1150
|
+
// Expression statements (could be vi.mock/jest.mock or setup blocks)
|
|
1019
1151
|
else if (statement.type === 'ExpressionStatement' && statement.expression?.type === 'CallExpression') {
|
|
1020
1152
|
const callExpr = statement.expression;
|
|
1021
1153
|
const callee = callExpr.callee;
|
|
1022
|
-
// Check for jest.mock()
|
|
1154
|
+
// Check for vi.mock() or jest.mock()
|
|
1023
1155
|
if (callee.type === 'MemberExpression' &&
|
|
1024
|
-
callee.object?.name === 'jest' &&
|
|
1156
|
+
(callee.object?.name === 'vi' || callee.object?.name === 'jest') &&
|
|
1025
1157
|
callee.property?.name === 'mock') {
|
|
1026
1158
|
const moduleName = callExpr.arguments[0]?.value || 'unknown';
|
|
1027
1159
|
let isVirtual = false;
|
|
@@ -1075,7 +1207,7 @@ function getFilePreamble(filePath) {
|
|
|
1075
1207
|
...topLevelVariables.map(v => ({ ...v, category: 'variable' }))
|
|
1076
1208
|
].sort((a, b) => a.startLine - b.startLine);
|
|
1077
1209
|
const fullCode = allItems.map(item => `// Lines ${item.startLine}-${item.endLine} (${item.category})\n${item.code}`).join('\n\n');
|
|
1078
|
-
console.log(` โ
Found file at ${targetPath}`);
|
|
1210
|
+
// console.log(` โ
Found file at ${targetPath}`);
|
|
1079
1211
|
// console.log(` fullCode: ${fullCode}`);
|
|
1080
1212
|
// console.log(` summary: ${JSON.stringify({
|
|
1081
1213
|
// importCount: imports.length,
|
|
@@ -1097,7 +1229,6 @@ function getFilePreamble(filePath) {
|
|
|
1097
1229
|
setupBlocks,
|
|
1098
1230
|
topLevelVariables
|
|
1099
1231
|
},
|
|
1100
|
-
fullCode,
|
|
1101
1232
|
summary: {
|
|
1102
1233
|
importCount: imports.length,
|
|
1103
1234
|
mockCount: mocks.length,
|
|
@@ -1118,7 +1249,7 @@ function getFilePreamble(filePath) {
|
|
|
1118
1249
|
}
|
|
1119
1250
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1120
1251
|
if (globalIndexer) {
|
|
1121
|
-
console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1252
|
+
// console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1122
1253
|
const indexData = globalIndexer.index;
|
|
1123
1254
|
if (indexData && indexData.files) {
|
|
1124
1255
|
const searchName = path.basename(filePath);
|
|
@@ -1151,10 +1282,10 @@ function getFilePreamble(filePath) {
|
|
|
1151
1282
|
// Sort by score (highest first)
|
|
1152
1283
|
scored.sort((a, b) => b.score - a.score);
|
|
1153
1284
|
correctPath = scored[0].path;
|
|
1154
|
-
console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1285
|
+
// console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1155
1286
|
}
|
|
1156
1287
|
else {
|
|
1157
|
-
console.log(` โ
Found file at ${correctPath}`);
|
|
1288
|
+
// console.log(` โ
Found file at ${correctPath}`);
|
|
1158
1289
|
}
|
|
1159
1290
|
const retryResult = extractPreamble(correctPath);
|
|
1160
1291
|
if (retryResult) {
|
|
@@ -1236,7 +1367,7 @@ function getClassMethods(filePath, className) {
|
|
|
1236
1367
|
}
|
|
1237
1368
|
// Fallback: If we have an indexer, search for the class in the index
|
|
1238
1369
|
if (globalIndexer) {
|
|
1239
|
-
console.log(` ๐ Class not found in ${filePath}, searching index...`);
|
|
1370
|
+
// console.log(` ๐ Class not found in ${filePath}, searching index...`);
|
|
1240
1371
|
// Search through all indexed files
|
|
1241
1372
|
const indexData = globalIndexer.index;
|
|
1242
1373
|
if (indexData && indexData.files) {
|
|
@@ -1245,7 +1376,7 @@ function getClassMethods(filePath, className) {
|
|
|
1245
1376
|
// Check if this file contains the class
|
|
1246
1377
|
const hasClass = analysis.classes?.some((cls) => cls.name === className);
|
|
1247
1378
|
if (hasClass) {
|
|
1248
|
-
console.log(` โ
Found ${className} in ${indexedFilePath}`);
|
|
1379
|
+
// console.log(` โ
Found ${className} in ${indexedFilePath}`);
|
|
1249
1380
|
const result = searchInFile(indexedFilePath);
|
|
1250
1381
|
if (result) {
|
|
1251
1382
|
return result;
|
|
@@ -1266,7 +1397,7 @@ function getClassMethods(filePath, className) {
|
|
|
1266
1397
|
}
|
|
1267
1398
|
// Other tool implementations
|
|
1268
1399
|
async function readFile(filePath) {
|
|
1269
|
-
const MAX_LINES =
|
|
1400
|
+
const MAX_LINES = 1000;
|
|
1270
1401
|
// Helper to read and validate file
|
|
1271
1402
|
const tryReadFile = async (targetPath) => {
|
|
1272
1403
|
try {
|
|
@@ -1279,14 +1410,14 @@ async function readFile(filePath) {
|
|
|
1279
1410
|
if (globalIndexer) {
|
|
1280
1411
|
const cached = globalIndexer.getFileAnalysis(targetPath);
|
|
1281
1412
|
if (cached) {
|
|
1282
|
-
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`);
|
|
1283
1414
|
// For test files, also include preamble automatically
|
|
1284
1415
|
let preamble = undefined;
|
|
1285
1416
|
if (isTestFile) {
|
|
1286
1417
|
const preambleResult = getFilePreamble(targetPath);
|
|
1287
1418
|
if (preambleResult.success) {
|
|
1288
1419
|
preamble = preambleResult;
|
|
1289
|
-
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)`);
|
|
1290
1421
|
}
|
|
1291
1422
|
}
|
|
1292
1423
|
return {
|
|
@@ -1324,7 +1455,7 @@ async function readFile(filePath) {
|
|
|
1324
1455
|
}
|
|
1325
1456
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1326
1457
|
if (globalIndexer) {
|
|
1327
|
-
console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1458
|
+
// console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1328
1459
|
const indexData = globalIndexer.index;
|
|
1329
1460
|
if (indexData && indexData.files) {
|
|
1330
1461
|
const searchName = path.basename(filePath);
|
|
@@ -1357,10 +1488,10 @@ async function readFile(filePath) {
|
|
|
1357
1488
|
// Sort by score (highest first)
|
|
1358
1489
|
scored.sort((a, b) => b.score - a.score);
|
|
1359
1490
|
correctPath = scored[0].path;
|
|
1360
|
-
console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1491
|
+
// console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1361
1492
|
}
|
|
1362
1493
|
else {
|
|
1363
|
-
console.log(` โ
Found file at ${correctPath}`);
|
|
1494
|
+
// console.log(` โ
Found file at ${correctPath}`);
|
|
1364
1495
|
}
|
|
1365
1496
|
// Try to read from correct path
|
|
1366
1497
|
const retryResult = await tryReadFile(correctPath);
|
|
@@ -1455,7 +1586,7 @@ async function readFileLines(filePath, startLine, endLine) {
|
|
|
1455
1586
|
}
|
|
1456
1587
|
// Fallback: If we have an indexer, search for file with matching path
|
|
1457
1588
|
if (globalIndexer) {
|
|
1458
|
-
console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1589
|
+
// console.log(` ๐ File not found at ${filePath}, searching index...`);
|
|
1459
1590
|
const indexData = globalIndexer.index;
|
|
1460
1591
|
if (indexData && indexData.files) {
|
|
1461
1592
|
const searchName = path.basename(filePath);
|
|
@@ -1488,10 +1619,10 @@ async function readFileLines(filePath, startLine, endLine) {
|
|
|
1488
1619
|
// Sort by score (highest first)
|
|
1489
1620
|
scored.sort((a, b) => b.score - a.score);
|
|
1490
1621
|
correctPath = scored[0].path;
|
|
1491
|
-
console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1622
|
+
// console.log(` ๐ก Multiple matches found, choosing best match: ${correctPath}`);
|
|
1492
1623
|
}
|
|
1493
1624
|
else {
|
|
1494
|
-
console.log(` โ
Found file at ${correctPath}`);
|
|
1625
|
+
// console.log(` โ
Found file at ${correctPath}`);
|
|
1495
1626
|
}
|
|
1496
1627
|
// Try to read from correct path
|
|
1497
1628
|
const retryResult = await tryReadFileLines(correctPath);
|
|
@@ -1889,7 +2020,55 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
|
|
|
1889
2020
|
return { success: false, error: error.message };
|
|
1890
2021
|
}
|
|
1891
2022
|
}
|
|
1892
|
-
|
|
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) {
|
|
1893
2072
|
try {
|
|
1894
2073
|
// Build Jest command with optional function name filter
|
|
1895
2074
|
let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
|
|
@@ -1903,7 +2082,8 @@ function runTests(testFilePath, functionNames) {
|
|
|
1903
2082
|
}
|
|
1904
2083
|
const output = (0, child_process_1.execSync)(command, {
|
|
1905
2084
|
encoding: 'utf-8',
|
|
1906
|
-
stdio: 'pipe'
|
|
2085
|
+
stdio: 'pipe',
|
|
2086
|
+
timeout: 10000
|
|
1907
2087
|
});
|
|
1908
2088
|
console.log(` Test run output: ${output}`);
|
|
1909
2089
|
return {
|
|
@@ -1914,8 +2094,8 @@ function runTests(testFilePath, functionNames) {
|
|
|
1914
2094
|
};
|
|
1915
2095
|
}
|
|
1916
2096
|
catch (error) {
|
|
1917
|
-
console.log(` Test run error: ${error.message}`);
|
|
1918
|
-
console.log(`output sent to ai: ${error.stdout + error.stderr}`);
|
|
2097
|
+
// console.log(` Test run error: ${error.message}`);
|
|
2098
|
+
// console.log(`output sent to ai: ${error.stdout + error.stderr}`);
|
|
1919
2099
|
return {
|
|
1920
2100
|
success: false,
|
|
1921
2101
|
output: error.stdout + error.stderr,
|
|
@@ -1924,6 +2104,121 @@ function runTests(testFilePath, functionNames) {
|
|
|
1924
2104
|
};
|
|
1925
2105
|
}
|
|
1926
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Run specific tests in isolation to detect if failure is due to pollution or regression
|
|
2109
|
+
* Used by smartValidateTestSuite to differentiate between test infrastructure issues and source code bugs
|
|
2110
|
+
*/
|
|
2111
|
+
function runTestsIsolated(testFilePath, specificTestNames) {
|
|
2112
|
+
try {
|
|
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`;
|
|
2115
|
+
if (specificTestNames && specificTestNames.length > 0) {
|
|
2116
|
+
const escapedNames = specificTestNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
2117
|
+
const pattern = escapedNames.join('|');
|
|
2118
|
+
command += ` -t "${pattern}"`;
|
|
2119
|
+
}
|
|
2120
|
+
const output = (0, child_process_1.execSync)(command, {
|
|
2121
|
+
encoding: 'utf-8',
|
|
2122
|
+
stdio: 'pipe',
|
|
2123
|
+
timeout: 60000, // 60 seconds
|
|
2124
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
2125
|
+
});
|
|
2126
|
+
return {
|
|
2127
|
+
success: true,
|
|
2128
|
+
output: stripAnsi(output),
|
|
2129
|
+
passed: true,
|
|
2130
|
+
command
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
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
|
+
}
|
|
2144
|
+
return {
|
|
2145
|
+
success: false,
|
|
2146
|
+
output: stripAnsi(output) || 'Test failed with no output captured',
|
|
2147
|
+
passed: false,
|
|
2148
|
+
error: error.message
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Parse Vitest output to extract names of failing tests
|
|
2154
|
+
* Returns array of test names that failed
|
|
2155
|
+
*/
|
|
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) {
|
|
2190
|
+
const failingTests = [];
|
|
2191
|
+
// Jest output patterns for failing tests:
|
|
2192
|
+
// โ describe block โบ test name
|
|
2193
|
+
// or: FAIL path/to/test.ts
|
|
2194
|
+
// โ test name (XXms)
|
|
2195
|
+
const lines = jestOutput.split('\n');
|
|
2196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2197
|
+
const line = lines[i];
|
|
2198
|
+
// Pattern 1: โ describe block โบ test name
|
|
2199
|
+
const bulletMatch = line.match(/^\s*โ\s+(.+?)\s+โบ\s+(.+?)$/);
|
|
2200
|
+
if (bulletMatch) {
|
|
2201
|
+
const testName = bulletMatch[2].trim();
|
|
2202
|
+
failingTests.push(testName);
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
// Pattern 2: โ test name
|
|
2206
|
+
const xMatch = line.match(/^\s*โ\s+(.+?)(?:\s+\(\d+m?s\))?$/);
|
|
2207
|
+
if (xMatch) {
|
|
2208
|
+
const testName = xMatch[1].trim();
|
|
2209
|
+
failingTests.push(testName);
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
// Pattern 3: FAIL in summary
|
|
2213
|
+
const failMatch = line.match(/^\s*โ?\s*(.+?)\s+\(\d+m?s\)$/);
|
|
2214
|
+
if (failMatch && line.includes('โ')) {
|
|
2215
|
+
const testName = failMatch[1].trim();
|
|
2216
|
+
failingTests.push(testName);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
// Remove duplicates
|
|
2220
|
+
return [...new Set(failingTests)];
|
|
2221
|
+
}
|
|
1927
2222
|
function listDirectory(directoryPath) {
|
|
1928
2223
|
try {
|
|
1929
2224
|
if (!fsSync.existsSync(directoryPath)) {
|
|
@@ -2004,25 +2299,138 @@ function findFile(filename) {
|
|
|
2004
2299
|
return { success: false, error: error.message };
|
|
2005
2300
|
}
|
|
2006
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
|
+
*/
|
|
2007
2310
|
function calculateRelativePath(fromFile, toFile) {
|
|
2008
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
|
|
2009
2411
|
const fromDir = path.dirname(fromFile);
|
|
2412
|
+
// Calculate relative path
|
|
2010
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
|
+
}
|
|
2011
2419
|
// Remove .ts, .tsx, .js, .jsx extensions for imports
|
|
2012
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$/, '');
|
|
2013
2423
|
// Ensure it starts with ./ or ../
|
|
2014
|
-
if (!relativePath.startsWith('.')) {
|
|
2424
|
+
if (!relativePath.startsWith('.') && !relativePath.startsWith('..')) {
|
|
2015
2425
|
relativePath = './' + relativePath;
|
|
2016
2426
|
}
|
|
2017
2427
|
// Convert backslashes to forward slashes (Windows compatibility)
|
|
2018
2428
|
relativePath = relativePath.replace(/\\/g, '/');
|
|
2019
|
-
//
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
// importStatement: `import { ... } from '${relativePath}';`
|
|
2025
|
-
// })})`)
|
|
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
|
+
}
|
|
2026
2434
|
return {
|
|
2027
2435
|
success: true,
|
|
2028
2436
|
from: fromFile,
|
|
@@ -2032,26 +2440,26 @@ function calculateRelativePath(fromFile, toFile) {
|
|
|
2032
2440
|
};
|
|
2033
2441
|
}
|
|
2034
2442
|
catch (error) {
|
|
2035
|
-
return { success: false, error: error.message };
|
|
2443
|
+
return { success: false, file: toFile, error: error.message };
|
|
2036
2444
|
}
|
|
2037
2445
|
}
|
|
2038
|
-
function reportLegitimateFailure(testFilePath, failingTests, reason, sourceCodeIssue) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}
|
|
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
|
+
// }
|
|
2055
2463
|
async function deleteLines(filePath, startLine, endLine) {
|
|
2056
2464
|
try {
|
|
2057
2465
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
@@ -2372,6 +2780,31 @@ async function insertAtPosition(filePath, content, position, afterMarker) {
|
|
|
2372
2780
|
return { success: false, error: error.message };
|
|
2373
2781
|
}
|
|
2374
2782
|
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Write code review findings to a markdown file in the reviews/ directory
|
|
2785
|
+
*/
|
|
2786
|
+
async function writeReview(filePath, reviewContent) {
|
|
2787
|
+
try {
|
|
2788
|
+
// Ensure the reviews directory exists
|
|
2789
|
+
const reviewsDir = path.dirname(filePath);
|
|
2790
|
+
if (!fsSync.existsSync(reviewsDir)) {
|
|
2791
|
+
await fs.mkdir(reviewsDir, { recursive: true });
|
|
2792
|
+
}
|
|
2793
|
+
// Write the review file
|
|
2794
|
+
await fs.writeFile(filePath, reviewContent, 'utf-8');
|
|
2795
|
+
return {
|
|
2796
|
+
success: true,
|
|
2797
|
+
message: `Review written to ${filePath}`,
|
|
2798
|
+
filePath: filePath
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
catch (error) {
|
|
2802
|
+
return {
|
|
2803
|
+
success: false,
|
|
2804
|
+
error: `Failed to write review: ${error.message}`
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2375
2808
|
// User-friendly messages for each tool
|
|
2376
2809
|
const TOOL_MESSAGES = {
|
|
2377
2810
|
'read_file': '๐ Reading source file',
|
|
@@ -2388,9 +2821,10 @@ const TOOL_MESSAGES = {
|
|
|
2388
2821
|
'list_directory': '๐ Exploring directory structure',
|
|
2389
2822
|
'find_file': '๐ Locating file in repository',
|
|
2390
2823
|
'calculate_relative_path': '๐งญ Calculating import path',
|
|
2391
|
-
'report_legitimate_failure': 'โ ๏ธ Reporting legitimate test failures',
|
|
2392
2824
|
'search_replace_block': '๐๐ Searching and replacing code block',
|
|
2393
2825
|
'insert_at_position': 'โ Inserting content at position',
|
|
2826
|
+
'write_review': '๐ Writing code review',
|
|
2827
|
+
'search_codebase': '๐ Searching codebase',
|
|
2394
2828
|
};
|
|
2395
2829
|
// Tool execution router
|
|
2396
2830
|
async function executeTool(toolName, args) {
|
|
@@ -2410,7 +2844,16 @@ async function executeTool(toolName, args) {
|
|
|
2410
2844
|
else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
|
|
2411
2845
|
friendlyMessage = `๐ Reading lines ${args.start_line}-${args.end_line}`;
|
|
2412
2846
|
}
|
|
2413
|
-
|
|
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
|
+
}
|
|
2414
2857
|
let result;
|
|
2415
2858
|
try {
|
|
2416
2859
|
switch (toolName) {
|
|
@@ -2421,21 +2864,21 @@ async function executeTool(toolName, args) {
|
|
|
2421
2864
|
result = await readFileLines(args.file_path, args.start_line, args.end_line);
|
|
2422
2865
|
break;
|
|
2423
2866
|
case 'analyze_file_ast':
|
|
2424
|
-
// Try cache first if indexer is available
|
|
2425
|
-
if (globalIndexer) {
|
|
2867
|
+
// Try cache first if indexer is available (only for non-filtered requests)
|
|
2868
|
+
if (globalIndexer && !args.function_name) {
|
|
2426
2869
|
// Check if file has been modified since caching
|
|
2427
2870
|
if (globalIndexer.isFileStale(args.file_path)) {
|
|
2428
|
-
|
|
2429
|
-
result = analyzeFileAST(args.file_path);
|
|
2430
|
-
// Update cache with new analysis
|
|
2431
|
-
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) {
|
|
2432
2875
|
await globalIndexer.updateIndex([args.file_path], analyzeFileAST);
|
|
2433
2876
|
}
|
|
2434
2877
|
break;
|
|
2435
2878
|
}
|
|
2436
2879
|
const cached = globalIndexer.getFileAnalysis(args.file_path);
|
|
2437
2880
|
if (cached) {
|
|
2438
|
-
|
|
2881
|
+
globalSpinner.text = '๐ฆ Using cached analysis';
|
|
2439
2882
|
result = {
|
|
2440
2883
|
success: true,
|
|
2441
2884
|
analysis: cached,
|
|
@@ -2443,14 +2886,14 @@ async function executeTool(toolName, args) {
|
|
|
2443
2886
|
functionCount: cached.functions.length,
|
|
2444
2887
|
classCount: cached.classes.length,
|
|
2445
2888
|
exportCount: cached.exports.length,
|
|
2446
|
-
typeCount: cached.types.length
|
|
2889
|
+
typeCount: cached.types.length,
|
|
2447
2890
|
}
|
|
2448
2891
|
};
|
|
2449
2892
|
break;
|
|
2450
2893
|
}
|
|
2451
2894
|
}
|
|
2452
|
-
// Fall back to actual analysis
|
|
2453
|
-
result = analyzeFileAST(args.file_path);
|
|
2895
|
+
// Fall back to actual analysis (with optional filtering)
|
|
2896
|
+
result = analyzeFileAST(args.file_path, args.function_name);
|
|
2454
2897
|
break;
|
|
2455
2898
|
case 'get_function_ast':
|
|
2456
2899
|
result = getFunctionAST(args.file_path, args.function_name);
|
|
@@ -2471,10 +2914,29 @@ async function executeTool(toolName, args) {
|
|
|
2471
2914
|
result = resolveImportPath(args.from_file, args.import_path);
|
|
2472
2915
|
break;
|
|
2473
2916
|
case 'upsert_function_tests':
|
|
2917
|
+
// CRITICAL VALIDATION: Prevent AI from creating per-function test files
|
|
2918
|
+
if (EXPECTED_TEST_FILE_PATH && args.test_file_path !== EXPECTED_TEST_FILE_PATH) {
|
|
2919
|
+
// Normalize paths for comparison (handle different path separators)
|
|
2920
|
+
const normalizedExpected = path.normalize(EXPECTED_TEST_FILE_PATH).replace(/\\/g, '/');
|
|
2921
|
+
const normalizedProvided = path.normalize(args.test_file_path).replace(/\\/g, '/');
|
|
2922
|
+
if (normalizedExpected !== normalizedProvided) {
|
|
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();
|
|
2927
|
+
// Override the test file path to the expected one
|
|
2928
|
+
args.test_file_path = EXPECTED_TEST_FILE_PATH;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2474
2931
|
result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
|
|
2475
2932
|
break;
|
|
2476
2933
|
case 'run_tests':
|
|
2477
|
-
|
|
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
|
+
}
|
|
2478
2940
|
break;
|
|
2479
2941
|
case 'list_directory':
|
|
2480
2942
|
result = listDirectory(args.directory_path);
|
|
@@ -2485,15 +2947,18 @@ async function executeTool(toolName, args) {
|
|
|
2485
2947
|
case 'calculate_relative_path':
|
|
2486
2948
|
result = calculateRelativePath(args.from_file, args.to_file);
|
|
2487
2949
|
break;
|
|
2488
|
-
case 'report_legitimate_failure':
|
|
2489
|
-
result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
|
|
2490
|
-
break;
|
|
2491
2950
|
case 'search_replace_block':
|
|
2492
2951
|
result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
|
|
2493
2952
|
break;
|
|
2494
2953
|
case 'insert_at_position':
|
|
2495
2954
|
result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
|
|
2496
2955
|
break;
|
|
2956
|
+
case 'write_review':
|
|
2957
|
+
result = await writeReview(args.file_path, args.review_content);
|
|
2958
|
+
break;
|
|
2959
|
+
case 'search_codebase':
|
|
2960
|
+
result = searchCodebase(args.pattern, args.file_extension, args.max_results, args.files_only);
|
|
2961
|
+
break;
|
|
2497
2962
|
default:
|
|
2498
2963
|
result = { success: false, error: `Unknown tool: ${toolName}` };
|
|
2499
2964
|
}
|
|
@@ -2501,16 +2966,139 @@ async function executeTool(toolName, args) {
|
|
|
2501
2966
|
catch (error) {
|
|
2502
2967
|
result = { success: false, error: error.message, stack: error.stack };
|
|
2503
2968
|
}
|
|
2504
|
-
//
|
|
2969
|
+
// Just keep spinner running - next tool will update the text
|
|
2970
|
+
// No checkmarks for intermediate steps - smoother like Claude CLI
|
|
2505
2971
|
if (result.success) {
|
|
2506
|
-
//
|
|
2507
|
-
|
|
2972
|
+
// Small delay so users see the operation briefly
|
|
2973
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
2508
2974
|
}
|
|
2509
2975
|
else if (result.error) {
|
|
2510
|
-
|
|
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));
|
|
2511
2983
|
}
|
|
2512
2984
|
return result;
|
|
2513
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
|
+
}
|
|
2514
3102
|
// File system utilities
|
|
2515
3103
|
async function listFilesRecursive(dir, fileList = []) {
|
|
2516
3104
|
const files = await fs.readdir(dir);
|
|
@@ -2573,7 +3161,7 @@ function getTestFilePath(sourceFile) {
|
|
|
2573
3161
|
// No subdirectory: testDir/filename.test.ts
|
|
2574
3162
|
testPath = path.join(CONFIG.testDir, testFileName);
|
|
2575
3163
|
}
|
|
2576
|
-
console.log(` ๐ Test file path: ${testPath}`);
|
|
3164
|
+
// console.log(` ๐ Test file path: ${testPath}`);
|
|
2577
3165
|
return testPath;
|
|
2578
3166
|
}
|
|
2579
3167
|
// AI Provider implementations
|
|
@@ -2749,603 +3337,34 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
|
|
|
2749
3337
|
}
|
|
2750
3338
|
// Main conversation loop
|
|
2751
3339
|
async function generateTests(sourceFile) {
|
|
2752
|
-
console.log(`\n๐ Generating tests for: ${sourceFile}\n`);
|
|
2753
|
-
//
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
console.log(
|
|
2758
|
-
|
|
2759
|
-
console.log(`\nโก File has more than 200 lines! Switching to function-by-function generation...\n`);
|
|
2760
|
-
// Analyze file to get all functions
|
|
2761
|
-
const result = analyzeFileAST(sourceFile);
|
|
2762
|
-
if (result && result.success && result.analysis && result.analysis.functions && result.analysis.functions.length > 0) {
|
|
2763
|
-
// Filter to only EXPORTED functions (public API)
|
|
2764
|
-
const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
|
|
2765
|
-
const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
|
|
2766
|
-
if (functionNames.length === 0) {
|
|
2767
|
-
console.log('โ ๏ธ No exported functions found in file. Falling back to regular generation.');
|
|
2768
|
-
// Fall through to regular generation
|
|
2769
|
-
}
|
|
2770
|
-
else {
|
|
2771
|
-
const totalFunctions = result.analysis.functions.length;
|
|
2772
|
-
const internalFunctions = totalFunctions - exportedFunctions.length;
|
|
2773
|
-
console.log(`โ
Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
|
|
2774
|
-
if (internalFunctions > 0) {
|
|
2775
|
-
console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
|
|
2776
|
-
}
|
|
2777
|
-
// Use function-by-function generation
|
|
2778
|
-
try {
|
|
2779
|
-
return await generateTestsForFunctions(sourceFile, functionNames);
|
|
2780
|
-
}
|
|
2781
|
-
catch (funcError) {
|
|
2782
|
-
// CRITICAL: Check if test file already exists with content
|
|
2783
|
-
const testFilePath = getTestFilePath(sourceFile);
|
|
2784
|
-
if (fsSync.existsSync(testFilePath)) {
|
|
2785
|
-
const existingContent = fsSync.readFileSync(testFilePath, 'utf-8');
|
|
2786
|
-
const hasExistingTests = existingContent.includes('describe(') || existingContent.includes('test(');
|
|
2787
|
-
if (hasExistingTests) {
|
|
2788
|
-
console.error('\nโ CRITICAL: Function-by-function generation failed, but test file already has tests!');
|
|
2789
|
-
console.error(' Cannot fall back to file-wise generation as it would OVERWRITE existing tests.');
|
|
2790
|
-
console.error(` Error: ${funcError.message}`);
|
|
2791
|
-
console.error('\n Options:');
|
|
2792
|
-
console.error(' 1. Fix the issue manually in the test file');
|
|
2793
|
-
console.error(' 2. Delete the test file and regenerate from scratch');
|
|
2794
|
-
console.error(' 3. Run function-wise generation again for remaining functions');
|
|
2795
|
-
throw new Error(`Function-by-function generation failed with existing tests. Manual intervention required. Original error: ${funcError.message}`);
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
// No existing tests, safe to fall back
|
|
2799
|
-
console.log('โ ๏ธ Function-by-function generation failed, but no existing tests found. Falling back to file-wise generation.');
|
|
2800
|
-
throw funcError; // Re-throw to be caught by outer try-catch
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
else {
|
|
2805
|
-
console.log('โ ๏ธ No functions found in file. Falling back to regular generation.');
|
|
2806
|
-
if (result && !result.success) {
|
|
2807
|
-
console.log(` Analysis error: ${result.error}`);
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
3340
|
+
// console.log(`\n๐ Generating tests for: ${sourceFile}\n`);
|
|
3341
|
+
// Analyze file to get all functions (with retry)
|
|
3342
|
+
let result = analyzeFileAST(sourceFile);
|
|
3343
|
+
// Retry once if failed
|
|
3344
|
+
if (!result.success) {
|
|
3345
|
+
// console.log('โ ๏ธ AST analysis failed, retrying once...');
|
|
3346
|
+
result = analyzeFileAST(sourceFile);
|
|
2811
3347
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
// This is a critical error - don't proceed with generation
|
|
2816
|
-
console.error(`\nโ Aborting: ${error.message}`);
|
|
2817
|
-
throw error; // Re-throw to stop execution
|
|
2818
|
-
}
|
|
2819
|
-
console.log(`โ ๏ธ Could not check file size: ${error}. Proceeding with regular generation.`);
|
|
2820
|
-
// Falls through to regular file-wise generation below
|
|
3348
|
+
// If still failed, throw error
|
|
3349
|
+
if (!result.success || !result.analysis || !result.analysis.functions) {
|
|
3350
|
+
throw new Error(`File analysis failed. Unable to extract functions from file. Error: ${result.error || 'unknown'}`);
|
|
2821
3351
|
}
|
|
2822
|
-
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
Source file: ${sourceFile}
|
|
2829
|
-
Test file path: ${testFilePath}
|
|
2830
|
-
|
|
2831
|
-
IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
|
|
2832
|
-
|
|
2833
|
-
Your task (you MUST complete ALL steps):
|
|
2834
|
-
1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
|
|
2835
|
-
- This provides metadata about all code structures without loading full file content
|
|
2836
|
-
- CRITICAL: You have only 50 iterations to complete this task, so make sure you are using the tools efficiently.
|
|
2837
|
-
- Do not over explore, use the tools to get the information you need and start generating tests.
|
|
2838
|
-
2. Use get_imports_ast tool to understand all dependencies
|
|
2839
|
-
3. For each dependency, use find_file(filePath) to locate the file and calculate_relative_path to get correct import paths for the test file
|
|
2840
|
-
4. For complex functions, use get_function_ast tool to get detailed information
|
|
2841
|
-
- Returns complete function code WITH JSDoc comments
|
|
2842
|
-
- Includes calledFunctions and calledMethods lists showing what the function calls
|
|
2843
|
-
- Use this to fetch related helper functions if needed
|
|
2844
|
-
- [CRITICAL]: If a function calls other functions from other files, use find_file + get_function_ast tools to locate them and check if they need to mocked, since they can be making api calls to external services.
|
|
2845
|
-
5. Use get_function_ast to get detailed information about the functions.
|
|
2846
|
-
6. For large test files (>5K lines), use get_file_preamble to see existing imports/mocks/setup blocks
|
|
2847
|
-
- Automatically included when reading large test files
|
|
2848
|
-
- Use before adding new test cases to avoid duplicate mocks/imports
|
|
2849
|
-
- Particularly useful when updating existing test files with upsert_function_tests
|
|
2850
|
-
- Captures complete multi-line mocks including complex jest.mock() statements
|
|
2851
|
-
7. For classes, use get_class_methods tool to extract all methods
|
|
2852
|
-
8. Use get_type_definitions tool to understand TypeScript types and interfaces
|
|
2853
|
-
9. Generate comprehensive Jest unit tests with:
|
|
2854
|
-
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors. Ensure tests fully mock the config module with all expected properties.
|
|
2855
|
-
- If required:
|
|
2856
|
-
- Mock database modules like '../database' or '../database/index' with virtual:true.
|
|
2857
|
-
- Mock models, and any modules that access config or the database with virtual:true.
|
|
2858
|
-
- Mock config properly with virtual:true.
|
|
2859
|
-
- Mock isEmpty from lodash to return the expected values with virtual:true.
|
|
2860
|
-
- Axios should be mocked with virtual:true.
|
|
2861
|
-
- Use jest.mock() calls at the TOP of the file before any imports
|
|
2862
|
-
- [CRITICAL]: Virtual modules should only be used for db/config/models/services/index/axios/routes files. You should not use virtual:true for any other files or helpers that exist in the source code. The actual helpers should never be mocked with virtual:true.
|
|
2863
|
-
9. REQUIRED: Write tests using upsert_function_tests tool for EACH function with REAL test code (NOT placeholders!)
|
|
2864
|
-
- Call upsert_function_tests once for EACH exported function
|
|
2865
|
-
- Ensure comprehensive mocks are included in the first function's test to set up the file
|
|
2866
|
-
- DO NOT use ANY placeholder comments like:
|
|
2867
|
-
* "// Mock setup", "// Assertions", "// Call function"
|
|
2868
|
-
* "// Further tests...", "// Additional tests..."
|
|
2869
|
-
* "// Similarly, write tests for..."
|
|
2870
|
-
* "// Add more tests...", "// TODO", "// ..."
|
|
2871
|
-
- Write ACTUAL working test code with real mocks, real assertions, real function calls
|
|
2872
|
-
- Every test MUST have [MANDATORY]:
|
|
2873
|
-
* Real setup code (mock functions, create test data)
|
|
2874
|
-
* Real execution (call the function being tested)
|
|
2875
|
-
* Real expect() assertions (at least one per test)
|
|
2876
|
-
* null/undefined handling tests for all API responses
|
|
2877
|
-
* Happy path scenarios
|
|
2878
|
-
* Edge cases (null, undefined, empty arrays, etc.)
|
|
2879
|
-
* Error conditions
|
|
2880
|
-
* Async behavior (if applicable)
|
|
2881
|
-
- Proper TypeScript types
|
|
2882
|
-
- Write tests for EVERY exported function (minimum 3-5 tests per function)
|
|
2883
|
-
- If source has 4 functions, test file MUST have 4 describe blocks with actual tests
|
|
2884
|
-
- Example of COMPLETE test structure:
|
|
2885
|
-
* Setup: Create mocks and test data
|
|
2886
|
-
* Execute: Call the function being tested
|
|
2887
|
-
* Assert: Use expect() to verify results
|
|
2888
|
-
10. REQUIRED: Run the tests using run_tests tool
|
|
2889
|
-
11. REQUIRED: If tests fail with import errors:
|
|
2890
|
-
- Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
|
|
2891
|
-
- Use calculate_relative_path tool to get correct import path
|
|
2892
|
-
- โ
PRIMARY METHOD: Use search_replace_block to fix imports:
|
|
2893
|
-
* Include 3-5 lines of context around the import to change
|
|
2894
|
-
* Example: search_replace_block({
|
|
2895
|
-
search: "import { oldImport } from './old-path';\nimport { other } from './other';",
|
|
2896
|
-
replace: "import { oldImport, newImport } from './correct-path';\nimport { other } from './other';"
|
|
2897
|
-
})
|
|
2898
|
-
- ๐ ALTERNATIVE: Use insert_at_position for adding new imports:
|
|
2899
|
-
* insert_at_position({ position: 'after_imports', content: "import { newImport } from './path';" })
|
|
2900
|
-
- โ ๏ธ AVOID: Line-based tools (deprecated, fragile)
|
|
2901
|
-
12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
|
|
2902
|
-
|
|
2903
|
-
FIXABLE ERRORS (you should fix these):
|
|
2904
|
-
- Wrong import paths
|
|
2905
|
-
- Missing mocks
|
|
2906
|
-
- Incorrect mock implementations
|
|
2907
|
-
- Wrong assertions or test logic
|
|
2908
|
-
- TypeScript compilation errors (syntax errors, bracket mismatches)
|
|
2909
|
-
- Missing test setup/teardown
|
|
2910
|
-
- Cannot read properties of undefined
|
|
2911
|
-
- Test case failed to run. Use read_file_lines tool to read the specific problematic section and fix the issue.
|
|
2912
|
-
|
|
2913
|
-
๐ก TIP: For syntax errors or bracket mismatches:
|
|
2914
|
-
- Use read_file to see the file content (it includes line numbers)
|
|
2915
|
-
- Use search_replace_block to fix the problematic section
|
|
2916
|
-
- Include 3-5 lines of context around the error to make search unique
|
|
2917
|
-
- Example: search_replace_block({
|
|
2918
|
-
search: "line before error\nproblematic code with syntax error\nline after",
|
|
2919
|
-
replace: "line before error\ncorrected code\nline after"
|
|
2920
|
-
})
|
|
2921
|
-
|
|
2922
|
-
LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
|
|
2923
|
-
- Function returns wrong type (e.g., undefined instead of object)
|
|
2924
|
-
- Missing null/undefined checks in source code
|
|
2925
|
-
- Logic errors in source code
|
|
2926
|
-
- Unhandled promise rejections in source code
|
|
2927
|
-
|
|
2928
|
-
13. If errors are FIXABLE (AFTER test file is written):
|
|
2929
|
-
- โ
PRIMARY METHOD: Use search_replace_block (RECOMMENDED):
|
|
2930
|
-
* Find the problematic code section
|
|
2931
|
-
* Include 3-5 lines of context before/after to make search unique
|
|
2932
|
-
* Replace with corrected version
|
|
2933
|
-
* Example: search_replace_block({
|
|
2934
|
-
file_path: "test.ts",
|
|
2935
|
-
search: "const mock = jest.fn();\ntest('old test', () => {\n mock();",
|
|
2936
|
-
replace: "const mock = jest.fn().mockResolvedValue({ data: 'test' });\ntest('fixed test', () => {\n mock();"
|
|
2937
|
-
})
|
|
2938
|
-
* Handles whitespace/indentation differences automatically!
|
|
2939
|
-
- ๐ ALTERNATIVE: Use insert_at_position for adding mocks/imports at top:
|
|
2940
|
-
* insert_at_position({ position: 'after_imports', content: "jest.mock('../database');" })
|
|
2941
|
-
- โ ๏ธ AVOID: Line-based tools (deprecated) - they are fragile and prone to errors
|
|
2942
|
-
- Then retry running tests
|
|
2943
|
-
14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
|
|
2944
|
-
- Provide failing test names, reason, and source code issue description
|
|
2945
|
-
- The test file will be kept as-is with legitimate failing tests
|
|
2946
|
-
- You are not allowed to call this tool for error - Test suite failed to run. You must ensure that test cases get executed. Fix any syntax or linting issues in the test file.
|
|
2947
|
-
15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
|
|
2948
|
-
16. REQUIRED: Ensure all functions are tested in the test file.
|
|
2949
|
-
17. CRITICAL: config and database modules must be mocked
|
|
2950
|
-
|
|
2951
|
-
18. Some known issues when running tests with fixes:
|
|
2952
|
-
Route loading issue: Importing the controller triggered route setup because axios-helper imports from index.ts, which loads all routes. Routes referenced functions that weren't available during test initialization.
|
|
2953
|
-
- Solution: Mocked index.ts to export only whitelistDomainsForHeaders without executing route setup code.
|
|
2954
|
-
Axios mock missing required properties: The axios mock didn't include properties needed by axios-retry (like interceptors).
|
|
2955
|
-
- Solution: Created a createMockAxiosInstance function that returns a mock axios instance with interceptors, defaults, and HTTP methods.
|
|
2956
|
-
axios-retry not mocked: axios-retry was trying to modify axios instances during module initialization.
|
|
2957
|
-
- Solution: Added a mock for axios-retry to prevent it from executing during tests.
|
|
2958
|
-
Routes file execution: The routes file was being executed when the controller was imported.
|
|
2959
|
-
- Solution: Mocked the routes file to return a simple Express router without executing route definitions.
|
|
2960
|
-
|
|
2961
|
-
CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
|
|
2962
|
-
|
|
2963
|
-
19. [ALWAYS FOLLOW] Write Jest test cases following these MANDATORY patterns to prevent mock pollution:
|
|
2964
|
-
|
|
2965
|
-
SECTION 1: Top-Level Mock Setup
|
|
2966
|
-
Declare ALL jest.mock() calls at the top of the file, outside any describe blocks
|
|
2967
|
-
Mock EVERY function and module the controller/function uses
|
|
2968
|
-
Load the controller/module under test ONCE in beforeAll()
|
|
2969
|
-
[MANDATORY] Always use calculate_relative_path tool to get the correct import path for the module to be used in jest.mock() calls.
|
|
2970
|
-
|
|
2971
|
-
SECTION 2: Module References in Describe Blocks (CRITICAL)
|
|
2972
|
-
Declare ALL module references as let variables at the top of each describe block
|
|
2973
|
-
NEVER use const for module requires inside describe blocks
|
|
2974
|
-
Re-assign these modules inside beforeEach AFTER jest.clearAllMocks()
|
|
2975
|
-
This ensures each test gets fresh module references with clean mocks
|
|
2976
|
-
|
|
2977
|
-
SECTION 3: BeforeEach Pattern (MANDATORY)
|
|
2978
|
-
Pattern Structure:
|
|
2979
|
-
|
|
2980
|
-
Declare let variables for all modules at top of describe block
|
|
2981
|
-
Load controller once in beforeAll
|
|
2982
|
-
In beforeEach: clear mocks first, then re-assign all modules, then reset all mock implementations
|
|
2983
|
-
In afterEach: restore all mocks for extra safety
|
|
2984
|
-
In individual tests: only override specific mocks needed for that test
|
|
2985
|
-
|
|
2986
|
-
Example Pattern:
|
|
2987
|
-
|
|
2988
|
-
Declare: let controller, let helperModule, let responseHelper, let otherDependency
|
|
2989
|
-
beforeAll: controller = require path to controller
|
|
2990
|
-
beforeEach step 1: jest.clearAllMocks()
|
|
2991
|
-
beforeEach step 2: Re-assign all modules with require statements
|
|
2992
|
-
beforeEach step 3: Set default mock implementations for all mocked functions
|
|
2993
|
-
afterEach: jest.restoreAllMocks()
|
|
2994
|
-
In tests: Override only what changes for that specific test case
|
|
2995
|
-
|
|
2996
|
-
SECTION 4: What to NEVER Do
|
|
2997
|
-
|
|
2998
|
-
Never use const for module requires inside describe blocks
|
|
2999
|
-
Never rely on top-level module requires for mocking within tests
|
|
3000
|
-
Never share module references across multiple describe blocks
|
|
3001
|
-
Never skip re-assigning modules in beforeEach
|
|
3002
|
-
Never mutate module exports directly, always use mockImplementation instead
|
|
3003
|
-
Never forget to clear mocks before getting fresh references
|
|
3004
|
-
|
|
3005
|
-
SECTION 5: Key Principles
|
|
3006
|
-
|
|
3007
|
-
Isolation: Each test has its own mock instances via fresh requires
|
|
3008
|
-
Cleanup: Always clear and restore mocks between tests
|
|
3009
|
-
Explicit: Make all dependencies explicit in beforeEach
|
|
3010
|
-
Fresh References: Re-require modules after clearing to get clean mocks
|
|
3011
|
-
Default Behaviors: Set up sensible defaults in beforeEach, override in individual tests
|
|
3012
|
-
|
|
3013
|
-
SECTION 6: Verification Steps
|
|
3014
|
-
|
|
3015
|
-
Run tests multiple times with runInBand flag
|
|
3016
|
-
Run tests in random order with randomize flag
|
|
3017
|
-
Tests should pass consistently regardless of execution order
|
|
3018
|
-
Each test should be completely independent
|
|
3019
|
-
|
|
3020
|
-
SECTION 7: Critical Reminders
|
|
3021
|
-
|
|
3022
|
-
Always add missing mock initializations in relevant beforeEach blocks
|
|
3023
|
-
Ensure all mocked functions have default behaviors set in beforeEach
|
|
3024
|
-
Re-require modules after jest.clearAllMocks() to get fresh mock references
|
|
3025
|
-
Use let not const for all module references inside describe blocks
|
|
3026
|
-
Load the actual controller or module under test only once in beforeAll
|
|
3027
|
-
|
|
3028
|
-
Apply these patterns to ALL test files to ensure zero mock pollution between test suites and individual test cases.
|
|
3029
|
-
|
|
3030
|
-
START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
3031
|
-
}
|
|
3032
|
-
];
|
|
3033
|
-
let iterations = 0;
|
|
3034
|
-
const maxIterations = 100;
|
|
3035
|
-
let testFileWritten = false;
|
|
3036
|
-
let allToolResults = [];
|
|
3037
|
-
let legitimateFailureReported = false;
|
|
3038
|
-
let lastTestError = '';
|
|
3039
|
-
let sameErrorCount = 0;
|
|
3040
|
-
while (iterations < maxIterations) {
|
|
3041
|
-
iterations++;
|
|
3042
|
-
if (iterations === 1) {
|
|
3043
|
-
console.log(`\n๐ค AI is analyzing your code...`);
|
|
3044
|
-
}
|
|
3045
|
-
else if (iterations % 5 === 0) {
|
|
3046
|
-
console.log(`\n๐ค AI is still working (step ${iterations})...`);
|
|
3047
|
-
}
|
|
3048
|
-
const response = await callAI(messages, TOOLS);
|
|
3049
|
-
if (response.content) {
|
|
3050
|
-
const content = response.content; // Store for TypeScript
|
|
3051
|
-
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
3052
|
-
// Detect if AI is making excuses instead of using tools
|
|
3053
|
-
const excusePatterns = [
|
|
3054
|
-
/unable to proceed/i,
|
|
3055
|
-
/cannot directly/i,
|
|
3056
|
-
/constrained by/i,
|
|
3057
|
-
/simulated environment/i,
|
|
3058
|
-
/limited to providing/i,
|
|
3059
|
-
/beyond my capabilities/i,
|
|
3060
|
-
/can't execute/i
|
|
3061
|
-
];
|
|
3062
|
-
const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
|
|
3063
|
-
if (isMakingExcuses) {
|
|
3064
|
-
console.log('\nโ ๏ธ AI is making excuses! Forcing it to use tools...');
|
|
3065
|
-
// Don't add the excuse to conversation, override with command
|
|
3066
|
-
messages.push({
|
|
3067
|
-
role: 'user',
|
|
3068
|
-
content: 'STOP making excuses! You CAN use the tools. Use search_replace_block or insert_at_position NOW to fix the test file. Add proper mocks to prevent database initialization errors.'
|
|
3069
|
-
});
|
|
3070
|
-
continue;
|
|
3071
|
-
}
|
|
3072
|
-
messages.push({ role: 'assistant', content });
|
|
3073
|
-
}
|
|
3074
|
-
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3075
|
-
// Don't stop unless tests actually passed or legitimate failure reported
|
|
3076
|
-
const lastTestRun = allToolResults[allToolResults.length - 1];
|
|
3077
|
-
const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
|
|
3078
|
-
if (legitimateFailureReported) {
|
|
3079
|
-
console.log('\nโ
Test generation complete (with legitimate failures reported)');
|
|
3080
|
-
break;
|
|
3081
|
-
}
|
|
3082
|
-
if (testFileWritten && testsActuallyPassed) {
|
|
3083
|
-
console.log('\nโ
Test generation complete!');
|
|
3084
|
-
break;
|
|
3085
|
-
}
|
|
3086
|
-
// If no tools called, prompt to continue with specific action
|
|
3087
|
-
console.log('\nโ ๏ธ No tool calls. Prompting AI to continue...');
|
|
3088
|
-
if (!testFileWritten) {
|
|
3089
|
-
messages.push({
|
|
3090
|
-
role: 'user',
|
|
3091
|
-
content: 'You have not written the test file yet. Use upsert_function_tests tool NOW with complete test code (not placeholders) for each function.'
|
|
3092
|
-
});
|
|
3093
|
-
}
|
|
3094
|
-
else {
|
|
3095
|
-
messages.push({
|
|
3096
|
-
role: 'user',
|
|
3097
|
-
content: `STOP talking and USE TOOLS!
|
|
3098
|
-
|
|
3099
|
-
If tests are failing:
|
|
3100
|
-
- FIXABLE errors (imports, mocks, assertions):
|
|
3101
|
-
โ
PRIMARY: Use search_replace_block with context (handles whitespace automatically!)
|
|
3102
|
-
๐ ALTERNATIVE: Use insert_at_position for adding imports/mocks
|
|
3103
|
-
โ ๏ธ AVOID: Line-based tools (deprecated, fragile)
|
|
3104
|
-
run_tests tool to run the tests and check if the tests pass.
|
|
3105
|
-
- LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
|
|
3106
|
-
|
|
3107
|
-
Example: search_replace_block({ search: "old code with context...", replace: "new fixed code..." })`
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3110
|
-
continue;
|
|
3111
|
-
}
|
|
3112
|
-
// Execute all tool calls
|
|
3113
|
-
const toolResults = [];
|
|
3114
|
-
for (const toolCall of response.toolCalls) {
|
|
3115
|
-
const result = await executeTool(toolCall.name, toolCall.input);
|
|
3116
|
-
const toolResult = {
|
|
3117
|
-
id: toolCall.id,
|
|
3118
|
-
name: toolCall.name,
|
|
3119
|
-
result
|
|
3120
|
-
};
|
|
3121
|
-
toolResults.push(toolResult);
|
|
3122
|
-
allToolResults.push(toolResult);
|
|
3123
|
-
// Track if legitimate failure was reported
|
|
3124
|
-
if (toolCall.name === 'report_legitimate_failure' && result.success) {
|
|
3125
|
-
legitimateFailureReported = true;
|
|
3126
|
-
console.log('\nโ
Legitimate failure acknowledged. Stopping test fixes.');
|
|
3127
|
-
console.log(` Recommendation: ${result.recommendation}`);
|
|
3128
|
-
}
|
|
3129
|
-
// Track if test file was written
|
|
3130
|
-
if (toolCall.name === 'upsert_function_tests') {
|
|
3131
|
-
if (result.success) {
|
|
3132
|
-
testFileWritten = true;
|
|
3133
|
-
console.log(`\n๐ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
3134
|
-
}
|
|
3135
|
-
}
|
|
3136
|
-
// Detect syntax errors from validation
|
|
3137
|
-
if (result.syntaxError && result.location) {
|
|
3138
|
-
console.log(`\nโ Syntax error introduced at line ${result.location.line}!`);
|
|
3139
|
-
messages.push({
|
|
3140
|
-
role: 'user',
|
|
3141
|
-
content: `๐จ SYNTAX ERROR DETECTED at line ${result.location.line}:${result.location.column}
|
|
3142
|
-
|
|
3143
|
-
${result.error}
|
|
3144
|
-
|
|
3145
|
-
๐ก ${result.suggestion}
|
|
3146
|
-
|
|
3147
|
-
Your last modification created invalid syntax and was ROLLED BACK automatically.
|
|
3148
|
-
|
|
3149
|
-
To fix this:
|
|
3150
|
-
1. Use read_file to see the current file content (includes line numbers)
|
|
3151
|
-
2. Find the section you need to modify around line ${result.location.line}
|
|
3152
|
-
3. Use search_replace_block with correct syntax:
|
|
3153
|
-
- Include 3-5 lines of context around the target
|
|
3154
|
-
- Ensure your replacement has valid syntax (matching brackets, quotes, etc.)
|
|
3155
|
-
- Double-check for missing semicolons, commas, or closing brackets
|
|
3156
|
-
|
|
3157
|
-
Example:
|
|
3158
|
-
search_replace_block({
|
|
3159
|
-
file_path: "${toolCall.input.file_path || toolCall.input.test_file_path || 'test file'}",
|
|
3160
|
-
search: "valid context from file\nline with issue\nmore context",
|
|
3161
|
-
replace: "valid context from file\nCORRECTED line with proper syntax\nmore context"
|
|
3162
|
-
})
|
|
3163
|
-
|
|
3164
|
-
Start NOW by reading the file around line ${result.location.line}!`
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
3167
|
-
// Detect repeated errors (suggests legitimate failure)
|
|
3168
|
-
if (toolCall.name === 'run_tests' && !result.success) {
|
|
3169
|
-
const errorOutput = result.output || result.error || '';
|
|
3170
|
-
const currentError = errorOutput.substring(0, 300); // First 300 chars as signature
|
|
3171
|
-
if (currentError === lastTestError) {
|
|
3172
|
-
sameErrorCount++;
|
|
3173
|
-
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
3174
|
-
if (sameErrorCount >= 3) {
|
|
3175
|
-
console.log('\n๐จ Same error repeated 3+ times! ');
|
|
3176
|
-
messages.push({
|
|
3177
|
-
role: 'user',
|
|
3178
|
-
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
Analyze the error and determine:
|
|
3182
|
-
1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
|
|
3183
|
-
2. Use available tools file read_file_lines to read the current state of file.
|
|
3184
|
-
2. Or is this a LEGITIMATE source code bug?
|
|
3185
|
-
|
|
3186
|
-
If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
|
|
3187
|
-
If FIXABLE: Make one more attempt to fix it.`
|
|
3188
|
-
});
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
else {
|
|
3192
|
-
lastTestError = currentError;
|
|
3193
|
-
sameErrorCount = 1;
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
// Detect import path errors
|
|
3197
|
-
if (toolCall.name === 'run_tests' && !result.success) {
|
|
3198
|
-
const errorOutput = result.output || result.error || '';
|
|
3199
|
-
// Check for module not found errors
|
|
3200
|
-
const moduleNotFoundMatch = errorOutput.match(/Cannot find module ['"]([^'"]+)['"]/);
|
|
3201
|
-
const tsModuleErrorMatch = errorOutput.match(/TS2307.*Cannot find module ['"]([^'"]+)['"]/);
|
|
3202
|
-
if (moduleNotFoundMatch || tsModuleErrorMatch) {
|
|
3203
|
-
const missingModule = moduleNotFoundMatch?.[1] || tsModuleErrorMatch?.[1];
|
|
3204
|
-
console.log(`\n๐ Import error detected: Cannot find module "${missingModule}"`);
|
|
3205
|
-
// Extract filename from the path
|
|
3206
|
-
const filename = missingModule?.split('/').pop();
|
|
3207
|
-
messages.push({
|
|
3208
|
-
role: 'user',
|
|
3209
|
-
content: `Import path error detected! Module not found: "${missingModule}"
|
|
3210
|
-
|
|
3211
|
-
โ
FIX WITH SEARCH-REPLACE:
|
|
3212
|
-
|
|
3213
|
-
Step 1: find_file tool to search for "${filename}" in the repository
|
|
3214
|
-
Step 2: calculate_relative_path tool to get correct import path
|
|
3215
|
-
Step 3: Fix using search_replace_block:
|
|
3216
|
-
a) Include the broken import line + 2-3 surrounding lines for context
|
|
3217
|
-
b) Replace with corrected import using the right path
|
|
3218
|
-
c) The tool handles whitespace/indentation automatically!
|
|
3219
|
-
|
|
3220
|
-
Example workflow:
|
|
3221
|
-
1. find_file({ filename: "${filename}.ts" })
|
|
3222
|
-
2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
|
|
3223
|
-
3. search_replace_block({
|
|
3224
|
-
file_path: "${testFilePath}",
|
|
3225
|
-
search: "import { something } from './other';\nimport { broken } from '${missingModule}';\nimport { another } from './path';",
|
|
3226
|
-
replace: "import { something } from './other';\nimport { fixed } from './correct-path';\nimport { another } from './path';"
|
|
3227
|
-
})
|
|
3228
|
-
|
|
3229
|
-
Start NOW with find_file!`
|
|
3230
|
-
});
|
|
3231
|
-
}
|
|
3232
|
-
// Check for database initialization errors
|
|
3233
|
-
const isDatabaseError = /Cannot read properties of undefined.*reading|database|config|SSL|CA|HOST/i.test(errorOutput);
|
|
3234
|
-
if (isDatabaseError) {
|
|
3235
|
-
console.log('\n๐ Database initialization error detected! Need to add mocks...');
|
|
3236
|
-
messages.push({
|
|
3237
|
-
role: 'user',
|
|
3238
|
-
content: `The test is failing because the source file imports modules that initialize database connections.
|
|
3239
|
-
|
|
3240
|
-
โ
FIX WITH SEARCH-REPLACE:
|
|
3241
|
-
|
|
3242
|
-
Option 1 (Recommended): Use insert_at_position to add mocks at beginning:
|
|
3243
|
-
insert_at_position({
|
|
3244
|
-
file_path: "${testFilePath}",
|
|
3245
|
-
position: "beginning",
|
|
3246
|
-
content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../database/index', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\n"
|
|
3247
|
-
})
|
|
3248
|
-
|
|
3249
|
-
Option 2: If file already has some mocks, use search_replace_block to add more:
|
|
3250
|
-
search_replace_block({
|
|
3251
|
-
file_path: "${testFilePath}",
|
|
3252
|
-
search: "jest.mock('./existing-mock');\n\nimport { something }",
|
|
3253
|
-
replace: "jest.mock('./existing-mock');\njest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\nimport { something }"
|
|
3254
|
-
})
|
|
3255
|
-
|
|
3256
|
-
โ ๏ธ Mocks MUST be at the TOP before any imports!
|
|
3257
|
-
|
|
3258
|
-
Start NOW with insert_at_position!`
|
|
3259
|
-
});
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
// Add tool results to conversation based on provider
|
|
3264
|
-
if (CONFIG.aiProvider === 'claude') {
|
|
3265
|
-
messages.push({
|
|
3266
|
-
role: 'assistant',
|
|
3267
|
-
content: response.toolCalls.map(tc => ({
|
|
3268
|
-
type: 'tool_use',
|
|
3269
|
-
id: tc.id,
|
|
3270
|
-
name: tc.name,
|
|
3271
|
-
input: tc.input
|
|
3272
|
-
}))
|
|
3273
|
-
});
|
|
3274
|
-
messages.push({
|
|
3275
|
-
role: 'user',
|
|
3276
|
-
content: toolResults.map(tr => ({
|
|
3277
|
-
type: 'tool_result',
|
|
3278
|
-
tool_use_id: tr.id,
|
|
3279
|
-
content: JSON.stringify(tr.result)
|
|
3280
|
-
}))
|
|
3281
|
-
});
|
|
3282
|
-
}
|
|
3283
|
-
else if (CONFIG.aiProvider === 'openai') {
|
|
3284
|
-
messages.push({
|
|
3285
|
-
role: 'assistant',
|
|
3286
|
-
tool_calls: response.toolCalls.map(tc => ({
|
|
3287
|
-
id: tc.id,
|
|
3288
|
-
type: 'function',
|
|
3289
|
-
function: {
|
|
3290
|
-
name: tc.name,
|
|
3291
|
-
arguments: JSON.stringify(tc.input)
|
|
3292
|
-
}
|
|
3293
|
-
}))
|
|
3294
|
-
});
|
|
3295
|
-
for (const tr of toolResults) {
|
|
3296
|
-
messages.push({
|
|
3297
|
-
role: 'tool',
|
|
3298
|
-
tool_call_id: tr.id,
|
|
3299
|
-
content: JSON.stringify(tr.result)
|
|
3300
|
-
});
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
else {
|
|
3304
|
-
// Gemini - use proper function call format
|
|
3305
|
-
for (const toolCall of response.toolCalls) {
|
|
3306
|
-
// Add model's function call
|
|
3307
|
-
messages.push({
|
|
3308
|
-
role: 'model',
|
|
3309
|
-
functionCall: {
|
|
3310
|
-
name: toolCall.name,
|
|
3311
|
-
args: toolCall.input
|
|
3312
|
-
}
|
|
3313
|
-
});
|
|
3314
|
-
// Add user's function response
|
|
3315
|
-
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
3316
|
-
messages.push({
|
|
3317
|
-
role: 'user',
|
|
3318
|
-
functionResponse: {
|
|
3319
|
-
name: toolCall.name,
|
|
3320
|
-
response: result?.result
|
|
3321
|
-
}
|
|
3322
|
-
});
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
// Check if legitimate failure was reported
|
|
3326
|
-
if (legitimateFailureReported) {
|
|
3327
|
-
console.log('\nโ
Stopping iteration: Legitimate failure reported.');
|
|
3328
|
-
break;
|
|
3329
|
-
}
|
|
3330
|
-
// Check if tests were run and passed
|
|
3331
|
-
const testRun = toolResults.find(tr => tr.name === 'run_tests');
|
|
3332
|
-
if (testRun?.result.passed) {
|
|
3333
|
-
console.log('\n๐ All tests passed!');
|
|
3334
|
-
break;
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
if (iterations >= maxIterations) {
|
|
3338
|
-
console.log('\nโ ๏ธ Reached maximum iterations. Tests may not be complete.');
|
|
3339
|
-
}
|
|
3340
|
-
if (!testFileWritten) {
|
|
3341
|
-
console.log('\nโ WARNING: Test file was never written! The AI may not have used the tools correctly.');
|
|
3342
|
-
console.log(' Try running again or check your API key and connectivity.');
|
|
3352
|
+
// Filter to only EXPORTED functions
|
|
3353
|
+
const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
|
|
3354
|
+
const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
|
|
3355
|
+
// Error if no exported functions
|
|
3356
|
+
if (functionNames.length === 0) {
|
|
3357
|
+
throw new Error('No exported functions found in file. Cannot generate tests.');
|
|
3343
3358
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3359
|
+
// Log what we found
|
|
3360
|
+
const totalFunctions = result.analysis.functions.length;
|
|
3361
|
+
const internalFunctions = totalFunctions - exportedFunctions.length;
|
|
3362
|
+
// console.log(`โ
Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
|
|
3363
|
+
if (internalFunctions > 0) {
|
|
3364
|
+
// console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
|
|
3347
3365
|
}
|
|
3348
|
-
|
|
3366
|
+
// Always use function-by-function generation
|
|
3367
|
+
return await generateTestsForFunctions(sourceFile, functionNames);
|
|
3349
3368
|
}
|
|
3350
3369
|
// Interactive CLI
|
|
3351
3370
|
async function promptUser(question) {
|
|
@@ -3428,14 +3447,233 @@ async function generateTestsForFolder() {
|
|
|
3428
3447
|
* @returns true if tests passed, false if legitimate failure reported
|
|
3429
3448
|
*/
|
|
3430
3449
|
async function generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists) {
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3450
|
+
// Set the expected test file path globally to prevent AI from creating per-function files
|
|
3451
|
+
EXPECTED_TEST_FILE_PATH = testFilePath;
|
|
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.
|
|
3459
|
+
|
|
3460
|
+
## TARGET
|
|
3461
|
+
Function: ${functionName} | Source: ${sourceFile}
|
|
3462
|
+
Test File: ${testFilePath} (Exists: ${testFileExists})
|
|
3463
|
+
|
|
3464
|
+
## WORKFLOW
|
|
3465
|
+
|
|
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
|
|
3473
|
+
|
|
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"
|
|
3480
|
+
|
|
3481
|
+
### 2. FILE STRUCTURE (STRICT ORDER)
|
|
3482
|
+
|
|
3483
|
+
|
|
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' } }));
|
|
3488
|
+
|
|
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';
|
|
3494
|
+
|
|
3495
|
+
// 3. TYPED MOCKS
|
|
3496
|
+
const mockDep = vi.mocked(dependency);
|
|
3497
|
+
|
|
3498
|
+
// 4. TESTS
|
|
3499
|
+
describe('${functionName}', () => {
|
|
3500
|
+
beforeEach(() => {
|
|
3501
|
+
// clearMocks: true in config handles cleanup
|
|
3502
|
+
mockDep.mockResolvedValue(defaultValue);
|
|
3503
|
+
});
|
|
3504
|
+
|
|
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
|
+
});
|
|
3519
|
+
|
|
3520
|
+
|
|
3521
|
+
### 3. COVERAGE (Minimum 5 tests)
|
|
3522
|
+
|
|
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.
|
|
3435
3671
|
|
|
3436
3672
|
## CONTEXT
|
|
3437
3673
|
Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
3438
3674
|
|
|
3675
|
+
โ ๏ธ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
|
|
3676
|
+
|
|
3439
3677
|
---
|
|
3440
3678
|
|
|
3441
3679
|
## EXECUTION PLAN
|
|
@@ -3448,8 +3686,9 @@ Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
|
3448
3686
|
- Same file: get_function_ast(${sourceFile},{functionName})
|
|
3449
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
|
|
3450
3688
|
4. get_imports_ast โ all dependencies
|
|
3451
|
-
5. calculate_relative_path for
|
|
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.
|
|
3452
3690
|
6. get_file_preamble โ imports and mocks already declared in the file
|
|
3691
|
+
7. search_codebase โ look for relevant context in codebase.
|
|
3453
3692
|
\\\`\\\`\\\`
|
|
3454
3693
|
|
|
3455
3694
|
**Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
|
|
@@ -3461,7 +3700,7 @@ Test file: ${testFilePath} | Exists: ${testFileExists}
|
|
|
3461
3700
|
|
|
3462
3701
|
**Phase 2: Test Generation**
|
|
3463
3702
|
|
|
3464
|
-
Mock Pattern (CRITICAL - Top of file
|
|
3703
|
+
Mock Pattern (CRITICAL - Top of file):
|
|
3465
3704
|
\\\`\\\`\\\`typescript
|
|
3466
3705
|
// ===== MOCKS (BEFORE IMPORTS) =====
|
|
3467
3706
|
jest.mock('config', () => ({
|
|
@@ -3469,14 +3708,19 @@ jest.mock('config', () => ({
|
|
|
3469
3708
|
AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
|
|
3470
3709
|
USER_DEL_SECRET: 'secret'
|
|
3471
3710
|
})
|
|
3472
|
-
}), { virtual: true });
|
|
3711
|
+
}), { virtual: true });
|
|
3712
|
+
|
|
3713
|
+
// virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
|
|
3473
3714
|
|
|
3474
|
-
jest.mock('../
|
|
3475
|
-
// Never virtual:true for actual source helpers!
|
|
3476
|
-
// โ ๏ธ CRITICAL: Mock ALL dependencies at top level, even if unused
|
|
3715
|
+
jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
|
|
3477
3716
|
|
|
3478
3717
|
// ===== IMPORTS =====
|
|
3479
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
|
+
|
|
3480
3724
|
\\\`\\\`\\\`
|
|
3481
3725
|
|
|
3482
3726
|
Requirements (5+ tests minimum):
|
|
@@ -3486,121 +3730,93 @@ Requirements (5+ tests minimum):
|
|
|
3486
3730
|
- โฑ๏ธ Async behavior
|
|
3487
3731
|
- ๐ API null/undefined handling
|
|
3488
3732
|
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
// ===== MOCKS (BEFORE IMPORTS) =====
|
|
3493
|
-
jest.mock('config', () => ({
|
|
3494
|
-
get: () => ({ KEY: 'value' })
|
|
3495
|
-
}), { virtual: true }); // virtual:true ONLY for config, db, models
|
|
3496
|
-
|
|
3497
|
-
jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
|
|
3498
|
-
|
|
3499
|
-
// ===== IMPORTS =====
|
|
3500
|
-
import { functionName } from '../controller';
|
|
3501
|
-
import { dependencyMethod } from '../helpers/dependency';
|
|
3733
|
+
/**
|
|
3734
|
+
* Phase 3: Anti-Pollution Pattern (MUST FOLLOW EXACTLY THIS PATTERN, NO VARIATIONS)
|
|
3735
|
+
*/
|
|
3502
3736
|
|
|
3503
|
-
|
|
3504
|
-
|
|
3737
|
+
\\\`\\\`\\\`typescript
|
|
3738
|
+
// ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
|
|
3739
|
+
afterEach(() => {
|
|
3740
|
+
jest.restoreAllMocks(); // Automatically restores ALL spies
|
|
3741
|
+
});
|
|
3505
3742
|
|
|
3506
|
-
|
|
3743
|
+
// ===== TESTS =====
|
|
3507
3744
|
describe('functionName', () => {
|
|
3508
3745
|
beforeEach(() => {
|
|
3509
|
-
//
|
|
3510
|
-
jest.clearAllMocks();
|
|
3746
|
+
jest.resetAllMocks(); // Resets ALL mocks (call history + implementations)
|
|
3511
3747
|
|
|
3512
|
-
// Set defaults for THIS describe block only
|
|
3513
|
-
|
|
3748
|
+
// Set fresh defaults for THIS describe block only
|
|
3749
|
+
mockDep1.mockResolvedValue({ status: 'success' });
|
|
3750
|
+
mockDep2.mockReturnValue(true);
|
|
3514
3751
|
});
|
|
3515
3752
|
|
|
3516
3753
|
test('happy path', async () => {
|
|
3517
|
-
// Override
|
|
3518
|
-
mockDependencyMethod.mockResolvedValueOnce({ id: 123 });
|
|
3754
|
+
mockDep1.mockResolvedValueOnce({ id: 123 }); // Override for this test only
|
|
3519
3755
|
|
|
3520
3756
|
const result = await functionName();
|
|
3521
3757
|
|
|
3522
3758
|
expect(result).toEqual({ id: 123 });
|
|
3523
|
-
expect(
|
|
3524
|
-
param: 'value'
|
|
3525
|
-
}));
|
|
3759
|
+
expect(mockDep1).toHaveBeenCalledWith(expect.objectContaining({ param: 'value' }));
|
|
3526
3760
|
});
|
|
3527
3761
|
|
|
3528
3762
|
test('error case', async () => {
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const result = await functionName();
|
|
3532
|
-
|
|
3533
|
-
expect(result).toEqual({});
|
|
3763
|
+
mockDep1.mockRejectedValueOnce(new Error('fail'));
|
|
3764
|
+
await expect(functionName()).rejects.toThrow('fail');
|
|
3534
3765
|
});
|
|
3535
3766
|
});
|
|
3536
3767
|
|
|
3537
|
-
|
|
3538
|
-
beforeEach(() => {
|
|
3539
|
-
jest.clearAllMocks();
|
|
3540
|
-
|
|
3541
|
-
// Different defaults for different function
|
|
3542
|
-
mockDependencyMethod.mockResolvedValue({ status: 'pending' });
|
|
3543
|
-
});
|
|
3544
|
-
|
|
3545
|
-
// ... tests
|
|
3546
|
-
});
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
### Step 3: Internal Function Mocking (When Needed)
|
|
3550
|
-
|
|
3768
|
+
// ===== INTERNAL SPIES (When testing same-file function calls) =====
|
|
3551
3769
|
describe('functionWithInternalCalls', () => {
|
|
3552
3770
|
let internalFnSpy: jest.SpyInstance;
|
|
3553
3771
|
|
|
3554
3772
|
beforeEach(() => {
|
|
3555
|
-
jest.
|
|
3773
|
+
jest.resetAllMocks();
|
|
3556
3774
|
|
|
3775
|
+
// โ
EXCEPTION: require() needed here for spying on same module
|
|
3557
3776
|
const controller = require('../controller');
|
|
3558
|
-
internalFnSpy = jest.spyOn(controller, 'internalFunction')
|
|
3559
|
-
.mockResolvedValue(undefined);
|
|
3560
|
-
});
|
|
3561
|
-
|
|
3562
|
-
afterEach(() => {
|
|
3563
|
-
internalFnSpy.mockRestore();
|
|
3777
|
+
internalFnSpy = jest.spyOn(controller, 'internalFunction').mockResolvedValue(undefined);
|
|
3564
3778
|
});
|
|
3779
|
+
|
|
3780
|
+
// No manual restore needed - global afterEach handles it
|
|
3565
3781
|
|
|
3566
3782
|
test('calls internal function', async () => {
|
|
3567
3783
|
await functionWithInternalCalls();
|
|
3568
3784
|
expect(internalFnSpy).toHaveBeenCalled();
|
|
3569
3785
|
});
|
|
3570
3786
|
});
|
|
3787
|
+
\\\`\\\`\\\`
|
|
3571
3788
|
|
|
3572
|
-
### CRITICAL RULES:
|
|
3789
|
+
### CRITICAL RULES (Prevent Mock Pollution):
|
|
3573
3790
|
**DO โ
**
|
|
3574
|
-
1.
|
|
3575
|
-
2.
|
|
3576
|
-
3.
|
|
3577
|
-
4.
|
|
3578
|
-
5.
|
|
3579
|
-
6.
|
|
3580
|
-
7.
|
|
3581
|
-
8. **Use calculate_relative_path** - For all import and mock paths
|
|
3791
|
+
1. \`jest.resetAllMocks()\` as FIRST line in every \`beforeEach()\` (not clearAllMocks)
|
|
3792
|
+
2. Global \`afterEach(() => jest.restoreAllMocks())\` near top of test file
|
|
3793
|
+
3. Set mock defaults in each \`describe\` block's \`beforeEach()\` independently
|
|
3794
|
+
4. Override with \`mockResolvedValueOnce/mockReturnValueOnce\` in individual tests
|
|
3795
|
+
5. Type all mocks: \`const mockFn = fn as jest.MockedFunction<typeof fn>\`
|
|
3796
|
+
6. All \`jest.mock()\` at top before imports (use calculate_relative_path for paths)
|
|
3797
|
+
7. Check for existing mocks with \`get_file_preamble\` tool before adding duplicates
|
|
3582
3798
|
|
|
3583
3799
|
**DON'T โ**
|
|
3584
|
-
1.
|
|
3585
|
-
2.
|
|
3586
|
-
3.
|
|
3587
|
-
4.
|
|
3588
|
-
5.
|
|
3589
|
-
6.
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
- **No pollution**: Each describe block sets its own defaults in beforeEach
|
|
3593
|
-
- **No conflicts**: clearAllMocks() resets all mock state
|
|
3594
|
-
- **Type safety**: TypeScript catches mock mismatches
|
|
3595
|
-
- **Predictable**: Tests run in any order with same results
|
|
3800
|
+
1. Use \`jest.clearAllMocks()\` (only clears history, not implementations) โ Use \`resetAllMocks()\`
|
|
3801
|
+
2. Manually \`.mockReset()\` individual mocks โ \`resetAllMocks()\` handles all
|
|
3802
|
+
3. Share mock state between \`describe\` blocks โ Each block sets its own defaults
|
|
3803
|
+
4. Use \`require()\` except when creating spies on same module being tested
|
|
3804
|
+
5. Use \`virtual:true\` for regular files โ Only for: config, db, models, services (modules not in filesystem)
|
|
3805
|
+
6. Forget global \`afterEach(() => jest.restoreAllMocks())\` โ Causes spy pollution
|
|
3806
|
+
|
|
3807
|
+
|
|
3596
3808
|
|
|
3597
3809
|
**Phase 4: Write Tests**
|
|
3810
|
+
โ ๏ธ CRITICAL REQUIREMENT: Use EXACTLY this test file path: "${testFilePath}"
|
|
3811
|
+
DO NOT modify the path. DO NOT create ${functionName}.test.ts or any other variation.
|
|
3812
|
+
|
|
3598
3813
|
โ upsert_function_tests({
|
|
3599
|
-
test_file_path: "${testFilePath}",
|
|
3814
|
+
test_file_path: "${testFilePath}", // โ ๏ธ USE THIS EXACT PATH - DO NOT CHANGE!
|
|
3600
3815
|
function_name: "${functionName}",
|
|
3601
3816
|
new_test_content: "describe('${functionName}', () => {...})"
|
|
3602
3817
|
})
|
|
3603
3818
|
This will automatically replace the existing test cases for the function with the new test cases or add new test cases if the function is not found in the test file.
|
|
3819
|
+
All functions from the same source file MUST share the same test file.
|
|
3604
3820
|
|
|
3605
3821
|
|
|
3606
3822
|
|
|
@@ -3636,14 +3852,8 @@ This will automatically replace the existing test cases for the function with th
|
|
|
3636
3852
|
| "Test suite failed to run" | get_file_preamble + fix imports/mocks |
|
|
3637
3853
|
| "Cannot find module" | calculate_relative_path |
|
|
3638
3854
|
|
|
3639
|
-
**LEGITIMATE** โ Report, don't fix:
|
|
3640
|
-
- Source returns wrong type
|
|
3641
|
-
- Missing null checks in source
|
|
3642
|
-
- Logic errors in source
|
|
3643
|
-
|
|
3644
|
-
โ NEVER report "Test suite failed to run" as legitimate
|
|
3645
3855
|
|
|
3646
|
-
3๏ธโฃ Repeat until: โ
|
|
3856
|
+
3๏ธโฃ Repeat until: โ
All test cases pass
|
|
3647
3857
|
|
|
3648
3858
|
---
|
|
3649
3859
|
|
|
@@ -3655,10 +3865,14 @@ This will automatically replace the existing test cases for the function with th
|
|
|
3655
3865
|
- Ensure test independence (no pollution)
|
|
3656
3866
|
- Fix test bugs, report source bugs
|
|
3657
3867
|
- [CRITICAL] Each test suite should be completely self-contained and not depend on or affect any other test suite's state.
|
|
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.
|
|
3658
3871
|
|
|
3659
|
-
**START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file
|
|
3660
|
-
|
|
3661
|
-
|
|
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
|
+
}
|
|
3662
3876
|
let iterations = 0;
|
|
3663
3877
|
const maxIterations = 100;
|
|
3664
3878
|
let testFileWritten = false;
|
|
@@ -3667,16 +3881,25 @@ This will automatically replace the existing test cases for the function with th
|
|
|
3667
3881
|
let lastTestError = '';
|
|
3668
3882
|
let sameErrorCount = 0;
|
|
3669
3883
|
while (iterations < maxIterations) {
|
|
3670
|
-
// console.log('USING CLAUDE PROMPT original 16')
|
|
3671
3884
|
iterations++;
|
|
3672
3885
|
if (iterations === 1) {
|
|
3673
|
-
|
|
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
|
+
}
|
|
3674
3892
|
}
|
|
3675
3893
|
else if (iterations % 5 === 0) {
|
|
3676
|
-
|
|
3894
|
+
if (globalSpinner) {
|
|
3895
|
+
globalSpinner.text = `๐ค AI is still working (step ${iterations})...`;
|
|
3896
|
+
}
|
|
3677
3897
|
}
|
|
3678
|
-
|
|
3898
|
+
// console.log('messages', messages);
|
|
3899
|
+
// console.log('TOOLS_FOR_TEST_GENERATION', TOOLS_FOR_TEST_GENERATION);
|
|
3900
|
+
const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
|
|
3679
3901
|
// console.log('response from AI', JSON.stringify(response, null, 2));
|
|
3902
|
+
// console.log('TEst file path', testFilePath);
|
|
3680
3903
|
if (response.content) {
|
|
3681
3904
|
const content = response.content;
|
|
3682
3905
|
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
@@ -3690,7 +3913,7 @@ This will automatically replace the existing test cases for the function with th
|
|
|
3690
3913
|
/beyond my capabilities/i,
|
|
3691
3914
|
/can't execute/i
|
|
3692
3915
|
];
|
|
3693
|
-
const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
|
|
3916
|
+
const isMakingExcuses = excusePatterns.some(pattern => typeof content === 'string' && pattern.test(content));
|
|
3694
3917
|
if (isMakingExcuses) {
|
|
3695
3918
|
console.log('\nโ ๏ธ AI is making excuses! Forcing it to use tools...');
|
|
3696
3919
|
messages.push({
|
|
@@ -3740,7 +3963,7 @@ This works for both NEW and EXISTING test files!`
|
|
|
3740
3963
|
4. Then run_tests to verify
|
|
3741
3964
|
|
|
3742
3965
|
๐ ALTERNATIVE: Use insert_at_position for adding imports/mocks
|
|
3743
|
-
- insert_at_position({ position: 'after_imports', content: "
|
|
3966
|
+
- insert_at_position({ position: 'after_imports', content: "vi.mock('../module');" })
|
|
3744
3967
|
|
|
3745
3968
|
โ ๏ธ SECONDARY: Use upsert_function_tests for function-level rewrites
|
|
3746
3969
|
|
|
@@ -3772,16 +3995,13 @@ Start NOW with search_replace_block or insert_at_position!`
|
|
|
3772
3995
|
const currentError = errorOutput.substring(0, 300);
|
|
3773
3996
|
if (currentError === lastTestError) {
|
|
3774
3997
|
sameErrorCount++;
|
|
3775
|
-
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
3998
|
+
// console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
3776
3999
|
if (sameErrorCount >= 3) {
|
|
3777
|
-
console.log('\n๐จ Same error repeated 3+ times! ');
|
|
4000
|
+
// console.log('\n๐จ Same error repeated 3+ times! ');
|
|
3778
4001
|
messages.push({
|
|
3779
4002
|
role: 'user',
|
|
3780
4003
|
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
If this is a source code bug: Call report_legitimate_failure tool NOW.
|
|
3784
|
-
If this is still fixable: Make focused attempt to fix it.`
|
|
4004
|
+
Make focused attempt to fix the tests using the tools available.`
|
|
3785
4005
|
});
|
|
3786
4006
|
}
|
|
3787
4007
|
}
|
|
@@ -3794,7 +4014,7 @@ If this is still fixable: Make focused attempt to fix it.`
|
|
|
3794
4014
|
if (toolCall.name === 'upsert_function_tests') {
|
|
3795
4015
|
if (result.success) {
|
|
3796
4016
|
testFileWritten = true;
|
|
3797
|
-
console.log(`\n๐ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
4017
|
+
// console.log(`\n๐ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
3798
4018
|
messages.push({
|
|
3799
4019
|
role: 'user',
|
|
3800
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.`
|
|
@@ -3880,12 +4100,194 @@ If this is still fixable: Make focused attempt to fix it.`
|
|
|
3880
4100
|
console.log('\n๐ Test file updated with legitimate failures documented.');
|
|
3881
4101
|
console.log(' These failures indicate bugs in the source code that need to be fixed.');
|
|
3882
4102
|
}
|
|
4103
|
+
// Clear the expected test file path (cleanup)
|
|
4104
|
+
EXPECTED_TEST_FILE_PATH = null;
|
|
3883
4105
|
// Return true if tests passed, false if legitimate failure reported
|
|
3884
4106
|
// Get the LAST test run result (not the first) to check final status
|
|
3885
4107
|
const testRuns = allToolResults.filter(tr => tr.name === 'run_tests');
|
|
3886
4108
|
const lastTestRun = testRuns.length > 0 ? testRuns[testRuns.length - 1] : null;
|
|
3887
4109
|
return !legitimateFailureReported && (lastTestRun?.result?.passed || false);
|
|
3888
4110
|
}
|
|
4111
|
+
/**
|
|
4112
|
+
* Smart validation that fixes failing tests
|
|
4113
|
+
* - Runs full test suite
|
|
4114
|
+
* - For failures, attempts to fix all failing tests using AI
|
|
4115
|
+
*/
|
|
4116
|
+
async function smartValidateTestSuite(sourceFile, testFilePath, functionNames) {
|
|
4117
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
4118
|
+
console.log(`๐ VALIDATION: Running full test suite (${functionNames.length} function(s))`);
|
|
4119
|
+
console.log(`${'='.repeat(80)}\n`);
|
|
4120
|
+
// Run tests for entire file (no function filter)
|
|
4121
|
+
let fullSuiteResult;
|
|
4122
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4123
|
+
fullSuiteResult = runTestsVitest(testFilePath);
|
|
4124
|
+
}
|
|
4125
|
+
else {
|
|
4126
|
+
fullSuiteResult = runTestsJest(testFilePath);
|
|
4127
|
+
}
|
|
4128
|
+
if (fullSuiteResult.passed) {
|
|
4129
|
+
console.log(`\nโ
Full test suite passed! All ${functionNames.length} function(s) working together correctly.`);
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
console.log(`\nโ ๏ธ Full test suite has failures. Attempting to fix failing tests...\n`);
|
|
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`);
|
|
4142
|
+
if (failingTests.length === 0) {
|
|
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);
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
console.log(`Found ${failingTests.length} failing test(s): ${failingTests.join(', ')}\n`);
|
|
4150
|
+
// Attempt to fix all failing tests
|
|
4151
|
+
await fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteResult.output);
|
|
4152
|
+
}
|
|
4153
|
+
/**
|
|
4154
|
+
* Fix failing tests using AI
|
|
4155
|
+
* Attempts to fix all test issues including pollution, imports, mocks, etc.
|
|
4156
|
+
*/
|
|
4157
|
+
async function fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteOutput) {
|
|
4158
|
+
const messages = [
|
|
4159
|
+
{
|
|
4160
|
+
role: 'user',
|
|
4161
|
+
content: `You are fixing FAILING TESTS in the Vitest test suite.
|
|
4162
|
+
|
|
4163
|
+
Source file: ${sourceFile}
|
|
4164
|
+
Test file: ${testFilePath}
|
|
4165
|
+
Functions tested: ${functionNames.join(', ')}
|
|
4166
|
+
|
|
4167
|
+
FAILING TESTS:
|
|
4168
|
+
${failingTests.map(t => `- ${t}`).join('\n')}
|
|
4169
|
+
|
|
4170
|
+
Full suite output:
|
|
4171
|
+
${fullSuiteOutput}
|
|
4172
|
+
|
|
4173
|
+
YOUR TASK - Fix all failing tests:
|
|
4174
|
+
|
|
4175
|
+
COMMON ISSUES TO FIX:
|
|
4176
|
+
- Mock state bleeding between describe blocks
|
|
4177
|
+
- Missing vitest imports (describe, it, expect, beforeEach, vi)
|
|
4178
|
+
- Incorrect mock typing (use MockedFunction from vitest)
|
|
4179
|
+
- beforeEach not setting up mocks properly
|
|
4180
|
+
- Missing or incorrect imports
|
|
4181
|
+
- Mock implementation issues
|
|
4182
|
+
- Incorrect test assertions
|
|
4183
|
+
- Test logic errors
|
|
4184
|
+
|
|
4185
|
+
NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled.
|
|
4186
|
+
|
|
4187
|
+
TOOLS TO USE:
|
|
4188
|
+
1. get_file_preamble - See current setup
|
|
4189
|
+
2. search_replace_block - Fix specific sections (preferred)
|
|
4190
|
+
3. insert_at_position - Add missing imports/mocks
|
|
4191
|
+
4. run_tests - Verify fixes
|
|
4192
|
+
|
|
4193
|
+
START by calling get_file_preamble to see the current test structure.`
|
|
4194
|
+
}
|
|
4195
|
+
];
|
|
4196
|
+
let iterations = 0;
|
|
4197
|
+
const maxIterations = 100; // Limit iterations for test fixes
|
|
4198
|
+
while (iterations < maxIterations) {
|
|
4199
|
+
iterations++;
|
|
4200
|
+
console.log(`\n๐ง Test fix attempt ${iterations}/${maxIterations}...`);
|
|
4201
|
+
const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
|
|
4202
|
+
if (response.content) {
|
|
4203
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
4204
|
+
}
|
|
4205
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
4206
|
+
// AI stopped - check if tests pass now
|
|
4207
|
+
let finalTest;
|
|
4208
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4209
|
+
finalTest = runTestsVitest(testFilePath);
|
|
4210
|
+
}
|
|
4211
|
+
else {
|
|
4212
|
+
finalTest = runTestsJest(testFilePath);
|
|
4213
|
+
}
|
|
4214
|
+
if (finalTest.passed) {
|
|
4215
|
+
console.log('\nโ
Tests fixed! Full test suite now passes.');
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
console.log('\nโ ๏ธ AI stopped but tests still failing.');
|
|
4219
|
+
break;
|
|
4220
|
+
}
|
|
4221
|
+
// Execute tool calls
|
|
4222
|
+
const toolResults = [];
|
|
4223
|
+
for (const toolCall of response.toolCalls) {
|
|
4224
|
+
const result = await executeTool(toolCall.name, toolCall.input);
|
|
4225
|
+
toolResults.push({ id: toolCall.id, name: toolCall.name, result });
|
|
4226
|
+
// Check if tests passed
|
|
4227
|
+
if (toolCall.name === 'run_tests' && result.passed) {
|
|
4228
|
+
console.log('\nโ
Tests fixed! Full test suite now passes.');
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
// Add tool results to conversation
|
|
4233
|
+
if (CONFIG.aiProvider === 'claude') {
|
|
4234
|
+
messages.push({
|
|
4235
|
+
role: 'assistant',
|
|
4236
|
+
content: response.toolCalls.map(tc => ({
|
|
4237
|
+
type: 'tool_use',
|
|
4238
|
+
id: tc.id,
|
|
4239
|
+
name: tc.name,
|
|
4240
|
+
input: tc.input
|
|
4241
|
+
}))
|
|
4242
|
+
});
|
|
4243
|
+
messages.push({
|
|
4244
|
+
role: 'user',
|
|
4245
|
+
content: toolResults.map(tr => ({
|
|
4246
|
+
type: 'tool_result',
|
|
4247
|
+
tool_use_id: tr.id,
|
|
4248
|
+
content: JSON.stringify(tr.result)
|
|
4249
|
+
}))
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
else if (CONFIG.aiProvider === 'openai') {
|
|
4253
|
+
messages.push({
|
|
4254
|
+
role: 'assistant',
|
|
4255
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
4256
|
+
id: tc.id,
|
|
4257
|
+
type: 'function',
|
|
4258
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
4259
|
+
}))
|
|
4260
|
+
});
|
|
4261
|
+
for (const tr of toolResults) {
|
|
4262
|
+
messages.push({
|
|
4263
|
+
role: 'tool',
|
|
4264
|
+
tool_call_id: tr.id,
|
|
4265
|
+
content: JSON.stringify(tr.result)
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
else {
|
|
4270
|
+
for (const toolCall of response.toolCalls) {
|
|
4271
|
+
messages.push({
|
|
4272
|
+
role: 'model',
|
|
4273
|
+
functionCall: {
|
|
4274
|
+
name: toolCall.name,
|
|
4275
|
+
args: toolCall.input
|
|
4276
|
+
}
|
|
4277
|
+
});
|
|
4278
|
+
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
4279
|
+
messages.push({
|
|
4280
|
+
role: 'user',
|
|
4281
|
+
functionResponse: {
|
|
4282
|
+
name: toolCall.name,
|
|
4283
|
+
response: result?.result
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
console.log('\nโ ๏ธ Could not automatically fix all failing tests. Manual review may be needed.');
|
|
4290
|
+
}
|
|
3889
4291
|
/** [Not useful, introduce side-effect]
|
|
3890
4292
|
* Validate and fix the complete test file after all functions are processed
|
|
3891
4293
|
* Runs full test suite and fixes file-level issues (mock pollution, imports, etc)
|
|
@@ -3895,7 +4297,13 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
|
|
|
3895
4297
|
console.log(`๐ FINAL VALIDATION: Running complete test suite`);
|
|
3896
4298
|
console.log(`${'='.repeat(80)}\n`);
|
|
3897
4299
|
// Run tests for entire file (no function filter)
|
|
3898
|
-
|
|
4300
|
+
let testResult;
|
|
4301
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4302
|
+
testResult = runTestsVitest(testFilePath);
|
|
4303
|
+
}
|
|
4304
|
+
else {
|
|
4305
|
+
testResult = runTestsJest(testFilePath);
|
|
4306
|
+
}
|
|
3899
4307
|
if (testResult.passed) {
|
|
3900
4308
|
console.log(`\nโ
Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
|
|
3901
4309
|
return;
|
|
@@ -3905,7 +4313,7 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
|
|
|
3905
4313
|
const messages = [
|
|
3906
4314
|
{
|
|
3907
4315
|
role: 'user',
|
|
3908
|
-
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.
|
|
3909
4317
|
|
|
3910
4318
|
Source file: ${sourceFile}
|
|
3911
4319
|
Test file: ${testFilePath}
|
|
@@ -3918,7 +4326,7 @@ CONTEXT:
|
|
|
3918
4326
|
* Mock pollution between test suites
|
|
3919
4327
|
* Shared state not being cleaned up
|
|
3920
4328
|
* Import/mock ordering issues
|
|
3921
|
-
* beforeEach
|
|
4329
|
+
* beforeEach setup issues
|
|
3922
4330
|
* Mock implementations interfering with each other
|
|
3923
4331
|
|
|
3924
4332
|
TEST OUTPUT:
|
|
@@ -3929,35 +4337,27 @@ YOUR TASK:
|
|
|
3929
4337
|
2. Identify file-level issues (NOT individual function logic issues)
|
|
3930
4338
|
3. Fix using search_replace_block or insert_at_position tools
|
|
3931
4339
|
4. Run tests again with run_tests tool
|
|
3932
|
-
5. Repeat until
|
|
4340
|
+
5. Repeat until all test cases pass
|
|
3933
4341
|
|
|
3934
4342
|
COMMON FILE-LEVEL ISSUES TO CHECK:
|
|
3935
|
-
- โ Missing
|
|
3936
|
-
- โ
|
|
3937
|
-
- โ
|
|
3938
|
-
- โ Missing virtual:true for config/database/models mocks
|
|
3939
|
-
- โ 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
|
|
3940
4346
|
- โ Test suites depending on execution order
|
|
3941
4347
|
|
|
4348
|
+
NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled for auto-cleanup.
|
|
4349
|
+
|
|
3942
4350
|
FIXABLE ISSUES (you should fix):
|
|
3943
4351
|
- Mock pollution between test suites
|
|
3944
|
-
- Missing cleanup in beforeEach/afterEach
|
|
3945
4352
|
- Incorrect mock setup at file level
|
|
3946
4353
|
- Import ordering issues
|
|
3947
|
-
- Missing
|
|
3948
|
-
|
|
3949
|
-
LEGITIMATE FAILURES (report these):
|
|
3950
|
-
- Source code bugs causing actual logic errors
|
|
3951
|
-
- Missing null checks in source code
|
|
3952
|
-
- Wrong return types from source functions
|
|
3953
|
-
- Use report_legitimate_failure tool for these
|
|
4354
|
+
- Missing vitest imports
|
|
3954
4355
|
|
|
3955
4356
|
CRITICAL RULES:
|
|
3956
4357
|
1. DO NOT change individual test logic (they passed individually!)
|
|
3957
4358
|
2. Focus ONLY on file-level integration issues
|
|
3958
4359
|
3. Use search_replace_block to fix specific sections
|
|
3959
4360
|
4. Preserve all existing test cases
|
|
3960
|
-
5. If failures are due to source code bugs, call report_legitimate_failure and STOP
|
|
3961
4361
|
|
|
3962
4362
|
START by calling get_file_preamble to understand current file structure.`
|
|
3963
4363
|
}
|
|
@@ -3969,13 +4369,19 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
3969
4369
|
while (iterations < maxIterations) {
|
|
3970
4370
|
iterations++;
|
|
3971
4371
|
console.log(`\n๐ง Fixing attempt ${iterations}/${maxIterations}...`);
|
|
3972
|
-
const response = await callAI(messages,
|
|
4372
|
+
const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
|
|
3973
4373
|
if (response.content) {
|
|
3974
4374
|
messages.push({ role: 'assistant', content: response.content });
|
|
3975
4375
|
}
|
|
3976
4376
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3977
4377
|
// AI stopped without fixing - check if tests pass now
|
|
3978
|
-
|
|
4378
|
+
let finalTest;
|
|
4379
|
+
if (CONFIG.testEnv == 'vitest') {
|
|
4380
|
+
finalTest = runTestsVitest(testFilePath);
|
|
4381
|
+
}
|
|
4382
|
+
else {
|
|
4383
|
+
finalTest = runTestsJest(testFilePath);
|
|
4384
|
+
}
|
|
3979
4385
|
if (finalTest.passed) {
|
|
3980
4386
|
console.log('\nโ
Complete test suite now passes!');
|
|
3981
4387
|
return;
|
|
@@ -3983,7 +4389,7 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
3983
4389
|
console.log('\nโ ๏ธ AI stopped but tests still failing. Prompting to continue...');
|
|
3984
4390
|
messages.push({
|
|
3985
4391
|
role: 'user',
|
|
3986
|
-
content: 'Tests are still failing! Use tools to fix
|
|
4392
|
+
content: 'Tests are still failing! Use tools to fix.'
|
|
3987
4393
|
});
|
|
3988
4394
|
continue;
|
|
3989
4395
|
}
|
|
@@ -4073,43 +4479,74 @@ START by calling get_file_preamble to understand current file structure.`
|
|
|
4073
4479
|
* Generate tests for multiple functions, one at a time
|
|
4074
4480
|
*/
|
|
4075
4481
|
async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
4076
|
-
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`);
|
|
4077
4483
|
const testFilePath = getTestFilePath(sourceFile);
|
|
4078
4484
|
let testFileExists = fsSync.existsSync(testFilePath);
|
|
4485
|
+
// Read validation interval from config
|
|
4486
|
+
const validationInterval = CONFIG.validationInterval;
|
|
4079
4487
|
// Process each function one at a time
|
|
4080
4488
|
for (let i = 0; i < functionNames.length; i++) {
|
|
4081
4489
|
const functionName = functionNames[i];
|
|
4490
|
+
// Clear spinner before showing section header
|
|
4491
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4492
|
+
globalSpinner.stop();
|
|
4493
|
+
globalSpinner = null;
|
|
4494
|
+
}
|
|
4082
4495
|
console.log(`\n${'='.repeat(80)}`);
|
|
4083
4496
|
console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
|
|
4084
4497
|
console.log(`${'='.repeat(80)}\n`);
|
|
4085
4498
|
const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
|
|
4086
4499
|
// After first function completes, test file will exist for subsequent functions
|
|
4087
4500
|
testFileExists = true;
|
|
4501
|
+
// Clear spinner before showing completion
|
|
4502
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4503
|
+
globalSpinner.stop();
|
|
4504
|
+
globalSpinner = null;
|
|
4505
|
+
}
|
|
4088
4506
|
if (passed) {
|
|
4089
4507
|
console.log(`\nโ
Function '${functionName}' tests completed successfully!`);
|
|
4090
4508
|
}
|
|
4091
4509
|
else {
|
|
4092
4510
|
console.log(`\nโ ๏ธ Function '${functionName}' completed with issues. Continuing to next function...`);
|
|
4093
4511
|
}
|
|
4512
|
+
// Periodic validation checkpoint (only if validation is enabled in config)
|
|
4513
|
+
if (validationInterval !== undefined && validationInterval !== null) {
|
|
4514
|
+
const isPeriodicCheckpoint = validationInterval > 0 && (i + 1) % validationInterval === 0;
|
|
4515
|
+
const isFinalFunction = i === functionNames.length - 1;
|
|
4516
|
+
if (isPeriodicCheckpoint || isFinalFunction) {
|
|
4517
|
+
console.log(`\n${'โ'.repeat(80)}`);
|
|
4518
|
+
console.log(`๐ CHECKPOINT ${i + 1}/${functionNames.length}: Running full suite validation...`);
|
|
4519
|
+
console.log(`${'โ'.repeat(80)}`);
|
|
4520
|
+
await smartValidateTestSuite(sourceFile, testFilePath, functionNames.slice(0, i + 1));
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4094
4523
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
4095
4524
|
}
|
|
4096
4525
|
console.log(`\n${'='.repeat(80)}`);
|
|
4097
4526
|
console.log(`โ
All ${functionNames.length} function(s) processed!`);
|
|
4098
4527
|
console.log(`${'='.repeat(80)}\n`);
|
|
4099
|
-
// FINAL VALIDATION: Run complete test suite and fix file-level issues
|
|
4100
|
-
// await validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames);
|
|
4101
4528
|
return testFilePath;
|
|
4102
4529
|
}
|
|
4103
4530
|
async function generateTestsForFunction() {
|
|
4104
4531
|
console.log('\n๐ฏ Function-wise Test Generation\n');
|
|
4105
4532
|
// List all files
|
|
4106
|
-
|
|
4533
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4534
|
+
globalSpinner = ora('๐ Scanning repository...').start();
|
|
4535
|
+
}
|
|
4536
|
+
else {
|
|
4537
|
+
globalSpinner.text = '๐ Scanning repository...';
|
|
4538
|
+
}
|
|
4107
4539
|
const files = await listFilesRecursive('.');
|
|
4108
4540
|
if (files.length === 0) {
|
|
4109
|
-
|
|
4541
|
+
globalSpinner.fail('No source files found!');
|
|
4542
|
+
globalSpinner = null;
|
|
4110
4543
|
return;
|
|
4111
4544
|
}
|
|
4112
|
-
|
|
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');
|
|
4113
4550
|
files.forEach((file, index) => {
|
|
4114
4551
|
console.log(`${index + 1}. ${file}`);
|
|
4115
4552
|
});
|
|
@@ -4145,6 +4582,11 @@ async function generateTestsForFunction() {
|
|
|
4145
4582
|
}
|
|
4146
4583
|
console.log(`\nโ
Selected functions: ${selectedFunctions.join(', ')}\n`);
|
|
4147
4584
|
await generateTestsForFunctions(selectedFile, selectedFunctions);
|
|
4585
|
+
// Clear spinner before final message
|
|
4586
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4587
|
+
globalSpinner.stop();
|
|
4588
|
+
globalSpinner = null;
|
|
4589
|
+
}
|
|
4148
4590
|
console.log('\nโจ Done!');
|
|
4149
4591
|
}
|
|
4150
4592
|
/**
|
|
@@ -4272,7 +4714,7 @@ Return ONLY the JSON array, nothing else.`;
|
|
|
4272
4714
|
return [];
|
|
4273
4715
|
}
|
|
4274
4716
|
// Parse AI response to extract function names
|
|
4275
|
-
const content = response.content.trim();
|
|
4717
|
+
const content = typeof response.content === 'string' ? response.content.trim() : JSON.stringify(response.content || '');
|
|
4276
4718
|
// console.log(` ๐ค AI response: ${content}`);
|
|
4277
4719
|
// Try to extract JSON array from response
|
|
4278
4720
|
const jsonMatch = content.match(/\[.*\]/s);
|
|
@@ -4304,16 +4746,25 @@ Return ONLY the JSON array, nothing else.`;
|
|
|
4304
4746
|
* Auto-generate tests for changed functions detected via git diff
|
|
4305
4747
|
*/
|
|
4306
4748
|
async function autoGenerateTests() {
|
|
4307
|
-
|
|
4749
|
+
if (!globalSpinner) {
|
|
4750
|
+
globalSpinner = ora('๐ Scanning git changes...').start();
|
|
4751
|
+
}
|
|
4752
|
+
else {
|
|
4753
|
+
globalSpinner.text = '๐ Scanning git changes...';
|
|
4754
|
+
globalSpinner.start();
|
|
4755
|
+
}
|
|
4308
4756
|
try {
|
|
4309
4757
|
// Get all changes from git diff
|
|
4310
4758
|
const { fullDiff, files } = await getGitDiff();
|
|
4311
4759
|
if (files.length === 0) {
|
|
4760
|
+
globalSpinner.stop();
|
|
4761
|
+
globalSpinner = null;
|
|
4312
4762
|
console.log('โ
No changes detected in source files.');
|
|
4313
4763
|
console.log(' (Only staged and unstaged changes are checked)');
|
|
4314
4764
|
return;
|
|
4315
4765
|
}
|
|
4316
|
-
|
|
4766
|
+
globalSpinner.text = `Found changes in ${files.length} file(s)`;
|
|
4767
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4317
4768
|
let totalFunctions = 0;
|
|
4318
4769
|
let processedFiles = 0;
|
|
4319
4770
|
let errorFiles = 0;
|
|
@@ -4322,25 +4773,41 @@ async function autoGenerateTests() {
|
|
|
4322
4773
|
const { filePath, diff } = fileInfo;
|
|
4323
4774
|
// Check if file exists
|
|
4324
4775
|
if (!fsSync.existsSync(filePath)) {
|
|
4325
|
-
|
|
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));
|
|
4326
4783
|
continue;
|
|
4327
4784
|
}
|
|
4328
|
-
|
|
4329
|
-
|
|
4785
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4786
|
+
globalSpinner = ora(`๐ Processing: ${path.basename(filePath)}`).start();
|
|
4787
|
+
}
|
|
4788
|
+
else {
|
|
4789
|
+
globalSpinner.text = `๐ Processing: ${path.basename(filePath)}`;
|
|
4790
|
+
}
|
|
4330
4791
|
// Use AI to extract changed function names from diff
|
|
4792
|
+
globalSpinner.text = `๐ค Analyzing diff with AI: ${path.basename(filePath)}`;
|
|
4331
4793
|
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4332
4794
|
// console.log('Changed functions are', changedFunctions);
|
|
4333
4795
|
if (changedFunctions.length === 0) {
|
|
4334
|
-
|
|
4796
|
+
globalSpinner.text = `โญ๏ธ No exported functions changed in ${path.basename(filePath)}`;
|
|
4797
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
4335
4798
|
continue;
|
|
4336
4799
|
}
|
|
4337
|
-
|
|
4800
|
+
globalSpinner.text = `Found ${changedFunctions.length} function(s): ${changedFunctions.join(', ')}`;
|
|
4801
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4338
4802
|
try {
|
|
4339
4803
|
// Use existing generateTestsForFunctions
|
|
4340
4804
|
await generateTestsForFunctions(filePath, changedFunctions);
|
|
4341
4805
|
processedFiles++;
|
|
4342
4806
|
totalFunctions += changedFunctions.length;
|
|
4343
|
-
|
|
4807
|
+
if (globalSpinner) {
|
|
4808
|
+
globalSpinner.text = `Tests generated for ${path.basename(filePath)}`;
|
|
4809
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4810
|
+
}
|
|
4344
4811
|
}
|
|
4345
4812
|
catch (error) {
|
|
4346
4813
|
errorFiles++;
|
|
@@ -4351,6 +4818,11 @@ async function autoGenerateTests() {
|
|
|
4351
4818
|
// Summary
|
|
4352
4819
|
console.log('\n' + '='.repeat(60));
|
|
4353
4820
|
console.log('๐ Auto-Generation Summary');
|
|
4821
|
+
// Clear spinner before final summary
|
|
4822
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
4823
|
+
globalSpinner.stop();
|
|
4824
|
+
globalSpinner = null;
|
|
4825
|
+
}
|
|
4354
4826
|
console.log('='.repeat(60));
|
|
4355
4827
|
console.log(`โ
Successfully processed: ${processedFiles} file(s)`);
|
|
4356
4828
|
console.log(`๐ Functions tested: ${totalFunctions}`);
|
|
@@ -4369,11 +4841,526 @@ async function autoGenerateTests() {
|
|
|
4369
4841
|
throw error;
|
|
4370
4842
|
}
|
|
4371
4843
|
}
|
|
4844
|
+
/**
|
|
4845
|
+
* Review code changes for quality, bugs, performance, and security issues
|
|
4846
|
+
*/
|
|
4847
|
+
async function reviewChangedFiles() {
|
|
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
|
+
}
|
|
4855
|
+
try {
|
|
4856
|
+
// Get all changes from git diff
|
|
4857
|
+
const { fullDiff, files } = await getGitDiff();
|
|
4858
|
+
if (files.length === 0) {
|
|
4859
|
+
globalSpinner.stop();
|
|
4860
|
+
globalSpinner = null;
|
|
4861
|
+
console.log('โ
No changes detected in source files.');
|
|
4862
|
+
console.log(' (Only staged and unstaged changes are checked)');
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
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 = [];
|
|
4869
|
+
for (const fileInfo of files) {
|
|
4870
|
+
const { filePath, diff } = fileInfo;
|
|
4871
|
+
// Check if file exists
|
|
4872
|
+
if (!fsSync.existsSync(filePath)) {
|
|
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));
|
|
4880
|
+
continue;
|
|
4881
|
+
}
|
|
4882
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
4883
|
+
globalSpinner = ora(`๐ Analyzing: ${filePath}`).start();
|
|
4884
|
+
}
|
|
4885
|
+
else {
|
|
4886
|
+
globalSpinner.text = `๐ Analyzing: ${filePath}`;
|
|
4887
|
+
}
|
|
4888
|
+
// Use AI to extract changed function names from diff
|
|
4889
|
+
const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
|
|
4890
|
+
if (changedFunctions.length === 0) {
|
|
4891
|
+
globalSpinner.text = `โญ๏ธ No exported functions changed in ${path.basename(filePath)}`;
|
|
4892
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
4893
|
+
continue;
|
|
4894
|
+
}
|
|
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;
|
|
4910
|
+
}
|
|
4911
|
+
console.log('\nโ
Code review completed!');
|
|
4912
|
+
}
|
|
4913
|
+
catch (error) {
|
|
4914
|
+
console.error(`\nโ Error during review: ${error.message}`);
|
|
4915
|
+
}
|
|
4916
|
+
// Summary
|
|
4917
|
+
console.log('\n' + '='.repeat(60));
|
|
4918
|
+
console.log('๐ Code Review Summary');
|
|
4919
|
+
console.log('='.repeat(60));
|
|
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`);
|
|
4923
|
+
console.log('='.repeat(60));
|
|
4924
|
+
console.log('\nโจ Done!');
|
|
4925
|
+
}
|
|
4926
|
+
catch (error) {
|
|
4927
|
+
if (error.message === 'Not a git repository') {
|
|
4928
|
+
console.error('โ Error: Not a git repository');
|
|
4929
|
+
console.error(' Review mode requires git to detect changes.');
|
|
4930
|
+
process.exit(1);
|
|
4931
|
+
}
|
|
4932
|
+
throw error;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
/**
|
|
4936
|
+
* Build AI prompt for a specific review step based on its ruleset
|
|
4937
|
+
*/
|
|
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}**
|
|
4961
|
+
|
|
4962
|
+
## CONTEXT
|
|
4963
|
+
Review Focus: ${step.name}
|
|
4964
|
+
Category: ${step.category}
|
|
4965
|
+
Total files changed: ${filesToReview.length}
|
|
4966
|
+
Total functions changed: ${totalFunctions}
|
|
4967
|
+
|
|
4968
|
+
Changed files and functions:
|
|
4969
|
+
${filesContext}
|
|
4970
|
+
|
|
4971
|
+
## GIT DIFFS
|
|
4972
|
+
|
|
4973
|
+
${filesToReview.map(f => `### ${f.filePath}
|
|
4974
|
+
\`\`\`diff
|
|
4975
|
+
${f.diff}
|
|
4976
|
+
\`\`\`
|
|
4977
|
+
`).join('\n')}
|
|
4978
|
+
|
|
4979
|
+
## YOUR TASK
|
|
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.
|
|
4982
|
+
|
|
4983
|
+
## ANALYSIS STEPS
|
|
4984
|
+
|
|
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?
|
|
4995
|
+
|
|
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}.
|
|
5003
|
+
|
|
5004
|
+
**Phase 3: Write Review**
|
|
5005
|
+
|
|
5006
|
+
Use the write_review tool to save findings to: ${stepOutputPath}
|
|
5007
|
+
|
|
5008
|
+
## OUTPUT FORMAT
|
|
5009
|
+
|
|
5010
|
+
Your review MUST be in markdown format with the following structure:
|
|
5011
|
+
|
|
5012
|
+
\`\`\`markdown
|
|
5013
|
+
# ${step.name}
|
|
5014
|
+
|
|
5015
|
+
|
|
5016
|
+
## Findings
|
|
5017
|
+
|
|
5018
|
+
### ๐ด Critical Issues
|
|
5019
|
+
|
|
5020
|
+
#### [Issue Title]
|
|
5021
|
+
**File**: \`filePath\`
|
|
5022
|
+
**Function**: \`functionName\`
|
|
5023
|
+
**Severity**: Critical
|
|
5024
|
+
|
|
5025
|
+
**Issue**:
|
|
5026
|
+
[Description]
|
|
5027
|
+
|
|
5028
|
+
**Current Code**:
|
|
5029
|
+
\`\`\`typescript
|
|
5030
|
+
// problematic code
|
|
5031
|
+
\`\`\`
|
|
5032
|
+
|
|
5033
|
+
**Recommended Code**:
|
|
5034
|
+
\`\`\`typescript
|
|
5035
|
+
// improved code
|
|
5036
|
+
\`\`\`
|
|
5037
|
+
|
|
5038
|
+
**Rationale**:
|
|
5039
|
+
[Why this is important]
|
|
5040
|
+
|
|
5041
|
+
---
|
|
5042
|
+
|
|
5043
|
+
### ๐ High Priority Issues
|
|
5044
|
+
[Same format as above]
|
|
5045
|
+
|
|
5046
|
+
---
|
|
5047
|
+
|
|
5048
|
+
### ๐ก Medium Priority Issues
|
|
5049
|
+
[Same format as above]
|
|
5050
|
+
|
|
5051
|
+
---
|
|
5052
|
+
|
|
5053
|
+
## Positive Aspects
|
|
5054
|
+
[What was done well in this area]
|
|
5055
|
+
|
|
5056
|
+
## Recommendations
|
|
5057
|
+
[Specific recommendations for ${step.name}]
|
|
5058
|
+
\`\`\`
|
|
5059
|
+
|
|
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.
|
|
5072
|
+
|
|
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
|
|
5197
|
+
|
|
5198
|
+
**Date**: ${timestamp}
|
|
5199
|
+
**Reviewer**: AI Code Review System
|
|
5200
|
+
**Files Changed**: ${filesToReview.length}
|
|
5201
|
+
**Functions Changed**: ${totalFunctions}
|
|
5202
|
+
|
|
5203
|
+
---
|
|
5204
|
+
|
|
5205
|
+
## Summary
|
|
5206
|
+
|
|
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(', ')}.
|
|
5208
|
+
|
|
5209
|
+
---
|
|
5210
|
+
|
|
5211
|
+
## Files Changed
|
|
5212
|
+
|
|
5213
|
+
${filesToReview.map(f => `- **${f.filePath}**
|
|
5214
|
+
- Functions: ${f.functions.join(', ')}`).join('\n')}
|
|
5215
|
+
|
|
5216
|
+
---
|
|
5217
|
+
|
|
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`;
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
catch (error) {
|
|
5245
|
+
mergedReview += `## ${result.stepName}\n\nโ ๏ธ Error reading review: ${error.message}\n\n---\n\n`;
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
else {
|
|
5249
|
+
mergedReview += `## ${result.stepName}\n\nโ **Review step failed**: ${result.error}\n\n---\n\n`;
|
|
5250
|
+
}
|
|
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));
|
|
5274
|
+
}
|
|
5275
|
+
await fs.rmdir(tmpDir);
|
|
5276
|
+
}
|
|
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();
|
|
5304
|
+
}
|
|
5305
|
+
else {
|
|
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
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
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;
|
|
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` : ''}`);
|
|
5348
|
+
}
|
|
4372
5349
|
async function main() {
|
|
4373
5350
|
console.log('๐งช AI-Powered Unit Test Generator with AST Analysis\n');
|
|
4374
|
-
//
|
|
5351
|
+
// Parse command from CLI arguments EARLY (before any prompts)
|
|
4375
5352
|
const args = process.argv.slice(2);
|
|
4376
|
-
const
|
|
5353
|
+
const command = args[0]; // First argument is the command: 'auto', 'test', 'review', or undefined
|
|
5354
|
+
// Validate command if provided
|
|
5355
|
+
if (command && !['auto', 'test', 'review', 'doc'].includes(command)) {
|
|
5356
|
+
console.error('โ Invalid command. Usage:\n');
|
|
5357
|
+
console.error(' testgen auto - Review changes and generate tests');
|
|
5358
|
+
console.error(' testgen test - Generate tests only');
|
|
5359
|
+
console.error(' testgen review - Review changes only');
|
|
5360
|
+
console.error(' testgen doc - Generate API documentation');
|
|
5361
|
+
console.error(' testgen - Interactive mode\n');
|
|
5362
|
+
process.exit(1);
|
|
5363
|
+
}
|
|
4377
5364
|
// Load configuration from codeguard.json
|
|
4378
5365
|
try {
|
|
4379
5366
|
CONFIG = (0, config_1.loadConfig)();
|
|
@@ -4396,20 +5383,52 @@ async function main() {
|
|
|
4396
5383
|
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
4397
5384
|
process.exit(1);
|
|
4398
5385
|
}
|
|
4399
|
-
// If
|
|
4400
|
-
if (
|
|
4401
|
-
|
|
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') {
|
|
5388
|
+
const modeLabel = command === 'auto' ? 'Auto Mode (Review + Test)' :
|
|
5389
|
+
command === 'test' ? 'Test Generation Mode' :
|
|
5390
|
+
command === 'review' ? 'Code Review Mode' :
|
|
5391
|
+
CONFIG.repoDoc ? 'Documentation Generation Mode (Full Repository)' : 'Documentation Generation Mode (Changes Only)';
|
|
5392
|
+
console.log(`๐ค ${modeLabel}: Detecting changes via git diff\n`);
|
|
4402
5393
|
console.log(`โ
Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
4403
|
-
// Initialize indexer
|
|
5394
|
+
// Initialize and prepare codebase indexer (build or update) without prompts
|
|
4404
5395
|
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
4405
5396
|
const hasExistingIndex = globalIndexer.hasIndex();
|
|
4406
5397
|
if (hasExistingIndex) {
|
|
4407
|
-
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
|
+
}
|
|
4408
5406
|
}
|
|
4409
5407
|
else {
|
|
4410
|
-
|
|
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);
|
|
5411
|
+
}
|
|
5412
|
+
// Execute based on command
|
|
5413
|
+
if (command === 'auto') {
|
|
5414
|
+
// Run review first, then tests
|
|
5415
|
+
await reviewChangedFiles();
|
|
5416
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
5417
|
+
await autoGenerateTests();
|
|
5418
|
+
}
|
|
5419
|
+
else if (command === 'test') {
|
|
5420
|
+
// Only generate tests
|
|
5421
|
+
await autoGenerateTests();
|
|
5422
|
+
}
|
|
5423
|
+
else if (command === 'review') {
|
|
5424
|
+
// Only review changes
|
|
5425
|
+
await reviewChangedFiles();
|
|
5426
|
+
}
|
|
5427
|
+
else if (command === 'doc') {
|
|
5428
|
+
// Only generate documentation
|
|
5429
|
+
console.log('Currently in development. Please try again later.');
|
|
5430
|
+
// await generateDocumentationMode();
|
|
4411
5431
|
}
|
|
4412
|
-
await autoGenerateTests();
|
|
4413
5432
|
return;
|
|
4414
5433
|
}
|
|
4415
5434
|
// Optional: Codebase Indexing
|
|
@@ -4425,16 +5444,18 @@ async function main() {
|
|
|
4425
5444
|
// Check for stale files (modified since last index)
|
|
4426
5445
|
const staleFiles = globalIndexer.getStaleFiles();
|
|
4427
5446
|
if (staleFiles.length > 0) {
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
if (staleFiles.length <= 5) {
|
|
4431
|
-
staleFiles.forEach(f => console.log(` ๐ ${path.basename(f)}`));
|
|
5447
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5448
|
+
globalSpinner = ora(`๐ Updating ${staleFiles.length} modified file(s)...`).start();
|
|
4432
5449
|
}
|
|
4433
5450
|
else {
|
|
4434
|
-
|
|
5451
|
+
globalSpinner.text = `๐ Updating ${staleFiles.length} modified file(s)...`;
|
|
4435
5452
|
}
|
|
4436
5453
|
await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
|
|
4437
|
-
|
|
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
|
|
4438
5459
|
}
|
|
4439
5460
|
else {
|
|
4440
5461
|
console.log('โ
All files up to date!\n');
|
|
@@ -4497,13 +5518,23 @@ async function main() {
|
|
|
4497
5518
|
case '1':
|
|
4498
5519
|
default:
|
|
4499
5520
|
// File-wise mode (original functionality)
|
|
4500
|
-
|
|
5521
|
+
if (!globalSpinner || !globalSpinner.isSpinning) {
|
|
5522
|
+
globalSpinner = ora('๐ Scanning repository...').start();
|
|
5523
|
+
}
|
|
5524
|
+
else {
|
|
5525
|
+
globalSpinner.text = '๐ Scanning repository...';
|
|
5526
|
+
}
|
|
4501
5527
|
const files = await listFilesRecursive('.');
|
|
4502
5528
|
if (files.length === 0) {
|
|
4503
|
-
|
|
5529
|
+
globalSpinner.fail('No source files found!');
|
|
5530
|
+
globalSpinner = null;
|
|
4504
5531
|
return;
|
|
4505
5532
|
}
|
|
4506
|
-
|
|
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');
|
|
4507
5538
|
files.forEach((file, index) => {
|
|
4508
5539
|
console.log(`${index + 1}. ${file}`);
|
|
4509
5540
|
});
|
|
@@ -4514,6 +5545,11 @@ async function main() {
|
|
|
4514
5545
|
return;
|
|
4515
5546
|
}
|
|
4516
5547
|
await generateTests(selectedFile);
|
|
5548
|
+
// Clear spinner before final message
|
|
5549
|
+
if (globalSpinner && globalSpinner.isSpinning) {
|
|
5550
|
+
globalSpinner.stop();
|
|
5551
|
+
globalSpinner = null;
|
|
5552
|
+
}
|
|
4517
5553
|
console.log('\nโจ Done!');
|
|
4518
5554
|
break;
|
|
4519
5555
|
}
|