codeguard-testgen 1.0.10 → 1.0.11

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