codeguard-testgen 1.0.10 → 1.0.12

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,78 +3449,295 @@ 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 functionAST = getFunctionAST(sourceFile, functionName);
3453
+ const testEnv = CONFIG.testEnv;
3454
+ let messages;
3455
+ if (testEnv == "vitest") {
3456
+ messages = [
3457
+ {
3458
+ role: "user",
3459
+ content: `You are a senior developer generating production-ready Vitest unit tests for TypeScript.
3008
3460
 
3009
- ## CONTEXT
3010
- Test file: ${testFilePath} | Exists: ${testFileExists}
3461
+ ## TARGET
3462
+ Function: ${functionName} | Source: ${sourceFile}
3463
+ Test File: ${testFilePath} (Exists: ${testFileExists})
3011
3464
 
3012
- ⚠️ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
3465
+ ## WORKFLOW
3013
3466
 
3014
- ---
3467
+ ### 1. ANALYSIS (Execute in order)
3468
+ 1. 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)
3065
- - Error conditions
3066
- - ⏱️ Async behavior
3067
- - 🔍 API null/undefined handling
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)
3068
3530
 
3069
- /**
3070
- * Phase 3: Anti-Pollution Pattern (MANDATORY)
3071
- */
3531
+ ### 4. MOCK STRATEGY
3072
3532
 
3073
- \\\`\\\`\\\`typescript
3074
- // ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
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
+ 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
+ **PRE COMPUTED FUNCTION AST **
3663
+ ${JSON.stringify(functionAST)}
3664
+ `
3665
+ },
3666
+ ];
3667
+ }
3668
+ else {
3669
+ messages = [
3670
+ {
3671
+ role: "user",
3672
+ content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
3673
+ [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.
3674
+ [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.
3675
+
3676
+ ## CONTEXT
3677
+ Test file: ${testFilePath} | Exists: ${testFileExists}
3678
+
3679
+ ⚠️ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
3680
+
3681
+ ---
3682
+
3683
+ ## EXECUTION PLAN
3684
+
3685
+ **Phase 1: Deep Analysis**
3686
+ \\\`\\\`\\\`
3687
+ 1. For each dependency:
3688
+ - Same file: get_function_ast(${sourceFile},{functionName}) -> implementation + dependencies
3689
+ - 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
3690
+ 2. get_imports_ast → all dependencies
3691
+ 3. 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.
3692
+ 4. get_file_preamble → imports and mocks already declared in the file
3693
+ 5. search_codebase → look for relevant context in codebase.
3694
+ \\\`\\\`\\\`
3695
+
3696
+ **Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
3697
+ *Before writing tests, map the logic requirements for external calls.*
3698
+ 1. Identify every external call (e.g., \`analyticsHelper.postEvent\`).
3699
+ 2. Trace backwards: What \`if\`, \`switch\`, or \`try/catch\` block guards this call?
3700
+ 3. Identify the dependency that controls that guard.
3701
+ 4. Plan the Mock Return: Determine exactly what value the dependency must return to enter that block.
3702
+
3703
+ **Phase 2: Test Generation**
3704
+
3705
+ Mock Pattern (CRITICAL - Top of file):
3706
+ \\\`\\\`\\\`typescript
3707
+ // ===== MOCKS (BEFORE IMPORTS) =====
3708
+ jest.mock('config', () => ({
3709
+ get: (key: string) => ({
3710
+ AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
3711
+ USER_DEL_SECRET: 'secret'
3712
+ })
3713
+ }), { virtual: true });
3714
+
3715
+ // virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
3716
+
3717
+ jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
3718
+
3719
+ // ===== IMPORTS =====
3720
+ import { functionName } from '../controller';
3721
+ import { dependencyMethod } from '../helpers/dependency';
3722
+
3723
+ // ===== TYPED MOCKS =====
3724
+ const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
3725
+
3726
+ \\\`\\\`\\\`
3727
+
3728
+ Requirements (5+ tests minimum):
3729
+ - ✅ Happy path
3730
+ - 🔸 Edge cases (null, undefined, empty)
3731
+ - ❌ Error conditions
3732
+ - ⏱️ Async behavior
3733
+ - 🔍 API null/undefined handling
3734
+
3735
+ /**
3736
+ * Phase 3: Anti-Pollution Pattern (MUST FOLLOW EXACTLY THIS PATTERN, NO VARIATIONS)
3737
+ */
3738
+
3739
+ \\\`\\\`\\\`typescript
3740
+ // ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
3075
3741
  afterEach(() => {
3076
3742
  jest.restoreAllMocks(); // Automatically restores ALL spies
3077
3743
  });
@@ -3188,14 +3854,8 @@ All functions from the same source file MUST share the same test file.
3188
3854
  | "Test suite failed to run" | get_file_preamble + fix imports/mocks |
3189
3855
  | "Cannot find module" | calculate_relative_path |
3190
3856
 
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
3857
 
3198
- 3️⃣ Repeat until: ✅ Pass OR 📋 Legitimate failure (report_legitimate_failure)
3858
+ 3️⃣ Repeat until: ✅ All test cases pass
3199
3859
 
3200
3860
  ---
3201
3861
 
@@ -3207,11 +3867,18 @@ All functions from the same source file MUST share the same test file.
3207
3867
  - Ensure test independence (no pollution)
3208
3868
  - Fix test bugs, report source bugs
3209
3869
  - [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.
3870
+ - 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.
3871
+ - Mocking of winston logger or any other external dependeny is critical and mandatory.
3872
+ - Use search_codebase tool to look for relevant context in codebase quickly.
3211
3873
 
3212
- **START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file.`
3213
- }
3214
- ];
3874
+ **START:** 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.
3875
+
3876
+ **PRE COMPUTED FUNCTION AST **
3877
+ ${JSON.stringify(functionAST)}
3878
+ `,
3879
+ },
3880
+ ];
3881
+ }
3215
3882
  let iterations = 0;
3216
3883
  const maxIterations = 100;
3217
3884
  let testFileWritten = false;
@@ -3220,16 +3887,25 @@ All functions from the same source file MUST share the same test file.
3220
3887
  let lastTestError = '';
3221
3888
  let sameErrorCount = 0;
3222
3889
  while (iterations < maxIterations) {
3223
- console.log('USING CLAUDE PROMPT original 16');
3224
3890
  iterations++;
3225
3891
  if (iterations === 1) {
3226
- console.log(`\n🤖 AI is analyzing selected functions...`);
3892
+ if (!globalSpinner || !globalSpinner.isSpinning) {
3893
+ globalSpinner = ora('🤖 AI is analyzing selected functions...').start();
3894
+ }
3895
+ else {
3896
+ globalSpinner.text = '🤖 AI is analyzing selected functions...';
3897
+ }
3227
3898
  }
3228
3899
  else if (iterations % 5 === 0) {
3229
- console.log(`\n🤖 AI is still working (step ${iterations})...`);
3900
+ if (globalSpinner) {
3901
+ globalSpinner.text = `🤖 AI is still working (step ${iterations})...`;
3902
+ }
3230
3903
  }
3904
+ // console.log('messages', messages);
3905
+ // console.log('TOOLS_FOR_TEST_GENERATION', TOOLS_FOR_TEST_GENERATION);
3231
3906
  const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3232
3907
  // console.log('response from AI', JSON.stringify(response, null, 2));
3908
+ // console.log('TEst file path', testFilePath);
3233
3909
  if (response.content) {
3234
3910
  const content = response.content;
3235
3911
  // Only show AI message if it's making excuses (for debugging), otherwise skip
@@ -3243,7 +3919,7 @@ All functions from the same source file MUST share the same test file.
3243
3919
  /beyond my capabilities/i,
3244
3920
  /can't execute/i
3245
3921
  ];
3246
- const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
3922
+ const isMakingExcuses = excusePatterns.some(pattern => typeof content === 'string' && pattern.test(content));
3247
3923
  if (isMakingExcuses) {
3248
3924
  console.log('\n⚠️ AI is making excuses! Forcing it to use tools...');
3249
3925
  messages.push({
@@ -3293,7 +3969,7 @@ This works for both NEW and EXISTING test files!`
3293
3969
  4. Then run_tests to verify
3294
3970
 
3295
3971
  📌 ALTERNATIVE: Use insert_at_position for adding imports/mocks
3296
- - insert_at_position({ position: 'after_imports', content: "jest.mock('../module');" })
3972
+ - insert_at_position({ position: 'after_imports', content: "vi.mock('../module');" })
3297
3973
 
3298
3974
  ⚠️ SECONDARY: Use upsert_function_tests for function-level rewrites
3299
3975
 
@@ -3325,16 +4001,13 @@ Start NOW with search_replace_block or insert_at_position!`
3325
4001
  const currentError = errorOutput.substring(0, 300);
3326
4002
  if (currentError === lastTestError) {
3327
4003
  sameErrorCount++;
3328
- console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
4004
+ // console.log(`\n⚠️ Same error repeated ${sameErrorCount} times`);
3329
4005
  if (sameErrorCount >= 3) {
3330
- console.log('\n🚨 Same error repeated 3+ times! ');
4006
+ // console.log('\n🚨 Same error repeated 3+ times! ');
3331
4007
  messages.push({
3332
4008
  role: 'user',
3333
4009
  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.`
4010
+ Make focused attempt to fix the tests using the tools available.`
3338
4011
  });
3339
4012
  }
3340
4013
  }
@@ -3347,7 +4020,7 @@ If this is still fixable: Make focused attempt to fix it.`
3347
4020
  if (toolCall.name === 'upsert_function_tests') {
3348
4021
  if (result.success) {
3349
4022
  testFileWritten = true;
3350
- console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
4023
+ // console.log(`\n📝 Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
3351
4024
  messages.push({
3352
4025
  role: 'user',
3353
4026
  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 +4124,32 @@ async function smartValidateTestSuite(sourceFile, testFilePath, functionNames) {
3451
4124
  console.log(`🔍 VALIDATION: Running full test suite (${functionNames.length} function(s))`);
3452
4125
  console.log(`${'='.repeat(80)}\n`);
3453
4126
  // Run tests for entire file (no function filter)
3454
- const fullSuiteResult = runTests(testFilePath);
4127
+ let fullSuiteResult;
4128
+ if (CONFIG.testEnv == 'vitest') {
4129
+ fullSuiteResult = runTestsVitest(testFilePath);
4130
+ }
4131
+ else {
4132
+ fullSuiteResult = runTestsJest(testFilePath);
4133
+ }
3455
4134
  if (fullSuiteResult.passed) {
3456
4135
  console.log(`\n✅ Full test suite passed! All ${functionNames.length} function(s) working together correctly.`);
3457
4136
  return;
3458
4137
  }
3459
4138
  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);
4139
+ // Parse failing test names from Vitest output
4140
+ let failingTests;
4141
+ if (CONFIG.testEnv == 'vitest') {
4142
+ failingTests = parseFailingTestNamesVitest(fullSuiteResult.output);
4143
+ }
4144
+ else {
4145
+ failingTests = parseFailingTestNamesJest(fullSuiteResult.output);
4146
+ }
4147
+ console.log(`\n📊 Debug: Found ${failingTests.length} failing test(s) from output`);
3462
4148
  if (failingTests.length === 0) {
3463
- console.log('⚠️ Could not parse specific failing test names. Skipping detailed analysis.');
4149
+ console.log('⚠️ Could not parse specific failing test names from output.');
4150
+ console.log(' Attempting general fix based on full error output...\n');
4151
+ // Fallback: Still attempt to fix using the full output even without parsed test names
4152
+ await fixFailingTests(sourceFile, testFilePath, functionNames, [], fullSuiteResult.output);
3464
4153
  return;
3465
4154
  }
3466
4155
  console.log(`Found ${failingTests.length} failing test(s): ${failingTests.join(', ')}\n`);
@@ -3475,7 +4164,7 @@ async function fixFailingTests(sourceFile, testFilePath, functionNames, failingT
3475
4164
  const messages = [
3476
4165
  {
3477
4166
  role: 'user',
3478
- content: `You are fixing FAILING TESTS in the test suite.
4167
+ content: `You are fixing FAILING TESTS in the Vitest test suite.
3479
4168
 
3480
4169
  Source file: ${sourceFile}
3481
4170
  Test file: ${testFilePath}
@@ -3490,29 +4179,28 @@ ${fullSuiteOutput}
3490
4179
  YOUR TASK - Fix all failing tests:
3491
4180
 
3492
4181
  COMMON ISSUES TO FIX:
3493
- - Missing jest.resetAllMocks() in beforeEach (should be first line)
3494
- - Missing jest.restoreAllMocks() in global afterEach
3495
4182
  - 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
4183
+ - Missing vitest imports (describe, it, expect, beforeEach, vi)
4184
+ - Incorrect mock typing (use MockedFunction from vitest)
4185
+ - beforeEach not setting up mocks properly
3500
4186
  - Missing or incorrect imports
3501
4187
  - Mock implementation issues
3502
4188
  - Incorrect test assertions
3503
4189
  - Test logic errors
3504
4190
 
4191
+ NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled.
4192
+
3505
4193
  TOOLS TO USE:
3506
4194
  1. get_file_preamble - See current setup
3507
4195
  2. search_replace_block - Fix specific sections (preferred)
3508
- 3. insert_at_position - Add missing global afterEach
4196
+ 3. insert_at_position - Add missing imports/mocks
3509
4197
  4. run_tests - Verify fixes
3510
4198
 
3511
4199
  START by calling get_file_preamble to see the current test structure.`
3512
4200
  }
3513
4201
  ];
3514
4202
  let iterations = 0;
3515
- const maxIterations = 100; // Limit iterations for test fixes
4203
+ const maxIterations = 500; // Limit iterations for test fixes
3516
4204
  while (iterations < maxIterations) {
3517
4205
  iterations++;
3518
4206
  console.log(`\n🔧 Test fix attempt ${iterations}/${maxIterations}...`);
@@ -3522,7 +4210,13 @@ START by calling get_file_preamble to see the current test structure.`
3522
4210
  }
3523
4211
  if (!response.toolCalls || response.toolCalls.length === 0) {
3524
4212
  // AI stopped - check if tests pass now
3525
- const finalTest = runTests(testFilePath);
4213
+ let finalTest;
4214
+ if (CONFIG.testEnv == 'vitest') {
4215
+ finalTest = runTestsVitest(testFilePath);
4216
+ }
4217
+ else {
4218
+ finalTest = runTestsJest(testFilePath);
4219
+ }
3526
4220
  if (finalTest.passed) {
3527
4221
  console.log('\n✅ Tests fixed! Full test suite now passes.');
3528
4222
  return;
@@ -3609,7 +4303,13 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
3609
4303
  console.log(`🔍 FINAL VALIDATION: Running complete test suite`);
3610
4304
  console.log(`${'='.repeat(80)}\n`);
3611
4305
  // Run tests for entire file (no function filter)
3612
- const testResult = runTests(testFilePath);
4306
+ let testResult;
4307
+ if (CONFIG.testEnv == 'vitest') {
4308
+ testResult = runTestsVitest(testFilePath);
4309
+ }
4310
+ else {
4311
+ testResult = runTestsJest(testFilePath);
4312
+ }
3613
4313
  if (testResult.passed) {
3614
4314
  console.log(`\n✅ Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
3615
4315
  return;
@@ -3619,7 +4319,7 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
3619
4319
  const messages = [
3620
4320
  {
3621
4321
  role: 'user',
3622
- content: `You are a senior software engineer fixing file-level integration issues in a Jest test file.
4322
+ content: `You are a senior software engineer fixing file-level integration issues in a Vitest test file.
3623
4323
 
3624
4324
  Source file: ${sourceFile}
3625
4325
  Test file: ${testFilePath}
@@ -3632,7 +4332,7 @@ CONTEXT:
3632
4332
  * Mock pollution between test suites
3633
4333
  * Shared state not being cleaned up
3634
4334
  * Import/mock ordering issues
3635
- * beforeEach/afterEach missing or incorrect
4335
+ * beforeEach setup issues
3636
4336
  * Mock implementations interfering with each other
3637
4337
 
3638
4338
  TEST OUTPUT:
@@ -3643,35 +4343,27 @@ YOUR TASK:
3643
4343
  2. Identify file-level issues (NOT individual function logic issues)
3644
4344
  3. Fix using search_replace_block or insert_at_position tools
3645
4345
  4. Run tests again with run_tests tool
3646
- 5. Repeat until tests pass OR you determine failures are legitimate source code bugs
4346
+ 5. Repeat until all test cases pass
3647
4347
 
3648
4348
  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
4349
+ - ❌ Missing vitest imports (describe, it, expect, beforeEach, vi)
4350
+ - ❌ Module imports inside describe blocks (use await import() in beforeEach)
4351
+ - ❌ beforeEach hooks not setting up mocks properly
3654
4352
  - ❌ Test suites depending on execution order
3655
4353
 
4354
+ NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled for auto-cleanup.
4355
+
3656
4356
  FIXABLE ISSUES (you should fix):
3657
4357
  - Mock pollution between test suites
3658
- - Missing cleanup in beforeEach/afterEach
3659
4358
  - Incorrect mock setup at file level
3660
4359
  - 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
4360
+ - Missing vitest imports
3668
4361
 
3669
4362
  CRITICAL RULES:
3670
4363
  1. DO NOT change individual test logic (they passed individually!)
3671
4364
  2. Focus ONLY on file-level integration issues
3672
4365
  3. Use search_replace_block to fix specific sections
3673
4366
  4. Preserve all existing test cases
3674
- 5. If failures are due to source code bugs, call report_legitimate_failure and STOP
3675
4367
 
3676
4368
  START by calling get_file_preamble to understand current file structure.`
3677
4369
  }
@@ -3689,7 +4381,13 @@ START by calling get_file_preamble to understand current file structure.`
3689
4381
  }
3690
4382
  if (!response.toolCalls || response.toolCalls.length === 0) {
3691
4383
  // AI stopped without fixing - check if tests pass now
3692
- const finalTest = runTests(testFilePath);
4384
+ let finalTest;
4385
+ if (CONFIG.testEnv == 'vitest') {
4386
+ finalTest = runTestsVitest(testFilePath);
4387
+ }
4388
+ else {
4389
+ finalTest = runTestsJest(testFilePath);
4390
+ }
3693
4391
  if (finalTest.passed) {
3694
4392
  console.log('\n✅ Complete test suite now passes!');
3695
4393
  return;
@@ -3697,7 +4395,7 @@ START by calling get_file_preamble to understand current file structure.`
3697
4395
  console.log('\n⚠️ AI stopped but tests still failing. Prompting to continue...');
3698
4396
  messages.push({
3699
4397
  role: 'user',
3700
- content: 'Tests are still failing! Use tools to fix or call report_legitimate_failure if these are source code bugs.'
4398
+ content: 'Tests are still failing! Use tools to fix.'
3701
4399
  });
3702
4400
  continue;
3703
4401
  }
@@ -3787,7 +4485,7 @@ START by calling get_file_preamble to understand current file structure.`
3787
4485
  * Generate tests for multiple functions, one at a time
3788
4486
  */
3789
4487
  async function generateTestsForFunctions(sourceFile, functionNames) {
3790
- console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
4488
+ // console.log(`\n📝 Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
3791
4489
  const testFilePath = getTestFilePath(sourceFile);
3792
4490
  let testFileExists = fsSync.existsSync(testFilePath);
3793
4491
  // Read validation interval from config
@@ -3795,12 +4493,22 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
3795
4493
  // Process each function one at a time
3796
4494
  for (let i = 0; i < functionNames.length; i++) {
3797
4495
  const functionName = functionNames[i];
4496
+ // Clear spinner before showing section header
4497
+ if (globalSpinner && globalSpinner.isSpinning) {
4498
+ globalSpinner.stop();
4499
+ globalSpinner = null;
4500
+ }
3798
4501
  console.log(`\n${'='.repeat(80)}`);
3799
4502
  console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
3800
4503
  console.log(`${'='.repeat(80)}\n`);
3801
4504
  const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
3802
4505
  // After first function completes, test file will exist for subsequent functions
3803
4506
  testFileExists = true;
4507
+ // Clear spinner before showing completion
4508
+ if (globalSpinner && globalSpinner.isSpinning) {
4509
+ globalSpinner.stop();
4510
+ globalSpinner = null;
4511
+ }
3804
4512
  if (passed) {
3805
4513
  console.log(`\n✅ Function '${functionName}' tests completed successfully!`);
3806
4514
  }
@@ -3828,13 +4536,23 @@ async function generateTestsForFunctions(sourceFile, functionNames) {
3828
4536
  async function generateTestsForFunction() {
3829
4537
  console.log('\n🎯 Function-wise Test Generation\n');
3830
4538
  // List all files
3831
- console.log('📂 Scanning repository...\n');
4539
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4540
+ globalSpinner = ora('📂 Scanning repository...').start();
4541
+ }
4542
+ else {
4543
+ globalSpinner.text = '📂 Scanning repository...';
4544
+ }
3832
4545
  const files = await listFilesRecursive('.');
3833
4546
  if (files.length === 0) {
3834
- console.log('No source files found!');
4547
+ globalSpinner.fail('No source files found!');
4548
+ globalSpinner = null;
3835
4549
  return;
3836
4550
  }
3837
- console.log('Select a file:\n');
4551
+ globalSpinner.text = `Found ${files.length} source file(s)`;
4552
+ await new Promise(resolve => setTimeout(resolve, 200));
4553
+ globalSpinner.stop();
4554
+ globalSpinner = null;
4555
+ console.log('\nSelect a file:\n');
3838
4556
  files.forEach((file, index) => {
3839
4557
  console.log(`${index + 1}. ${file}`);
3840
4558
  });
@@ -3870,6 +4588,11 @@ async function generateTestsForFunction() {
3870
4588
  }
3871
4589
  console.log(`\n✅ Selected functions: ${selectedFunctions.join(', ')}\n`);
3872
4590
  await generateTestsForFunctions(selectedFile, selectedFunctions);
4591
+ // Clear spinner before final message
4592
+ if (globalSpinner && globalSpinner.isSpinning) {
4593
+ globalSpinner.stop();
4594
+ globalSpinner = null;
4595
+ }
3873
4596
  console.log('\n✨ Done!');
3874
4597
  }
3875
4598
  /**
@@ -3997,7 +4720,7 @@ Return ONLY the JSON array, nothing else.`;
3997
4720
  return [];
3998
4721
  }
3999
4722
  // Parse AI response to extract function names
4000
- const content = response.content.trim();
4723
+ const content = typeof response.content === 'string' ? response.content.trim() : JSON.stringify(response.content || '');
4001
4724
  // console.log(` 🤖 AI response: ${content}`);
4002
4725
  // Try to extract JSON array from response
4003
4726
  const jsonMatch = content.match(/\[.*\]/s);
@@ -4029,16 +4752,25 @@ Return ONLY the JSON array, nothing else.`;
4029
4752
  * Auto-generate tests for changed functions detected via git diff
4030
4753
  */
4031
4754
  async function autoGenerateTests() {
4032
- console.log('🔍 Scanning git changes...\n');
4755
+ if (!globalSpinner) {
4756
+ globalSpinner = ora('🔍 Scanning git changes...').start();
4757
+ }
4758
+ else {
4759
+ globalSpinner.text = '🔍 Scanning git changes...';
4760
+ globalSpinner.start();
4761
+ }
4033
4762
  try {
4034
4763
  // Get all changes from git diff
4035
4764
  const { fullDiff, files } = await getGitDiff();
4036
4765
  if (files.length === 0) {
4766
+ globalSpinner.stop();
4767
+ globalSpinner = null;
4037
4768
  console.log('✅ No changes detected in source files.');
4038
4769
  console.log(' (Only staged and unstaged changes are checked)');
4039
4770
  return;
4040
4771
  }
4041
- console.log(`📝 Found changes in ${files.length} file(s)\n`);
4772
+ globalSpinner.text = `Found changes in ${files.length} file(s)`;
4773
+ await new Promise(resolve => setTimeout(resolve, 200));
4042
4774
  let totalFunctions = 0;
4043
4775
  let processedFiles = 0;
4044
4776
  let errorFiles = 0;
@@ -4047,25 +4779,41 @@ async function autoGenerateTests() {
4047
4779
  const { filePath, diff } = fileInfo;
4048
4780
  // Check if file exists
4049
4781
  if (!fsSync.existsSync(filePath)) {
4050
- console.log(`⏭️ Skipping ${filePath} (file not found)`);
4782
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4783
+ globalSpinner = ora(`⏭️ Skipping ${filePath} (file not found)`).start();
4784
+ }
4785
+ else {
4786
+ globalSpinner.text = `⏭️ Skipping ${filePath} (file not found)`;
4787
+ }
4788
+ await new Promise(resolve => setTimeout(resolve, 100));
4051
4789
  continue;
4052
4790
  }
4053
- console.log(`\n🔄 Processing: ${filePath}`);
4054
- console.log(` 🤖 Analyzing diff with AI...`);
4791
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4792
+ globalSpinner = ora(`🔄 Processing: ${path.basename(filePath)}`).start();
4793
+ }
4794
+ else {
4795
+ globalSpinner.text = `🔄 Processing: ${path.basename(filePath)}`;
4796
+ }
4055
4797
  // Use AI to extract changed function names from diff
4798
+ globalSpinner.text = `🤖 Analyzing diff with AI: ${path.basename(filePath)}`;
4056
4799
  const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4057
4800
  // console.log('Changed functions are', changedFunctions);
4058
4801
  if (changedFunctions.length === 0) {
4059
- console.log(` ⏭️ No exported functions changed`);
4802
+ globalSpinner.text = `⏭️ No exported functions changed in ${path.basename(filePath)}`;
4803
+ await new Promise(resolve => setTimeout(resolve, 150));
4060
4804
  continue;
4061
4805
  }
4062
- console.log(` 📦 Changed functions: ${changedFunctions.join(', ')}`);
4806
+ globalSpinner.text = `Found ${changedFunctions.length} function(s): ${changedFunctions.join(', ')}`;
4807
+ await new Promise(resolve => setTimeout(resolve, 200));
4063
4808
  try {
4064
4809
  // Use existing generateTestsForFunctions
4065
4810
  await generateTestsForFunctions(filePath, changedFunctions);
4066
4811
  processedFiles++;
4067
4812
  totalFunctions += changedFunctions.length;
4068
- console.log(` ✅ Tests generated successfully`);
4813
+ if (globalSpinner) {
4814
+ globalSpinner.text = `Tests generated for ${path.basename(filePath)}`;
4815
+ await new Promise(resolve => setTimeout(resolve, 200));
4816
+ }
4069
4817
  }
4070
4818
  catch (error) {
4071
4819
  errorFiles++;
@@ -4076,6 +4824,11 @@ async function autoGenerateTests() {
4076
4824
  // Summary
4077
4825
  console.log('\n' + '='.repeat(60));
4078
4826
  console.log('📊 Auto-Generation Summary');
4827
+ // Clear spinner before final summary
4828
+ if (globalSpinner && globalSpinner.isSpinning) {
4829
+ globalSpinner.stop();
4830
+ globalSpinner = null;
4831
+ }
4079
4832
  console.log('='.repeat(60));
4080
4833
  console.log(`✅ Successfully processed: ${processedFiles} file(s)`);
4081
4834
  console.log(`📝 Functions tested: ${totalFunctions}`);
@@ -4098,56 +4851,81 @@ async function autoGenerateTests() {
4098
4851
  * Review code changes for quality, bugs, performance, and security issues
4099
4852
  */
4100
4853
  async function reviewChangedFiles() {
4101
- console.log('🔍 Scanning git changes for review...\n');
4854
+ if (!globalSpinner) {
4855
+ globalSpinner = ora('🔍 Scanning git changes for review...').start();
4856
+ }
4857
+ else {
4858
+ globalSpinner.text = '🔍 Scanning git changes for review...';
4859
+ globalSpinner.start();
4860
+ }
4102
4861
  try {
4103
4862
  // Get all changes from git diff
4104
4863
  const { fullDiff, files } = await getGitDiff();
4105
4864
  if (files.length === 0) {
4865
+ globalSpinner.stop();
4866
+ globalSpinner = null;
4106
4867
  console.log('✅ No changes detected in source files.');
4107
4868
  console.log(' (Only staged and unstaged changes are checked)');
4108
4869
  return;
4109
4870
  }
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
4871
+ globalSpinner.text = `Found changes in ${files.length} file(s) to review`;
4872
+ await new Promise(resolve => setTimeout(resolve, 200));
4873
+ // Collect all changed files and their functions
4874
+ const filesToReview = [];
4114
4875
  for (const fileInfo of files) {
4115
4876
  const { filePath, diff } = fileInfo;
4116
4877
  // Check if file exists
4117
4878
  if (!fsSync.existsSync(filePath)) {
4118
- console.log(`⏭️ Skipping ${filePath} (file not found)`);
4879
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4880
+ globalSpinner = ora(`⏭️ Skipping ${filePath} (file not found)`).start();
4881
+ }
4882
+ else {
4883
+ globalSpinner.text = `⏭️ Skipping ${filePath} (file not found)`;
4884
+ }
4885
+ await new Promise(resolve => setTimeout(resolve, 100));
4119
4886
  continue;
4120
4887
  }
4121
- console.log(`\n🔄 Reviewing: ${filePath}`);
4122
- console.log(` 🤖 Analyzing changes with AI...`);
4888
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4889
+ globalSpinner = ora(`🔄 Analyzing: ${filePath}`).start();
4890
+ }
4891
+ else {
4892
+ globalSpinner.text = `🔄 Analyzing: ${filePath}`;
4893
+ }
4123
4894
  // Use AI to extract changed function names from diff
4124
4895
  const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4125
4896
  if (changedFunctions.length === 0) {
4126
- console.log(` ⏭️ No exported functions changed`);
4897
+ globalSpinner.text = `⏭️ No exported functions changed in ${path.basename(filePath)}`;
4898
+ await new Promise(resolve => setTimeout(resolve, 150));
4127
4899
  continue;
4128
4900
  }
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
4901
+ globalSpinner.text = `Found ${changedFunctions.length} changed function(s) in ${path.basename(filePath)}`;
4902
+ await new Promise(resolve => setTimeout(resolve, 200));
4903
+ filesToReview.push({ filePath, diff, functions: changedFunctions });
4904
+ }
4905
+ if (filesToReview.length === 0) {
4906
+ console.log('\nNo functions to review.');
4907
+ return;
4908
+ }
4909
+ // Generate unified review for all changes
4910
+ try {
4911
+ await generateUnifiedCodeReview(filesToReview);
4912
+ // Ensure spinner is cleared before final message
4913
+ if (globalSpinner && globalSpinner.isSpinning) {
4914
+ globalSpinner.stop();
4915
+ globalSpinner = null;
4140
4916
  }
4917
+ console.log('\n✅ Code review completed!');
4918
+ }
4919
+ catch (error) {
4920
+ console.error(`\n❌ Error during review: ${error.message}`);
4141
4921
  }
4142
4922
  // Summary
4143
4923
  console.log('\n' + '='.repeat(60));
4144
4924
  console.log('📊 Code Review Summary');
4145
4925
  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`);
4926
+ console.log(`✅ Files reviewed: ${filesToReview.length}`);
4927
+ console.log(`📝 Total functions: ${filesToReview.reduce((sum, f) => sum + f.functions.length, 0)}`);
4928
+ console.log(`📁 Review saved to: reviews/code_review.md`);
4151
4929
  console.log('='.repeat(60));
4152
4930
  console.log('\n✨ Done!');
4153
4931
  }
@@ -4161,305 +4939,418 @@ async function reviewChangedFiles() {
4161
4939
  }
4162
4940
  }
4163
4941
  /**
4164
- * Generate comprehensive code review for a specific file
4942
+ * Build AI prompt for a specific review step based on its ruleset
4165
4943
  */
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.
4944
+ async function buildStepPrompt(step, filesToReview, stepOutputPath) {
4945
+ const filesContext = filesToReview.map(f => `- ${f.filePath}: ${f.functions.join(', ')}`).join('\n');
4946
+ const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
4947
+ // Read ruleset from markdown file
4948
+ const rulesetPath = path.join(process.cwd(), 'codeguard-ruleset', step.ruleset);
4949
+ let rulesetContent;
4950
+ try {
4951
+ rulesetContent = await fs.readFile(rulesetPath, 'utf-8');
4952
+ if (globalSpinner && globalSpinner.isSpinning) {
4953
+ globalSpinner.text = `📖 Loaded ruleset: ${step.ruleset}`;
4954
+ await new Promise(resolve => setTimeout(resolve, 100));
4955
+ }
4956
+ }
4957
+ catch (error) {
4958
+ if (globalSpinner && globalSpinner.isSpinning) {
4959
+ globalSpinner.stop();
4960
+ globalSpinner = null;
4961
+ }
4962
+ console.warn(`⚠️ Could not read ruleset file: ${rulesetPath}`);
4963
+ console.warn(` Using default ruleset message. Error: ${error.message}`);
4964
+ rulesetContent = `Please review the code for ${step.name} following industry best practices.`;
4965
+ }
4966
+ return `You are a senior software engineer conducting a focused code review for: **${step.name}**
4173
4967
 
4174
4968
  ## CONTEXT
4175
- File: ${filePath}
4176
- Changed Functions: ${changedFunctions.join(', ')}
4969
+ Review Focus: ${step.name}
4970
+ Category: ${step.category}
4971
+ Total files changed: ${filesToReview.length}
4972
+ Total functions changed: ${totalFunctions}
4973
+
4974
+ Changed files and functions:
4975
+ ${filesContext}
4976
+
4977
+ ## GIT DIFFS
4177
4978
 
4178
- ## GIT DIFF
4979
+ ${filesToReview.map(f => `### ${f.filePath}
4179
4980
  \`\`\`diff
4180
- ${diff}
4981
+ ${f.diff}
4181
4982
  \`\`\`
4983
+ `).join('\n')}
4182
4984
 
4183
4985
  ## YOUR TASK
4184
- Conduct a comprehensive code review using the available tools to understand the full context. You MUST use tools to analyze the code thoroughly before providing your review.
4185
-
4186
- ## EXECUTION PLAN
4187
-
4188
- **Phase 1: Deep Code Analysis (MANDATORY)**
4189
- 1. analyze_file_ast(${filePath}) → Get file structure and all functions
4190
- 2. For each changed function (${changedFunctions.join(', ')}):
4191
- - get_function_ast(${filePath}, {functionName}) → Get implementation details
4192
- 3. get_imports_ast(${filePath}) → Understand dependencies
4193
- 4. get_type_definitions(${filePath}) → Review type safety
4194
- 5. For key dependencies, use find_file() and get_function_ast() to understand their behavior
4195
-
4196
- **Phase 2: Review Analysis**
4197
-
4198
- Analyze each changed function for:
4199
-
4200
- ### 1. CODE QUALITY
4201
- - Naming conventions (functions, variables, types)
4202
- - Code complexity and readability
4203
- - Code duplication
4204
- - Best practices adherence
4205
- - TypeScript/JavaScript patterns
4206
- - Error handling completeness
4207
- - Logging and debugging support
4208
-
4209
- ### 2. POTENTIAL BUGS
4210
- - Logic errors
4211
- - Edge cases not handled (null, undefined, empty arrays, etc.)
4212
- - Type mismatches
4213
- - Async/await issues
4214
- - Race conditions
4215
- - Off-by-one errors
4216
- - Incorrect conditionals
4217
-
4218
- ### 3. PERFORMANCE ISSUES
4219
- - Inefficient algorithms (O(n²) when O(n) possible)
4220
- - Unnecessary iterations or computations
4221
- - Memory leaks (closures, event listeners)
4222
- - Missing caching opportunities
4223
- - Inefficient data structures
4224
- - Unnecessary re-renders (React)
4225
-
4226
- ### 4. SECURITY VULNERABILITIES
4227
- - Input validation missing
4228
- - SQL injection risks
4229
- - XSS vulnerabilities
4230
- - Authentication/authorization issues
4231
- - Sensitive data exposure
4232
- - Insecure dependencies
4233
- - Missing rate limiting
4234
-
4235
- ## OUTPUT FORMAT
4986
+ Conduct a focused code review ONLY for ${step.name}. Use the available tools to analyze the code thoroughly.
4987
+ Must complete and write the review within 1 minutes. Your review should be brief and to the point.
4236
4988
 
4237
- **Phase 3: Generate Review**
4989
+ ## ANALYSIS STEPS
4238
4990
 
4239
- Use the write_review tool to create a comprehensive markdown review file.
4991
+ **Phase 1: Code Analysis (MANDATORY)**
4992
+ For each changed file:
4993
+ 1. analyze_file_ast(filePath) → Get file structure and all functions
4994
+ - OR use analyze_file_ast(filePath, functionName) for specific function reviews (token-efficient)
4995
+ 2. For each changed function:
4996
+ - get_function_ast(filePath, functionName) → Get implementation details
4997
+ 3. get_imports_ast(filePath) → Understand dependencies
4998
+ 4. get_type_definitions(filePath) → Review type safety
4999
+ 5. Use search_codebase() if needed to understand usage patterns
5000
+ 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
5001
 
4241
- The review MUST include:
5002
+ **Phase 2: Review Analysis for ${step.name}**
5003
+
5004
+ Review the code against the following ruleset:
5005
+
5006
+ ${rulesetContent}
5007
+
5008
+ Also give attention to any industry specific points that are not covered by the ruleset for ${step.name}.
4242
5009
 
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)
5010
+ **Phase 3: Write Review**
4249
5011
 
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
5012
+ Use the write_review tool to save findings to: ${stepOutputPath}
4258
5013
 
4259
- 3. **Positive Aspects**: What was done well
4260
- 4. **Recommendations**: General suggestions for improvement
5014
+ ## OUTPUT FORMAT
4261
5015
 
4262
- ## MARKDOWN TEMPLATE
5016
+ Your review MUST be in markdown format with the following structure:
4263
5017
 
4264
5018
  \`\`\`markdown
4265
- # Code Review: {filename}
5019
+ # ${step.name}
4266
5020
 
4267
- **Date**: {current_date}
4268
- **Reviewer**: AI Code Review System
4269
- **Changed Functions**: {list of functions}
4270
-
4271
- ---
4272
-
4273
- ## Summary
4274
-
4275
- [Brief overview of changes and overall code quality assessment]
4276
-
4277
- ---
4278
5021
 
4279
5022
  ## Findings
4280
5023
 
4281
5024
  ### 🔴 Critical Issues
4282
5025
 
4283
- #### [Category] - [Issue Title]
4284
- **Severity**: Critical
4285
- **Function**: \`functionName\`
4286
- **Location**: Line ~XX
5026
+ #### [Issue Title]
5027
+ **File**: \`filePath\`
5028
+ **Function**: \`functionName\`
5029
+ **Severity**: Critical
4287
5030
 
4288
5031
  **Issue**:
4289
- [Description of the problem]
5032
+ [Description]
4290
5033
 
4291
5034
  **Current Code**:
4292
5035
  \`\`\`typescript
4293
- // problematic code snippet
5036
+ // problematic code
4294
5037
  \`\`\`
4295
5038
 
4296
- **Recommended Fix**:
5039
+ **Recommended Code**:
4297
5040
  \`\`\`typescript
4298
- // improved code snippet
5041
+ // improved code
4299
5042
  \`\`\`
4300
5043
 
4301
5044
  **Rationale**:
4302
- [Why this is important and how the fix helps]
5045
+ [Why this is important]
4303
5046
 
4304
5047
  ---
4305
5048
 
4306
5049
  ### 🟠 High Priority Issues
4307
-
4308
5050
  [Same format as above]
4309
5051
 
4310
5052
  ---
4311
5053
 
4312
5054
  ### 🟡 Medium Priority Issues
4313
-
4314
5055
  [Same format as above]
4315
5056
 
4316
5057
  ---
4317
5058
 
4318
- ### 🟢 Low Priority Issues
5059
+ ## Positive Aspects
5060
+ [What was done well in this area]
4319
5061
 
4320
- [Same format as above]
5062
+ ## Recommendations
5063
+ [Specific recommendations for ${step.name}]
5064
+ \`\`\`
4321
5065
 
4322
- ---
5066
+ ## CRITICAL REMINDERS
4323
5067
 
4324
- ## Positive Aspects
5068
+ - ALWAYS use tools to analyze code before reviewing
5069
+ - Focus ONLY on ${step.name} - do not review other aspects
5070
+ - Be specific and actionable
5071
+ - Include code examples
5072
+ - Use write_review tool to save to: ${stepOutputPath}
5073
+ - Must complete within 30 seconds
5074
+ - Use avaiable tools extensively to analyze the code.
5075
+ - The review should be based on the ruleset, it should be to the point without too much text.
5076
+ - Before writing the review, make sure to analyze the code thoroughly using the tools available, how similar code is used in the codebase.
5077
+ - Keep the review short and to the point, do not write too much text.
4325
5078
 
4326
- - [What was done well]
4327
- - [Good practices observed]
5079
+ **START**: Begin by calling analyze_file_ast on each changed file.`;
5080
+ }
5081
+ /**
5082
+ * Execute a single review step with AI
5083
+ */
5084
+ async function executeReviewStep(step, filesToReview, stepOutputPath) {
5085
+ // console.log(`\n🔍 Running ${step.name} review step...`);
5086
+ try {
5087
+ const prompt = await buildStepPrompt(step, filesToReview, stepOutputPath);
5088
+ const messages = [
5089
+ {
5090
+ role: 'user',
5091
+ content: prompt
5092
+ }
5093
+ ];
5094
+ let iterations = 0;
5095
+ const maxIterations = 30;
5096
+ let reviewWritten = false;
5097
+ while (iterations < maxIterations) {
5098
+ iterations++;
5099
+ const response = await callAI(messages, TOOLS_FOR_CODE_REVIEW, CONFIG.aiProvider);
5100
+ if (response.content) {
5101
+ const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
5102
+ messages.push({ role: 'assistant', content });
5103
+ // Check if review is complete
5104
+ if (typeof content === 'string' && (content.toLowerCase().includes('review complete') ||
5105
+ content.toLowerCase().includes('review has been written'))) {
5106
+ if (reviewWritten) {
5107
+ break;
5108
+ }
5109
+ }
5110
+ }
5111
+ if (!response.toolCalls || response.toolCalls.length === 0) {
5112
+ if (reviewWritten) {
5113
+ break;
5114
+ }
5115
+ // Prompt AI to continue
5116
+ messages.push({
5117
+ role: 'user',
5118
+ 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.`
5119
+ });
5120
+ continue;
5121
+ }
5122
+ // Execute tool calls
5123
+ const toolResults = [];
5124
+ for (const toolCall of response.toolCalls) {
5125
+ const toolResult = await executeTool(toolCall.name, toolCall.input);
5126
+ toolResults.push({ id: toolCall.id, name: toolCall.name, result: toolResult });
5127
+ // Track if review was written
5128
+ if (toolCall.name === 'write_review' && toolResult.success) {
5129
+ reviewWritten = true;
5130
+ }
5131
+ }
5132
+ // Add tool calls and results to messages (format differs by provider)
5133
+ if (CONFIG.aiProvider === 'openai') {
5134
+ messages.push({
5135
+ role: 'assistant',
5136
+ tool_calls: response.toolCalls.map(tc => ({
5137
+ id: tc.id,
5138
+ type: 'function',
5139
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
5140
+ }))
5141
+ });
5142
+ for (const tr of toolResults) {
5143
+ messages.push({
5144
+ role: 'tool',
5145
+ tool_call_id: tr.id,
5146
+ content: JSON.stringify(tr.result)
5147
+ });
5148
+ }
5149
+ }
5150
+ else if (CONFIG.aiProvider === 'gemini') {
5151
+ for (const toolCall of response.toolCalls) {
5152
+ messages.push({
5153
+ role: 'model',
5154
+ functionCall: { name: toolCall.name, args: toolCall.input }
5155
+ });
5156
+ const result = toolResults.find(tr => tr.name === toolCall.name);
5157
+ messages.push({
5158
+ role: 'user',
5159
+ functionResponse: { name: toolCall.name, response: result?.result }
5160
+ });
5161
+ }
5162
+ }
5163
+ else {
5164
+ // Claude
5165
+ messages.push({
5166
+ role: 'assistant',
5167
+ content: response.toolCalls.map(tc => ({
5168
+ type: 'tool_use',
5169
+ id: tc.id,
5170
+ name: tc.name,
5171
+ input: tc.input
5172
+ }))
5173
+ });
5174
+ messages.push({
5175
+ role: 'user',
5176
+ content: toolResults.map(tr => ({
5177
+ type: 'tool_result',
5178
+ tool_use_id: tr.id,
5179
+ content: JSON.stringify(tr.result)
5180
+ }))
5181
+ });
5182
+ }
5183
+ }
5184
+ if (!reviewWritten) {
5185
+ throw new Error(`Could not complete ${step.name} review within iteration limit`);
5186
+ }
5187
+ console.log(` ✅ ${step.name} review completed`);
5188
+ return { success: true, stepId: step.id, stepName: step.name };
5189
+ }
5190
+ catch (error) {
5191
+ console.error(` ❌ ${step.name} review failed: ${error.message}`);
5192
+ return { success: false, error: error.message, stepId: step.id, stepName: step.name };
5193
+ }
5194
+ }
5195
+ /**
5196
+ * Merge review results from multiple steps into a single unified review
5197
+ */
5198
+ async function mergeReviewResults(stepResults, filesToReview, finalOutputPath) {
5199
+ const timestamp = new Date().toISOString().split('T')[0];
5200
+ const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
5201
+ // Start building the merged review
5202
+ let mergedReview = `# Code Review
5203
+
5204
+ **Date**: ${timestamp}
5205
+ **Reviewer**: AI Code Review System
5206
+ **Files Changed**: ${filesToReview.length}
5207
+ **Functions Changed**: ${totalFunctions}
4328
5208
 
4329
5209
  ---
4330
5210
 
4331
- ## 💡 General Recommendations
5211
+ ## Summary
4332
5212
 
4333
- 1. [Recommendation 1]
4334
- 2. [Recommendation 2]
5213
+ 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
5214
 
4336
5215
  ---
4337
5216
 
4338
- ## Conclusion
5217
+ ## Files Changed
4339
5218
 
4340
- [Final thoughts and overall assessment]
4341
- \`\`\`
5219
+ ${filesToReview.map(f => `- **${f.filePath}**
5220
+ - Functions: ${f.functions.join(', ')}`).join('\n')}
4342
5221
 
4343
- ## CRITICAL REMINDERS
5222
+ ---
4344
5223
 
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;
5224
+ `;
5225
+ // Add each step's findings
5226
+ for (const result of stepResults) {
5227
+ const stepFilePath = path.join('reviews', '.tmp', `step-${result.stepId}.md`);
5228
+ if (result.success) {
5229
+ try {
5230
+ // Read the step review file
5231
+ if (fsSync.existsSync(stepFilePath)) {
5232
+ const stepContent = await fs.readFile(stepFilePath, 'utf-8');
5233
+ // Extract content after the first heading (skip the step title)
5234
+ const lines = stepContent.split('\n');
5235
+ let contentStart = 0;
5236
+ for (let i = 0; i < lines.length; i++) {
5237
+ if (lines[i].startsWith('# ')) {
5238
+ contentStart = i + 1;
5239
+ break;
5240
+ }
5241
+ }
5242
+ const content = lines.slice(contentStart).join('\n').trim();
5243
+ // Add as a section in the merged review
5244
+ mergedReview += `## ${result.stepName}\n\n${content}\n\n---\n\n`;
5245
+ }
5246
+ else {
5247
+ mergedReview += `## ${result.stepName}\n\n⚠️ Review file not found.\n\n---\n\n`;
4381
5248
  }
4382
5249
  }
4383
- }
4384
- if (!response.toolCalls || response.toolCalls.length === 0) {
4385
- if (reviewWritten) {
4386
- console.log('\n✅ Code review complete!');
4387
- break;
5250
+ catch (error) {
5251
+ mergedReview += `## ${result.stepName}\n\n⚠️ Error reading review: ${error.message}\n\n---\n\n`;
4388
5252
  }
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
5253
  }
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
- }
5254
+ else {
5255
+ mergedReview += `## ${result.stepName}\n\n❌ **Review step failed**: ${result.error}\n\n---\n\n`;
4405
5256
  }
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
- });
5257
+ }
5258
+ // Add conclusion
5259
+ mergedReview += `## Conclusion
5260
+
5261
+ This comprehensive review analyzed the code changes from multiple perspectives. Please address the findings based on their severity levels, starting with critical issues.
5262
+
5263
+ The review was conducted using AI-powered analysis with access to the full codebase context.
5264
+ `;
5265
+ // Ensure reviews directory exists
5266
+ const reviewsDir = path.dirname(finalOutputPath);
5267
+ if (!fsSync.existsSync(reviewsDir)) {
5268
+ await fs.mkdir(reviewsDir, { recursive: true });
5269
+ }
5270
+ // Write the merged review
5271
+ await fs.writeFile(finalOutputPath, mergedReview, 'utf-8');
5272
+ console.log(`\n✅ Merged review written to: ${finalOutputPath}`);
5273
+ // Clean up temporary files
5274
+ try {
5275
+ const tmpDir = path.join('reviews', '.tmp');
5276
+ if (fsSync.existsSync(tmpDir)) {
5277
+ const tmpFiles = await fs.readdir(tmpDir);
5278
+ for (const file of tmpFiles) {
5279
+ await fs.unlink(path.join(tmpDir, file));
4423
5280
  }
5281
+ await fs.rmdir(tmpDir);
4424
5282
  }
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
- }
5283
+ }
5284
+ catch (error) {
5285
+ // Ignore cleanup errors
5286
+ }
5287
+ }
5288
+ /**
5289
+ * Generate unified code review for all changed files
5290
+ */
5291
+ async function generateUnifiedCodeReview(filesToReview) {
5292
+ const reviewFilePath = path.join('reviews', 'code_review.md');
5293
+ // Get enabled review steps from config
5294
+ const enabledSteps = CONFIG.reviewSteps.filter(step => step.enabled);
5295
+ if (enabledSteps.length === 0) {
5296
+ console.log('\n⚠️ No review steps enabled. Please enable at least one step in codeguard.json');
5297
+ return;
5298
+ }
5299
+ console.log(`\n📋 Running ${enabledSteps.length} review step(s): ${enabledSteps.map(s => s.name).join(', ')}`);
5300
+ // Ensure temporary directory exists
5301
+ const tmpDir = path.join('reviews', '.tmp');
5302
+ if (!fsSync.existsSync(tmpDir)) {
5303
+ await fs.mkdir(tmpDir, { recursive: true });
5304
+ }
5305
+ // Execute review steps
5306
+ let stepResults;
5307
+ if (CONFIG.reviewExecutionMode === 'parallel') {
5308
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5309
+ globalSpinner = ora('🔄 Executing review steps in parallel...').start();
4438
5310
  }
4439
5311
  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
- });
5312
+ globalSpinner.text = '🔄 Executing review steps in parallel...';
5313
+ }
5314
+ // Run all steps in parallel
5315
+ const stepPromises = enabledSteps.map(step => {
5316
+ const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
5317
+ return executeReviewStep(step, filesToReview, stepOutputPath);
5318
+ });
5319
+ stepResults = await Promise.all(stepPromises);
5320
+ }
5321
+ else {
5322
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5323
+ globalSpinner = ora('🔄 Executing review steps sequentially...').start();
5324
+ }
5325
+ else {
5326
+ globalSpinner.text = '🔄 Executing review steps sequentially...';
5327
+ }
5328
+ // Run steps sequentially
5329
+ stepResults = [];
5330
+ for (const step of enabledSteps) {
5331
+ if (globalSpinner) {
5332
+ globalSpinner.text = `🔍 Running ${step.name} review step...`;
5333
+ }
5334
+ const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
5335
+ const result = await executeReviewStep(step, filesToReview, stepOutputPath);
5336
+ stepResults.push(result);
5337
+ if (globalSpinner && result.success) {
5338
+ globalSpinner.text = `${step.name} review completed`;
5339
+ await new Promise(resolve => setTimeout(resolve, 200));
5340
+ }
4458
5341
  }
4459
5342
  }
4460
- if (!reviewWritten) {
4461
- console.log('\n⚠️ Could not complete code review. Manual review may be needed.');
5343
+ // Merge results into final review
5344
+ await mergeReviewResults(stepResults, filesToReview, reviewFilePath);
5345
+ // Clear global spinner before final summary
5346
+ if (globalSpinner && globalSpinner.isSpinning) {
5347
+ globalSpinner.stop();
5348
+ globalSpinner = null;
4462
5349
  }
5350
+ // Summary
5351
+ const successCount = stepResults.filter(r => r.success).length;
5352
+ const failCount = stepResults.filter(r => !r.success).length;
5353
+ console.log(`\n✅ Review complete: ${successCount} step(s) succeeded${failCount > 0 ? `, ${failCount} failed` : ''}`);
4463
5354
  }
4464
5355
  async function main() {
4465
5356
  console.log('🧪 AI-Powered Unit Test Generator with AST Analysis\n');
@@ -4467,11 +5358,12 @@ async function main() {
4467
5358
  const args = process.argv.slice(2);
4468
5359
  const command = args[0]; // First argument is the command: 'auto', 'test', 'review', or undefined
4469
5360
  // Validate command if provided
4470
- if (command && !['auto', 'test', 'review'].includes(command)) {
5361
+ if (command && !['auto', 'test', 'review', 'doc'].includes(command)) {
4471
5362
  console.error('❌ Invalid command. Usage:\n');
4472
5363
  console.error(' testgen auto - Review changes and generate tests');
4473
5364
  console.error(' testgen test - Generate tests only');
4474
5365
  console.error(' testgen review - Review changes only');
5366
+ console.error(' testgen doc - Generate API documentation');
4475
5367
  console.error(' testgen - Interactive mode\n');
4476
5368
  process.exit(1);
4477
5369
  }
@@ -4497,21 +5389,31 @@ async function main() {
4497
5389
  console.error('npm install @babel/parser @babel/traverse ts-node\n');
4498
5390
  process.exit(1);
4499
5391
  }
4500
- // If command mode (auto, test, review), skip indexing setup and proceed directly
4501
- if (command === 'auto' || command === 'test' || command === 'review') {
5392
+ // If command mode (auto, test, review, doc), prepare index (build/update) and proceed directly
5393
+ if (command === 'auto' || command === 'test' || command === 'review' || command === 'doc') {
4502
5394
  const modeLabel = command === 'auto' ? 'Auto Mode (Review + Test)' :
4503
5395
  command === 'test' ? 'Test Generation Mode' :
4504
- 'Code Review Mode';
5396
+ command === 'review' ? 'Code Review Mode' :
5397
+ CONFIG.repoDoc ? 'Documentation Generation Mode (Full Repository)' : 'Documentation Generation Mode (Changes Only)';
4505
5398
  console.log(`🤖 ${modeLabel}: Detecting changes via git diff\n`);
4506
5399
  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
5400
+ // Initialize and prepare codebase indexer (build or update) without prompts
4508
5401
  globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
4509
5402
  const hasExistingIndex = globalIndexer.hasIndex();
4510
5403
  if (hasExistingIndex) {
4511
- await globalIndexer.loadIndex();
5404
+ const loaded = await globalIndexer.loadIndex();
5405
+ if (loaded) {
5406
+ const staleFiles = globalIndexer.getStaleFiles();
5407
+ if (staleFiles.length > 0) {
5408
+ console.log(`🔄 Updating ${staleFiles.length} modified file(s) in index...`);
5409
+ await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
5410
+ }
5411
+ }
4512
5412
  }
4513
5413
  else {
4514
- globalIndexer = null;
5414
+ // Build index once for the whole repo to speed up AST queries
5415
+ console.log('📦 Building codebase index for faster analysis...');
5416
+ await globalIndexer.buildIndex('.', analyzeFileAST);
4515
5417
  }
4516
5418
  // Execute based on command
4517
5419
  if (command === 'auto') {
@@ -4528,6 +5430,11 @@ async function main() {
4528
5430
  // Only review changes
4529
5431
  await reviewChangedFiles();
4530
5432
  }
5433
+ else if (command === 'doc') {
5434
+ // Only generate documentation
5435
+ console.log('Currently in development. Please try again later.');
5436
+ // await generateDocumentationMode();
5437
+ }
4531
5438
  return;
4532
5439
  }
4533
5440
  // Optional: Codebase Indexing
@@ -4543,16 +5450,18 @@ async function main() {
4543
5450
  // Check for stale files (modified since last index)
4544
5451
  const staleFiles = globalIndexer.getStaleFiles();
4545
5452
  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)}`));
5453
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5454
+ globalSpinner = ora(`🔄 Updating ${staleFiles.length} modified file(s)...`).start();
4550
5455
  }
4551
5456
  else {
4552
- console.log(` 📝 Updating ${staleFiles.length} files...`);
5457
+ globalSpinner.text = `🔄 Updating ${staleFiles.length} modified file(s)...`;
4553
5458
  }
4554
5459
  await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
4555
- console.log(`✅ Index updated!\n`);
5460
+ globalSpinner.text = `Index updated - ${staleFiles.length} file(s) refreshed`;
5461
+ await new Promise(resolve => setTimeout(resolve, 200));
5462
+ globalSpinner.stop();
5463
+ globalSpinner = null;
5464
+ console.log(); // Empty line for spacing
4556
5465
  }
4557
5466
  else {
4558
5467
  console.log('✅ All files up to date!\n');
@@ -4615,13 +5524,23 @@ async function main() {
4615
5524
  case '1':
4616
5525
  default:
4617
5526
  // File-wise mode (original functionality)
4618
- console.log('\n📂 Scanning repository...\n');
5527
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5528
+ globalSpinner = ora('📂 Scanning repository...').start();
5529
+ }
5530
+ else {
5531
+ globalSpinner.text = '📂 Scanning repository...';
5532
+ }
4619
5533
  const files = await listFilesRecursive('.');
4620
5534
  if (files.length === 0) {
4621
- console.log('No source files found!');
5535
+ globalSpinner.fail('No source files found!');
5536
+ globalSpinner = null;
4622
5537
  return;
4623
5538
  }
4624
- console.log('Select a file to generate tests:\n');
5539
+ globalSpinner.text = `Found ${files.length} source file(s)`;
5540
+ await new Promise(resolve => setTimeout(resolve, 200));
5541
+ globalSpinner.stop();
5542
+ globalSpinner = null;
5543
+ console.log('\nSelect a file to generate tests:\n');
4625
5544
  files.forEach((file, index) => {
4626
5545
  console.log(`${index + 1}. ${file}`);
4627
5546
  });
@@ -4632,6 +5551,11 @@ async function main() {
4632
5551
  return;
4633
5552
  }
4634
5553
  await generateTests(selectedFile);
5554
+ // Clear spinner before final message
5555
+ if (globalSpinner && globalSpinner.isSpinning) {
5556
+ globalSpinner.stop();
5557
+ globalSpinner = null;
5558
+ }
4635
5559
  console.log('\n✨ Done!');
4636
5560
  break;
4637
5561
  }