codeguard-testgen 1.0.9 โ†’ 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,11 +33,13 @@ exports.insertAtPosition = insertAtPosition;
33
33
  exports.deleteLines = deleteLines;
34
34
  exports.insertLines = insertLines;
35
35
  exports.replaceLines = replaceLines;
36
+ exports.writeReview = writeReview;
36
37
  const fs = require("fs/promises");
37
38
  const fsSync = require("fs");
38
39
  const path = require("path");
39
40
  const child_process_1 = require("child_process");
40
41
  const readline = require("readline");
42
+ const ora = require('ora');
41
43
  // AST parsers
42
44
  const babelParser = require("@babel/parser");
43
45
  const traverse = require('@babel/traverse').default;
@@ -52,6 +54,9 @@ const fuzzyMatcher_1 = require("./fuzzyMatcher");
52
54
  let CONFIG;
53
55
  // Global indexer instance (optional - only initialized if user chooses to index)
54
56
  let globalIndexer = null;
57
+ let globalSpinner = null; // Shared spinner for all operations
58
+ // Global variable to track expected test file path (to prevent AI from creating per-function files)
59
+ let EXPECTED_TEST_FILE_PATH = null;
55
60
  // AI Provider configurations - models will be set from CONFIG
56
61
  function getAIProviders() {
57
62
  return {
@@ -121,13 +126,17 @@ const TOOLS = [
121
126
  },
122
127
  {
123
128
  name: 'analyze_file_ast',
124
- description: 'Parse file using AST and extract detailed information about 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.',
125
130
  input_schema: {
126
131
  type: 'object',
127
132
  properties: {
128
133
  file_path: {
129
134
  type: 'string',
130
135
  description: 'The relative path to the source file.'
136
+ },
137
+ function_name: {
138
+ type: 'string',
139
+ description: 'Optional: Filter results to only this function. Use in function-wise test generation to reduce tokens (returns metadata for 1 function instead of all). Omit to get all functions (file-wise mode).'
131
140
  }
132
141
  },
133
142
  required: ['file_path']
@@ -181,7 +190,7 @@ const TOOLS = [
181
190
  },
182
191
  {
183
192
  name: 'get_file_preamble',
184
- description: 'Extract top-level code (imports, 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.',
185
194
  input_schema: {
186
195
  type: 'object',
187
196
  properties: {
@@ -253,7 +262,7 @@ const TOOLS = [
253
262
  },
254
263
  {
255
264
  name: 'run_tests',
256
- description: 'Run 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.',
257
266
  input_schema: {
258
267
  type: 'object',
259
268
  properties: {
@@ -264,7 +273,7 @@ const TOOLS = [
264
273
  function_names: {
265
274
  type: 'array',
266
275
  items: { type: 'string' },
267
- description: 'Optional: Array of function names to test. When provided, only runs tests matching these function names using 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.'
268
277
  }
269
278
  },
270
279
  required: ['test_file_path']
@@ -300,7 +309,7 @@ const TOOLS = [
300
309
  },
301
310
  {
302
311
  name: 'calculate_relative_path',
303
- description: 'Calculate the correct relative import path from one file to 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!',
304
313
  input_schema: {
305
314
  type: 'object',
306
315
  properties: {
@@ -310,39 +319,12 @@ const TOOLS = [
310
319
  },
311
320
  to_file: {
312
321
  type: 'string',
313
- description: '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")'
314
323
  }
315
324
  },
316
325
  required: ['from_file', 'to_file']
317
326
  }
318
327
  },
319
- {
320
- name: 'report_legitimate_failure',
321
- description: 'Report that test failures are legitimate due to bugs in the source code, not issues with the test itself. Use this when tests are correctly written but fail because the source code has bugs.',
322
- input_schema: {
323
- type: 'object',
324
- properties: {
325
- test_file_path: {
326
- type: 'string',
327
- description: 'The path to the test file'
328
- },
329
- failing_tests: {
330
- type: 'array',
331
- items: { type: 'string' },
332
- description: 'List of test names that are legitimately failing'
333
- },
334
- reason: {
335
- type: 'string',
336
- description: 'Explanation of why the failures are legitimate (e.g., "Function returns undefined instead of expected object", "Missing null check causes TypeError")'
337
- },
338
- source_code_issue: {
339
- type: 'string',
340
- description: 'Description of the bug in the source code that causes the failure'
341
- }
342
- },
343
- required: ['test_file_path', 'failing_tests', 'reason', 'source_code_issue']
344
- }
345
- },
346
328
  {
347
329
  name: 'search_replace_block',
348
330
  description: '๐Ÿš€ RECOMMENDED: Search for a code block and replace it with new content. Uses intelligent fuzzy matching to handle whitespace/indentation differences. 10x more reliable than line-based editing! Include 3-5 lines of context before/after the target to make the search unique.',
@@ -397,8 +379,60 @@ const TOOLS = [
397
379
  required: ['file_path', 'content']
398
380
  }
399
381
  },
382
+ {
383
+ name: 'write_review',
384
+ description: 'Write code review findings to a markdown file in the reviews/ directory. Use this to output your comprehensive code review.',
385
+ input_schema: {
386
+ type: 'object',
387
+ properties: {
388
+ file_path: {
389
+ type: 'string',
390
+ description: 'The path to the review file (e.g., "reviews/index.review.md"). Should be in reviews/ directory with .review.md extension.'
391
+ },
392
+ review_content: {
393
+ type: 'string',
394
+ description: 'The complete markdown content of the code review including summary, findings by category (Code Quality, Bugs, Performance, Security), severity levels, and recommendations.'
395
+ }
396
+ },
397
+ required: ['file_path', 'review_content']
398
+ }
399
+ },
400
+ {
401
+ name: 'search_codebase',
402
+ description: 'Search the entire codebase for a pattern using grep. Useful for finding where a function is used, where routes are registered, or finding usage examples. Returns matching lines with file paths and line numbers. IMPORTANT: Use focused searches to avoid token limits - be specific with patterns and use file_extension filter.',
403
+ input_schema: {
404
+ type: 'object',
405
+ properties: {
406
+ pattern: {
407
+ type: 'string',
408
+ description: 'The search pattern (can be a string or regex). Examples: "getUsers", "router.get.*getUsers" (be specific!)'
409
+ },
410
+ file_extension: {
411
+ type: 'string',
412
+ description: 'RECOMMENDED: Filter by file extension (e.g., ".ts", ".js") to reduce results'
413
+ },
414
+ max_results: {
415
+ type: 'number',
416
+ description: 'Maximum number of matching lines to return (default: 20, max: 50). Use low values to avoid token limits.'
417
+ },
418
+ files_only: {
419
+ type: 'boolean',
420
+ description: 'If true, return only file paths without content. Use this first to see how many files match, then search specific files.'
421
+ }
422
+ },
423
+ required: ['pattern']
424
+ }
425
+ },
400
426
  ];
401
427
  exports.TOOLS = TOOLS;
428
+ // Filtered tools for test generation (excludes write_review)
429
+ const TOOLS_FOR_TEST_GENERATION = TOOLS.filter(tool => tool.name !== 'write_review');
430
+ const TOOLS_FOR_CODE_REVIEW = TOOLS.filter((tool) => tool.name !== "run_tests" &&
431
+ tool.name !== "upsert_function_tests" &&
432
+ tool.name !== "insert_at_position" &&
433
+ tool.name !== "search_replace_block" &&
434
+ tool.name !== "calculate_relative_path" &&
435
+ tool.name !== "resolve_import_path");
402
436
  // AST Parsing utilities
403
437
  function parseFileToAST(filePath, content) {
404
438
  const ext = path.extname(filePath);
@@ -423,7 +457,7 @@ function parseFileToAST(filePath, content) {
423
457
  throw new Error(`Failed to parse ${filePath}: ${error.message}`);
424
458
  }
425
459
  }
426
- function analyzeFileAST(filePath) {
460
+ function analyzeFileAST(filePath, functionName) {
427
461
  try {
428
462
  const content = fsSync.readFileSync(filePath, 'utf-8');
429
463
  const ast = parseFileToAST(filePath, content);
@@ -436,7 +470,8 @@ function analyzeFileAST(filePath) {
436
470
  exports: [],
437
471
  imports: [],
438
472
  types: [],
439
- constants: []
473
+ constants: [],
474
+ routeRegistrations: []
440
475
  };
441
476
  traverse(ast, {
442
477
  // Function declarations
@@ -547,6 +582,49 @@ function analyzeFileAST(filePath) {
547
582
  });
548
583
  }
549
584
  });
585
+ // Detect route registrations
586
+ analysis.routeRegistrations = detectRouteRegistrations(content, analysis.imports);
587
+ // Filter to specific function if requested (token optimization)
588
+ if (functionName) {
589
+ const targetFunction = analysis.functions.find(f => f.name === functionName);
590
+ if (!targetFunction) {
591
+ // Provide helpful error with available functions
592
+ const available = analysis.functions.map(f => f.name).slice(0, 10).join(', ');
593
+ const more = analysis.functions.length > 10 ? ` (and ${analysis.functions.length - 10} more)` : '';
594
+ return {
595
+ success: false,
596
+ error: `Function '${functionName}' not found in ${filePath}. Available functions: ${available}${more}`
597
+ };
598
+ }
599
+ // Find related class if function is a method
600
+ let relatedClass = null;
601
+ for (const cls of analysis.classes) {
602
+ if (cls.methods?.some((m) => m.name === functionName)) {
603
+ relatedClass = cls;
604
+ break;
605
+ }
606
+ }
607
+ return {
608
+ success: true,
609
+ analysis: {
610
+ functions: [targetFunction],
611
+ classes: relatedClass ? [relatedClass] : [],
612
+ exports: analysis.exports.filter(e => e.name === functionName || e.local === functionName),
613
+ imports: analysis.imports, // Keep all imports for dependency context
614
+ types: [], // Types filtered out for token efficiency
615
+ constants: [], // Constants filtered out for token efficiency
616
+ routeRegistrations: analysis.routeRegistrations.filter(r => r.handler === functionName)
617
+ },
618
+ summary: {
619
+ functionCount: 1,
620
+ classCount: relatedClass ? 1 : 0,
621
+ exportCount: analysis.exports.filter(e => e.name === functionName || e.local === functionName).length,
622
+ typeCount: 0,
623
+ filtered: true,
624
+ targetFunction: functionName
625
+ }
626
+ };
627
+ }
550
628
  return {
551
629
  success: true,
552
630
  analysis,
@@ -562,6 +640,60 @@ function analyzeFileAST(filePath) {
562
640
  return { success: false, error: error.message };
563
641
  }
564
642
  }
643
+ // Fast route detection using regex patterns
644
+ function detectRouteRegistrations(code, imports) {
645
+ const registrations = [];
646
+ // Express: router.get('/users', getUsers) or app.post('/users', createUser)
647
+ const expressPattern = /\b(app|router)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
648
+ for (const match of code.matchAll(expressPattern)) {
649
+ registrations.push({
650
+ framework: 'express',
651
+ method: match[2].toUpperCase(),
652
+ path: match[3],
653
+ handler: match[4],
654
+ handlerImportedFrom: findImportSource(match[4], imports),
655
+ line: getLineNumber(code, match.index || 0)
656
+ });
657
+ }
658
+ // Fastify: fastify.get('/users', getUsers)
659
+ const fastifyPattern = /\bfastify\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
660
+ for (const match of code.matchAll(fastifyPattern)) {
661
+ registrations.push({
662
+ framework: 'fastify',
663
+ method: match[1].toUpperCase(),
664
+ path: match[2],
665
+ handler: match[3],
666
+ handlerImportedFrom: findImportSource(match[3], imports),
667
+ line: getLineNumber(code, match.index || 0)
668
+ });
669
+ }
670
+ // Koa: router.get('/users', getUsers)
671
+ const koaPattern = /\brouter\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/g;
672
+ for (const match of code.matchAll(koaPattern)) {
673
+ registrations.push({
674
+ framework: 'koa',
675
+ method: match[1].toUpperCase(),
676
+ path: match[2],
677
+ handler: match[3],
678
+ handlerImportedFrom: findImportSource(match[3], imports),
679
+ line: getLineNumber(code, match.index || 0)
680
+ });
681
+ }
682
+ return registrations;
683
+ }
684
+ function findImportSource(functionName, imports) {
685
+ for (const imp of imports) {
686
+ if (imp.imported && Array.isArray(imp.imported)) {
687
+ if (imp.imported.some((i) => i.local === functionName || i.imported === functionName)) {
688
+ return imp.source;
689
+ }
690
+ }
691
+ }
692
+ return undefined;
693
+ }
694
+ function getLineNumber(code, index) {
695
+ return code.substring(0, index).split('\n').length;
696
+ }
565
697
  function extractParamInfo(param) {
566
698
  if (param.type === 'Identifier') {
567
699
  return {
@@ -669,7 +801,7 @@ function getFunctionAST(filePath, functionName) {
669
801
  }
670
802
  // Fallback: If we have an indexer, search for the function in the index
671
803
  if (globalIndexer) {
672
- console.log(` ๐Ÿ” Function not found in ${filePath}, searching index...`);
804
+ // console.log(` ๐Ÿ” Function not found in ${filePath}, searching index...`);
673
805
  // Search through all indexed files
674
806
  const indexData = globalIndexer.index;
675
807
  if (indexData && indexData.files) {
@@ -679,7 +811,7 @@ function getFunctionAST(filePath, functionName) {
679
811
  const hasFunction = analysis.functions?.some((fn) => fn.name === functionName);
680
812
  const hasMethod = analysis.classes?.some((cls) => cls.methods?.some((method) => method.name === functionName));
681
813
  if (hasFunction || hasMethod) {
682
- console.log(` โœ… Found ${functionName} in ${indexedFilePath}`);
814
+ // console.log(` โœ… Found ${functionName} in ${indexedFilePath}`);
683
815
  const result = searchInFile(indexedFilePath);
684
816
  if (result) {
685
817
  return result;
@@ -820,7 +952,7 @@ function getImportsAST(filePath) {
820
952
  catch (error) {
821
953
  // File not found - try to find similar file in index
822
954
  if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
823
- console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
955
+ // console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
824
956
  const indexData = globalIndexer.index;
825
957
  if (indexData && indexData.files) {
826
958
  const searchName = path.basename(filePath);
@@ -930,7 +1062,7 @@ function getTypeDefinitions(filePath) {
930
1062
  catch (error) {
931
1063
  // File not found - try to find similar file in index
932
1064
  if (globalIndexer && (error.code === 'ENOENT' || error.message.includes('no such file'))) {
933
- console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
1065
+ // console.log(` ๐Ÿ” File not found at ${filePath}, searching index for similar files...`);
934
1066
  const indexData = globalIndexer.index;
935
1067
  if (indexData && indexData.files) {
936
1068
  const searchName = path.basename(filePath);
@@ -1015,13 +1147,13 @@ function getFilePreamble(filePath) {
1015
1147
  });
1016
1148
  });
1017
1149
  }
1018
- // Expression statements (could be jest.mock or setup blocks)
1150
+ // Expression statements (could be vi.mock/jest.mock or setup blocks)
1019
1151
  else if (statement.type === 'ExpressionStatement' && statement.expression?.type === 'CallExpression') {
1020
1152
  const callExpr = statement.expression;
1021
1153
  const callee = callExpr.callee;
1022
- // Check for jest.mock()
1154
+ // Check for vi.mock() or jest.mock()
1023
1155
  if (callee.type === 'MemberExpression' &&
1024
- callee.object?.name === 'jest' &&
1156
+ (callee.object?.name === 'vi' || callee.object?.name === 'jest') &&
1025
1157
  callee.property?.name === 'mock') {
1026
1158
  const moduleName = callExpr.arguments[0]?.value || 'unknown';
1027
1159
  let isVirtual = false;
@@ -1075,7 +1207,7 @@ function getFilePreamble(filePath) {
1075
1207
  ...topLevelVariables.map(v => ({ ...v, category: 'variable' }))
1076
1208
  ].sort((a, b) => a.startLine - b.startLine);
1077
1209
  const fullCode = allItems.map(item => `// Lines ${item.startLine}-${item.endLine} (${item.category})\n${item.code}`).join('\n\n');
1078
- console.log(` โœ… Found file at ${targetPath}`);
1210
+ // console.log(` โœ… Found file at ${targetPath}`);
1079
1211
  // console.log(` fullCode: ${fullCode}`);
1080
1212
  // console.log(` summary: ${JSON.stringify({
1081
1213
  // importCount: imports.length,
@@ -1097,7 +1229,6 @@ function getFilePreamble(filePath) {
1097
1229
  setupBlocks,
1098
1230
  topLevelVariables
1099
1231
  },
1100
- fullCode,
1101
1232
  summary: {
1102
1233
  importCount: imports.length,
1103
1234
  mockCount: mocks.length,
@@ -1118,7 +1249,7 @@ function getFilePreamble(filePath) {
1118
1249
  }
1119
1250
  // Fallback: If we have an indexer, search for file with matching path
1120
1251
  if (globalIndexer) {
1121
- console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1252
+ // console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1122
1253
  const indexData = globalIndexer.index;
1123
1254
  if (indexData && indexData.files) {
1124
1255
  const searchName = path.basename(filePath);
@@ -1151,10 +1282,10 @@ function getFilePreamble(filePath) {
1151
1282
  // Sort by score (highest first)
1152
1283
  scored.sort((a, b) => b.score - a.score);
1153
1284
  correctPath = scored[0].path;
1154
- console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1285
+ // console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1155
1286
  }
1156
1287
  else {
1157
- console.log(` โœ… Found file at ${correctPath}`);
1288
+ // console.log(` โœ… Found file at ${correctPath}`);
1158
1289
  }
1159
1290
  const retryResult = extractPreamble(correctPath);
1160
1291
  if (retryResult) {
@@ -1236,7 +1367,7 @@ function getClassMethods(filePath, className) {
1236
1367
  }
1237
1368
  // Fallback: If we have an indexer, search for the class in the index
1238
1369
  if (globalIndexer) {
1239
- console.log(` ๐Ÿ” Class not found in ${filePath}, searching index...`);
1370
+ // console.log(` ๐Ÿ” Class not found in ${filePath}, searching index...`);
1240
1371
  // Search through all indexed files
1241
1372
  const indexData = globalIndexer.index;
1242
1373
  if (indexData && indexData.files) {
@@ -1245,7 +1376,7 @@ function getClassMethods(filePath, className) {
1245
1376
  // Check if this file contains the class
1246
1377
  const hasClass = analysis.classes?.some((cls) => cls.name === className);
1247
1378
  if (hasClass) {
1248
- console.log(` โœ… Found ${className} in ${indexedFilePath}`);
1379
+ // console.log(` โœ… Found ${className} in ${indexedFilePath}`);
1249
1380
  const result = searchInFile(indexedFilePath);
1250
1381
  if (result) {
1251
1382
  return result;
@@ -1266,7 +1397,7 @@ function getClassMethods(filePath, className) {
1266
1397
  }
1267
1398
  // Other tool implementations
1268
1399
  async function readFile(filePath) {
1269
- const MAX_LINES = 2000;
1400
+ const MAX_LINES = 1000;
1270
1401
  // Helper to read and validate file
1271
1402
  const tryReadFile = async (targetPath) => {
1272
1403
  try {
@@ -1279,14 +1410,14 @@ async function readFile(filePath) {
1279
1410
  if (globalIndexer) {
1280
1411
  const cached = globalIndexer.getFileAnalysis(targetPath);
1281
1412
  if (cached) {
1282
- console.log(` ๐Ÿ“ฆ File too large (${lines.length} lines), returning cached analysis instead`);
1413
+ // console.log(` ๐Ÿ“ฆ File too large (${lines.length} lines), returning cached analysis instead`);
1283
1414
  // For test files, also include preamble automatically
1284
1415
  let preamble = undefined;
1285
1416
  if (isTestFile) {
1286
1417
  const preambleResult = getFilePreamble(targetPath);
1287
1418
  if (preambleResult.success) {
1288
1419
  preamble = preambleResult;
1289
- console.log(` ๐Ÿ“‹ Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
1420
+ // console.log(` ๐Ÿ“‹ Also extracted preamble (${preambleResult.summary.importCount} imports, ${preambleResult.summary.mockCount} mocks)`);
1290
1421
  }
1291
1422
  }
1292
1423
  return {
@@ -1324,7 +1455,7 @@ async function readFile(filePath) {
1324
1455
  }
1325
1456
  // Fallback: If we have an indexer, search for file with matching path
1326
1457
  if (globalIndexer) {
1327
- console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1458
+ // console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1328
1459
  const indexData = globalIndexer.index;
1329
1460
  if (indexData && indexData.files) {
1330
1461
  const searchName = path.basename(filePath);
@@ -1357,10 +1488,10 @@ async function readFile(filePath) {
1357
1488
  // Sort by score (highest first)
1358
1489
  scored.sort((a, b) => b.score - a.score);
1359
1490
  correctPath = scored[0].path;
1360
- console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1491
+ // console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1361
1492
  }
1362
1493
  else {
1363
- console.log(` โœ… Found file at ${correctPath}`);
1494
+ // console.log(` โœ… Found file at ${correctPath}`);
1364
1495
  }
1365
1496
  // Try to read from correct path
1366
1497
  const retryResult = await tryReadFile(correctPath);
@@ -1455,7 +1586,7 @@ async function readFileLines(filePath, startLine, endLine) {
1455
1586
  }
1456
1587
  // Fallback: If we have an indexer, search for file with matching path
1457
1588
  if (globalIndexer) {
1458
- console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1589
+ // console.log(` ๐Ÿ” File not found at ${filePath}, searching index...`);
1459
1590
  const indexData = globalIndexer.index;
1460
1591
  if (indexData && indexData.files) {
1461
1592
  const searchName = path.basename(filePath);
@@ -1488,10 +1619,10 @@ async function readFileLines(filePath, startLine, endLine) {
1488
1619
  // Sort by score (highest first)
1489
1620
  scored.sort((a, b) => b.score - a.score);
1490
1621
  correctPath = scored[0].path;
1491
- console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1622
+ // console.log(` ๐Ÿ’ก Multiple matches found, choosing best match: ${correctPath}`);
1492
1623
  }
1493
1624
  else {
1494
- console.log(` โœ… Found file at ${correctPath}`);
1625
+ // console.log(` โœ… Found file at ${correctPath}`);
1495
1626
  }
1496
1627
  // Try to read from correct path
1497
1628
  const retryResult = await tryReadFileLines(correctPath);
@@ -1889,7 +2020,55 @@ async function replaceFunctionTests(testFilePath, functionName, newTestContent)
1889
2020
  return { success: false, error: error.message };
1890
2021
  }
1891
2022
  }
1892
- 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) {
1893
2072
  try {
1894
2073
  // Build Jest command with optional function name filter
1895
2074
  let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
@@ -1903,7 +2082,8 @@ function runTests(testFilePath, functionNames) {
1903
2082
  }
1904
2083
  const output = (0, child_process_1.execSync)(command, {
1905
2084
  encoding: 'utf-8',
1906
- stdio: 'pipe'
2085
+ stdio: 'pipe',
2086
+ timeout: 10000
1907
2087
  });
1908
2088
  console.log(` Test run output: ${output}`);
1909
2089
  return {
@@ -1914,8 +2094,8 @@ function runTests(testFilePath, functionNames) {
1914
2094
  };
1915
2095
  }
1916
2096
  catch (error) {
1917
- console.log(` Test run error: ${error.message}`);
1918
- console.log(`output sent to ai: ${error.stdout + error.stderr}`);
2097
+ // console.log(` Test run error: ${error.message}`);
2098
+ // console.log(`output sent to ai: ${error.stdout + error.stderr}`);
1919
2099
  return {
1920
2100
  success: false,
1921
2101
  output: error.stdout + error.stderr,
@@ -1924,6 +2104,121 @@ function runTests(testFilePath, functionNames) {
1924
2104
  };
1925
2105
  }
1926
2106
  }
2107
+ /**
2108
+ * Run specific tests in isolation to detect if failure is due to pollution or regression
2109
+ * Used by smartValidateTestSuite to differentiate between test infrastructure issues and source code bugs
2110
+ */
2111
+ function runTestsIsolated(testFilePath, specificTestNames) {
2112
+ try {
2113
+ // Build Vitest command using npm run test with dot reporter and no color for clean output
2114
+ let command = `npm run test ${testFilePath} -- --reporter=dot --no-color`;
2115
+ if (specificTestNames && specificTestNames.length > 0) {
2116
+ const escapedNames = specificTestNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
2117
+ const pattern = escapedNames.join('|');
2118
+ command += ` -t "${pattern}"`;
2119
+ }
2120
+ const output = (0, child_process_1.execSync)(command, {
2121
+ encoding: 'utf-8',
2122
+ stdio: 'pipe',
2123
+ timeout: 60000, // 60 seconds
2124
+ maxBuffer: 10 * 1024 * 1024 // 10MB buffer
2125
+ });
2126
+ return {
2127
+ success: true,
2128
+ output: stripAnsi(output),
2129
+ passed: true,
2130
+ command
2131
+ };
2132
+ }
2133
+ catch (error) {
2134
+ // execSync error has stdout/stderr as strings when encoding is set
2135
+ let output = '';
2136
+ if (error.stdout)
2137
+ output += error.stdout;
2138
+ if (error.stderr)
2139
+ output += error.stderr;
2140
+ // Fallback: extract from error message if output is empty
2141
+ if (!output && error.message) {
2142
+ output = error.message;
2143
+ }
2144
+ return {
2145
+ success: false,
2146
+ output: stripAnsi(output) || 'Test failed with no output captured',
2147
+ passed: false,
2148
+ error: error.message
2149
+ };
2150
+ }
2151
+ }
2152
+ /**
2153
+ * Parse Vitest output to extract names of failing tests
2154
+ * Returns array of test names that failed
2155
+ */
2156
+ function parseFailingTestNamesVitest(vitestOutput) {
2157
+ const failingTests = [];
2158
+ // Vitest output patterns for failing tests:
2159
+ // โ— describe block โ€บ test name
2160
+ // or: FAIL path/to/test.ts
2161
+ // โœ• test name (XXms)
2162
+ const lines = vitestOutput.split('\n');
2163
+ for (let i = 0; i < lines.length; i++) {
2164
+ const line = lines[i];
2165
+ // Pattern 1: โ— describe block โ€บ test name
2166
+ const bulletMatch = line.match(/^\s*โ—\s+(.+?)\s+โ€บ\s+(.+?)$/);
2167
+ if (bulletMatch) {
2168
+ const testName = bulletMatch[2].trim();
2169
+ failingTests.push(testName);
2170
+ continue;
2171
+ }
2172
+ // Pattern 2: โœ• test name
2173
+ const xMatch = line.match(/^\s*โœ•\s+(.+?)(?:\s+\(\d+m?s\))?$/);
2174
+ if (xMatch) {
2175
+ const testName = xMatch[1].trim();
2176
+ failingTests.push(testName);
2177
+ continue;
2178
+ }
2179
+ // Pattern 3: FAIL in summary
2180
+ const failMatch = line.match(/^\s*โœ“?\s*(.+?)\s+\(\d+m?s\)$/);
2181
+ if (failMatch && line.includes('โœ•')) {
2182
+ const testName = failMatch[1].trim();
2183
+ failingTests.push(testName);
2184
+ }
2185
+ }
2186
+ // Remove duplicates
2187
+ return [...new Set(failingTests)];
2188
+ }
2189
+ function parseFailingTestNamesJest(jestOutput) {
2190
+ const failingTests = [];
2191
+ // Jest output patterns for failing tests:
2192
+ // โ— describe block โ€บ test name
2193
+ // or: FAIL path/to/test.ts
2194
+ // โœ• test name (XXms)
2195
+ const lines = jestOutput.split('\n');
2196
+ for (let i = 0; i < lines.length; i++) {
2197
+ const line = lines[i];
2198
+ // Pattern 1: โ— describe block โ€บ test name
2199
+ const bulletMatch = line.match(/^\s*โ—\s+(.+?)\s+โ€บ\s+(.+?)$/);
2200
+ if (bulletMatch) {
2201
+ const testName = bulletMatch[2].trim();
2202
+ failingTests.push(testName);
2203
+ continue;
2204
+ }
2205
+ // Pattern 2: โœ• test name
2206
+ const xMatch = line.match(/^\s*โœ•\s+(.+?)(?:\s+\(\d+m?s\))?$/);
2207
+ if (xMatch) {
2208
+ const testName = xMatch[1].trim();
2209
+ failingTests.push(testName);
2210
+ continue;
2211
+ }
2212
+ // Pattern 3: FAIL in summary
2213
+ const failMatch = line.match(/^\s*โœ“?\s*(.+?)\s+\(\d+m?s\)$/);
2214
+ if (failMatch && line.includes('โœ•')) {
2215
+ const testName = failMatch[1].trim();
2216
+ failingTests.push(testName);
2217
+ }
2218
+ }
2219
+ // Remove duplicates
2220
+ return [...new Set(failingTests)];
2221
+ }
1927
2222
  function listDirectory(directoryPath) {
1928
2223
  try {
1929
2224
  if (!fsSync.existsSync(directoryPath)) {
@@ -2004,25 +2299,138 @@ function findFile(filename) {
2004
2299
  return { success: false, error: error.message };
2005
2300
  }
2006
2301
  }
2302
+ /**
2303
+ * Calculate relative import path from one file to one or more target files.
2304
+ * Handles multiple paths for efficiency - AI can get all relative paths in a single call.
2305
+ *
2306
+ * @param fromFile - The file containing the import
2307
+ * @param toFile - Single path or comma-separated list of paths to import
2308
+ * @returns Object with relative paths for all target files
2309
+ */
2007
2310
  function calculateRelativePath(fromFile, toFile) {
2008
2311
  try {
2312
+ // Normalize and validate the from_file path
2313
+ const normalizedFromFile = normalizePath(fromFile);
2314
+ if (!normalizedFromFile) {
2315
+ return { success: false, error: `Invalid from_file path: "${fromFile}"` };
2316
+ }
2317
+ // Parse multiple to_file paths (comma-separated)
2318
+ const toFiles = parseMultiplePaths(toFile);
2319
+ if (toFiles.length === 0) {
2320
+ return { success: false, error: `No valid to_file paths provided: "${toFile}"` };
2321
+ }
2322
+ // Calculate relative path for each target file
2323
+ const results = [];
2324
+ const errors = [];
2325
+ for (const targetFile of toFiles) {
2326
+ const result = calculateSingleRelativePath(normalizedFromFile, targetFile);
2327
+ if (result.success) {
2328
+ results.push(result);
2329
+ }
2330
+ else {
2331
+ errors.push({ file: targetFile, error: result.error });
2332
+ }
2333
+ }
2334
+ // Return appropriate response based on number of files
2335
+ if (toFiles.length === 1) {
2336
+ // Single file: return flat result for backward compatibility
2337
+ return results.length > 0 ? results[0] : errors[0];
2338
+ }
2339
+ else {
2340
+ // Multiple files: return array of results
2341
+ return {
2342
+ success: errors.length === 0,
2343
+ from: normalizedFromFile,
2344
+ totalFiles: toFiles.length,
2345
+ successCount: results.length,
2346
+ errorCount: errors.length,
2347
+ results,
2348
+ errors: errors.length > 0 ? errors : undefined
2349
+ };
2350
+ }
2351
+ }
2352
+ catch (error) {
2353
+ return { success: false, error: error.message };
2354
+ }
2355
+ }
2356
+ /**
2357
+ * Parse comma-separated or space-separated file paths into an array.
2358
+ * Handles various input formats and trims whitespace.
2359
+ */
2360
+ function parseMultiplePaths(input) {
2361
+ if (!input || typeof input !== 'string') {
2362
+ return [];
2363
+ }
2364
+ // Split by comma (primary delimiter)
2365
+ const paths = input
2366
+ .split(',')
2367
+ .map(p => p.trim())
2368
+ .filter(p => p.length > 0)
2369
+ .map(p => normalizePath(p))
2370
+ .filter((p) => p !== null);
2371
+ return paths;
2372
+ }
2373
+ /**
2374
+ * Normalize a file path - handle various edge cases.
2375
+ */
2376
+ function normalizePath(filePath) {
2377
+ if (!filePath || typeof filePath !== 'string') {
2378
+ return null;
2379
+ }
2380
+ let normalized = filePath.trim();
2381
+ // Remove surrounding quotes if present
2382
+ if ((normalized.startsWith('"') && normalized.endsWith('"')) ||
2383
+ (normalized.startsWith("'") && normalized.endsWith("'"))) {
2384
+ normalized = normalized.slice(1, -1);
2385
+ }
2386
+ // Convert backslashes to forward slashes
2387
+ normalized = normalized.replace(/\\/g, '/');
2388
+ // Remove leading ./ if present (will be added back correctly)
2389
+ if (normalized.startsWith('./')) {
2390
+ normalized = normalized.slice(2);
2391
+ }
2392
+ // Remove duplicate slashes
2393
+ normalized = normalized.replace(/\/+/g, '/');
2394
+ // Remove trailing slash
2395
+ if (normalized.endsWith('/')) {
2396
+ normalized = normalized.slice(0, -1);
2397
+ }
2398
+ // Validate it looks like a file path (has at least one character and doesn't start with special chars)
2399
+ if (normalized.length === 0 || /^[\s@#$%^&*]/.test(normalized)) {
2400
+ return null;
2401
+ }
2402
+ return normalized;
2403
+ }
2404
+ /**
2405
+ * Calculate relative path from one file to a single target file.
2406
+ * Core path calculation logic with robust handling.
2407
+ */
2408
+ function calculateSingleRelativePath(fromFile, toFile) {
2409
+ try {
2410
+ // Get the directory of the source file
2009
2411
  const fromDir = path.dirname(fromFile);
2412
+ // Calculate relative path
2010
2413
  let relativePath = path.relative(fromDir, toFile);
2414
+ // Handle empty path (same directory)
2415
+ if (!relativePath) {
2416
+ const basename = path.basename(toFile);
2417
+ relativePath = './' + basename;
2418
+ }
2011
2419
  // Remove .ts, .tsx, .js, .jsx extensions for imports
2012
2420
  relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, '');
2421
+ // Also handle /index suffix (import from './folder' instead of './folder/index')
2422
+ relativePath = relativePath.replace(/\/index$/, '');
2013
2423
  // Ensure it starts with ./ or ../
2014
- if (!relativePath.startsWith('.')) {
2424
+ if (!relativePath.startsWith('.') && !relativePath.startsWith('..')) {
2015
2425
  relativePath = './' + relativePath;
2016
2426
  }
2017
2427
  // Convert backslashes to forward slashes (Windows compatibility)
2018
2428
  relativePath = relativePath.replace(/\\/g, '/');
2019
- // console.log(`calculate_relative_path: JSON.stringify(${JSON.stringify({
2020
- // success: true,
2021
- // from: fromFile,
2022
- // to: toFile,
2023
- // relativePath,
2024
- // importStatement: `import { ... } from '${relativePath}';`
2025
- // })})`)
2429
+ // Handle edge case where path ends up as just '.'
2430
+ if (relativePath === '.') {
2431
+ const basename = path.basename(toFile).replace(/\.(ts|tsx|js|jsx)$/, '');
2432
+ relativePath = './' + basename;
2433
+ }
2026
2434
  return {
2027
2435
  success: true,
2028
2436
  from: fromFile,
@@ -2032,26 +2440,26 @@ function calculateRelativePath(fromFile, toFile) {
2032
2440
  };
2033
2441
  }
2034
2442
  catch (error) {
2035
- return { success: false, error: error.message };
2443
+ return { success: false, file: toFile, error: error.message };
2036
2444
  }
2037
2445
  }
2038
- function reportLegitimateFailure(testFilePath, failingTests, reason, sourceCodeIssue) {
2039
- console.log('\nโš ๏ธ LEGITIMATE TEST FAILURE REPORTED');
2040
- console.log(` Test file: ${testFilePath}`);
2041
- console.log(` Failing tests: ${failingTests.join(', ')}`);
2042
- console.log(` Reason: ${reason}`);
2043
- console.log(` Source code issue: ${sourceCodeIssue}`);
2044
- return {
2045
- success: true,
2046
- acknowledged: true,
2047
- message: 'Legitimate failure reported. Tests have been written correctly but source code has bugs.',
2048
- testFilePath,
2049
- failingTests,
2050
- reason,
2051
- sourceCodeIssue,
2052
- recommendation: 'Fix the source code to resolve these test failures.'
2053
- };
2054
- }
2446
+ // function reportLegitimateFailure(testFilePath: string, failingTests: string[], reason: string, sourceCodeIssue: string): any {
2447
+ // console.log('\nโš ๏ธ LEGITIMATE TEST FAILURE REPORTED');
2448
+ // console.log(` Test file: ${testFilePath}`);
2449
+ // console.log(` Failing tests: ${failingTests.join(', ')}`);
2450
+ // console.log(` Reason: ${reason}`);
2451
+ // console.log(` Source code issue: ${sourceCodeIssue}`);
2452
+ // return {
2453
+ // success: true,
2454
+ // acknowledged: true,
2455
+ // message: 'Legitimate failure reported. Tests have been written correctly but source code has bugs.',
2456
+ // testFilePath,
2457
+ // failingTests,
2458
+ // reason,
2459
+ // sourceCodeIssue,
2460
+ // recommendation: 'Fix the source code to resolve these test failures.'
2461
+ // };
2462
+ // }
2055
2463
  async function deleteLines(filePath, startLine, endLine) {
2056
2464
  try {
2057
2465
  const content = await fs.readFile(filePath, 'utf-8');
@@ -2372,6 +2780,31 @@ async function insertAtPosition(filePath, content, position, afterMarker) {
2372
2780
  return { success: false, error: error.message };
2373
2781
  }
2374
2782
  }
2783
+ /**
2784
+ * Write code review findings to a markdown file in the reviews/ directory
2785
+ */
2786
+ async function writeReview(filePath, reviewContent) {
2787
+ try {
2788
+ // Ensure the reviews directory exists
2789
+ const reviewsDir = path.dirname(filePath);
2790
+ if (!fsSync.existsSync(reviewsDir)) {
2791
+ await fs.mkdir(reviewsDir, { recursive: true });
2792
+ }
2793
+ // Write the review file
2794
+ await fs.writeFile(filePath, reviewContent, 'utf-8');
2795
+ return {
2796
+ success: true,
2797
+ message: `Review written to ${filePath}`,
2798
+ filePath: filePath
2799
+ };
2800
+ }
2801
+ catch (error) {
2802
+ return {
2803
+ success: false,
2804
+ error: `Failed to write review: ${error.message}`
2805
+ };
2806
+ }
2807
+ }
2375
2808
  // User-friendly messages for each tool
2376
2809
  const TOOL_MESSAGES = {
2377
2810
  'read_file': '๐Ÿ“– Reading source file',
@@ -2388,9 +2821,10 @@ const TOOL_MESSAGES = {
2388
2821
  'list_directory': '๐Ÿ“‚ Exploring directory structure',
2389
2822
  'find_file': '๐Ÿ” Locating file in repository',
2390
2823
  'calculate_relative_path': '๐Ÿงญ Calculating import path',
2391
- 'report_legitimate_failure': 'โš ๏ธ Reporting legitimate test failures',
2392
2824
  'search_replace_block': '๐Ÿ”๐Ÿ”„ Searching and replacing code block',
2393
2825
  'insert_at_position': 'โž• Inserting content at position',
2826
+ 'write_review': '๐Ÿ“ Writing code review',
2827
+ 'search_codebase': '๐Ÿ”Ž Searching codebase',
2394
2828
  };
2395
2829
  // Tool execution router
2396
2830
  async function executeTool(toolName, args) {
@@ -2410,7 +2844,16 @@ async function executeTool(toolName, args) {
2410
2844
  else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
2411
2845
  friendlyMessage = `๐Ÿ“– Reading lines ${args.start_line}-${args.end_line}`;
2412
2846
  }
2413
- 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
+ }
2414
2857
  let result;
2415
2858
  try {
2416
2859
  switch (toolName) {
@@ -2421,21 +2864,21 @@ async function executeTool(toolName, args) {
2421
2864
  result = await readFileLines(args.file_path, args.start_line, args.end_line);
2422
2865
  break;
2423
2866
  case 'analyze_file_ast':
2424
- // Try cache first if indexer is available
2425
- if (globalIndexer) {
2867
+ // Try cache first if indexer is available (only for non-filtered requests)
2868
+ if (globalIndexer && !args.function_name) {
2426
2869
  // Check if file has been modified since caching
2427
2870
  if (globalIndexer.isFileStale(args.file_path)) {
2428
- console.log(' ๐Ÿ”„ File modified, re-analyzing...');
2429
- result = analyzeFileAST(args.file_path);
2430
- // Update cache with new analysis
2431
- if (result.success) {
2871
+ globalSpinner.text = '๐Ÿ”„ File modified, re-analyzing...';
2872
+ result = analyzeFileAST(args.file_path, args.function_name);
2873
+ // Update cache with new analysis (only if not filtered)
2874
+ if (result.success && !args.function_name) {
2432
2875
  await globalIndexer.updateIndex([args.file_path], analyzeFileAST);
2433
2876
  }
2434
2877
  break;
2435
2878
  }
2436
2879
  const cached = globalIndexer.getFileAnalysis(args.file_path);
2437
2880
  if (cached) {
2438
- console.log(' ๐Ÿ“ฆ Using cached analysis');
2881
+ globalSpinner.text = '๐Ÿ“ฆ Using cached analysis';
2439
2882
  result = {
2440
2883
  success: true,
2441
2884
  analysis: cached,
@@ -2443,14 +2886,14 @@ async function executeTool(toolName, args) {
2443
2886
  functionCount: cached.functions.length,
2444
2887
  classCount: cached.classes.length,
2445
2888
  exportCount: cached.exports.length,
2446
- typeCount: cached.types.length
2889
+ typeCount: cached.types.length,
2447
2890
  }
2448
2891
  };
2449
2892
  break;
2450
2893
  }
2451
2894
  }
2452
- // Fall back to actual analysis
2453
- result = analyzeFileAST(args.file_path);
2895
+ // Fall back to actual analysis (with optional filtering)
2896
+ result = analyzeFileAST(args.file_path, args.function_name);
2454
2897
  break;
2455
2898
  case 'get_function_ast':
2456
2899
  result = getFunctionAST(args.file_path, args.function_name);
@@ -2471,10 +2914,29 @@ async function executeTool(toolName, args) {
2471
2914
  result = resolveImportPath(args.from_file, args.import_path);
2472
2915
  break;
2473
2916
  case 'upsert_function_tests':
2917
+ // CRITICAL VALIDATION: Prevent AI from creating per-function test files
2918
+ if (EXPECTED_TEST_FILE_PATH && args.test_file_path !== EXPECTED_TEST_FILE_PATH) {
2919
+ // Normalize paths for comparison (handle different path separators)
2920
+ const normalizedExpected = path.normalize(EXPECTED_TEST_FILE_PATH).replace(/\\/g, '/');
2921
+ const normalizedProvided = path.normalize(args.test_file_path).replace(/\\/g, '/');
2922
+ if (normalizedExpected !== normalizedProvided) {
2923
+ if (globalSpinner)
2924
+ globalSpinner.stop();
2925
+ console.log(`\nโš ๏ธ BLOCKED: AI attempted separate test file, enforcing single file policy`);
2926
+ globalSpinner = ora(friendlyMessage).start();
2927
+ // Override the test file path to the expected one
2928
+ args.test_file_path = EXPECTED_TEST_FILE_PATH;
2929
+ }
2930
+ }
2474
2931
  result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
2475
2932
  break;
2476
2933
  case 'run_tests':
2477
- 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
+ }
2478
2940
  break;
2479
2941
  case 'list_directory':
2480
2942
  result = listDirectory(args.directory_path);
@@ -2485,15 +2947,18 @@ async function executeTool(toolName, args) {
2485
2947
  case 'calculate_relative_path':
2486
2948
  result = calculateRelativePath(args.from_file, args.to_file);
2487
2949
  break;
2488
- case 'report_legitimate_failure':
2489
- result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
2490
- break;
2491
2950
  case 'search_replace_block':
2492
2951
  result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
2493
2952
  break;
2494
2953
  case 'insert_at_position':
2495
2954
  result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
2496
2955
  break;
2956
+ case 'write_review':
2957
+ result = await writeReview(args.file_path, args.review_content);
2958
+ break;
2959
+ case 'search_codebase':
2960
+ result = searchCodebase(args.pattern, args.file_extension, args.max_results, args.files_only);
2961
+ break;
2497
2962
  default:
2498
2963
  result = { success: false, error: `Unknown tool: ${toolName}` };
2499
2964
  }
@@ -2501,16 +2966,139 @@ async function executeTool(toolName, args) {
2501
2966
  catch (error) {
2502
2967
  result = { success: false, error: error.message, stack: error.stack };
2503
2968
  }
2504
- // 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
2505
2971
  if (result.success) {
2506
- // console.log('result', JSON.stringify(result, null, 2));
2507
- console.log(` โœ… Done`);
2972
+ // Small delay so users see the operation briefly
2973
+ await new Promise(resolve => setTimeout(resolve, 150));
2508
2974
  }
2509
2975
  else if (result.error) {
2510
- 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));
2511
2983
  }
2512
2984
  return result;
2513
2985
  }
2986
+ // Search codebase utility
2987
+ function searchCodebase(pattern, fileExtension, maxResults = 20, filesOnly = false) {
2988
+ try {
2989
+ const { execSync } = require('child_process');
2990
+ // Cap maxResults to prevent token explosion
2991
+ const limit = Math.min(maxResults || 20, 50);
2992
+ // Build grep command
2993
+ let cmd = `grep -rn`;
2994
+ // If files_only, just list files
2995
+ if (filesOnly) {
2996
+ cmd = `grep -rl`; // -l for files with matches only
2997
+ }
2998
+ else {
2999
+ cmd += ` -C 1`; // Only 1 line of context to save tokens
3000
+ }
3001
+ // Add file extension filter if provided
3002
+ if (fileExtension) {
3003
+ const ext = fileExtension.startsWith('.') ? fileExtension : `.${fileExtension}`;
3004
+ cmd += ` --include="*${ext}"`;
3005
+ }
3006
+ // Exclude common directories
3007
+ const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage', '.codeguard-cache', '.jest-cache', '.testgen-cache', '.vitest', 'reviews'];
3008
+ for (const dir of excludeDirs) {
3009
+ cmd += ` --exclude-dir=${dir}`;
3010
+ }
3011
+ // Add pattern and search path
3012
+ cmd += ` "${pattern}" .`;
3013
+ // Limit output lines to prevent token overflow
3014
+ cmd += ` | head -n ${limit * 3}`; // 3 lines per match (context + match)
3015
+ // Execute grep
3016
+ const output = execSync(cmd, {
3017
+ encoding: 'utf-8',
3018
+ cwd: process.cwd(),
3019
+ maxBuffer: 5 * 1024 * 1024, // 1MB max (reduced from 10MB)
3020
+ stdio: ['pipe', 'pipe', 'ignore'] // Ignore stderr
3021
+ });
3022
+ if (filesOnly) {
3023
+ // Return just file list
3024
+ const files = output.trim().split('\n').filter(f => f.length > 0).slice(0, limit);
3025
+ return {
3026
+ success: true,
3027
+ pattern,
3028
+ filesOnly: true,
3029
+ totalFiles: files.length,
3030
+ files: files.map(f => ({ file: f })),
3031
+ message: `Found ${files.length} file(s) matching pattern`,
3032
+ hint: 'Use search_codebase with files_only=false on specific files to see content'
3033
+ };
3034
+ }
3035
+ // Parse results with token awareness
3036
+ const lines = output.trim().split('\n').slice(0, limit);
3037
+ const results = [];
3038
+ const fileSet = new Set();
3039
+ let currentFile = '';
3040
+ let currentMatches = [];
3041
+ for (const line of lines) {
3042
+ if (line.includes(':')) {
3043
+ const match = line.match(/^([^:]+):(\d+):(.*)/);
3044
+ if (match) {
3045
+ const [, file, lineNum, content] = match;
3046
+ fileSet.add(file);
3047
+ if (file !== currentFile) {
3048
+ if (currentFile && currentMatches.length > 0) {
3049
+ results.push({
3050
+ file: currentFile,
3051
+ matchCount: currentMatches.length,
3052
+ matches: currentMatches.slice(0, 5) // Max 5 matches per file
3053
+ });
3054
+ }
3055
+ currentFile = file;
3056
+ currentMatches = [];
3057
+ }
3058
+ // Truncate long lines to save tokens
3059
+ const truncatedContent = content.length > 150 ? content.substring(0, 150) + '...' : content;
3060
+ currentMatches.push(`Line ${lineNum}: ${truncatedContent}`);
3061
+ }
3062
+ }
3063
+ }
3064
+ // Add last file
3065
+ if (currentFile && currentMatches.length > 0) {
3066
+ results.push({
3067
+ file: currentFile,
3068
+ matchCount: currentMatches.length,
3069
+ matches: currentMatches.slice(0, 5)
3070
+ });
3071
+ }
3072
+ const totalMatches = results.reduce((sum, r) => sum + r.matchCount, 0);
3073
+ return {
3074
+ success: true,
3075
+ pattern,
3076
+ totalFiles: fileSet.size,
3077
+ totalMatches,
3078
+ files: results.slice(0, 10), // Max 10 files in response
3079
+ truncated: results.length > 10 || lines.length >= limit,
3080
+ message: `Found ${fileSet.size} file(s) with ${totalMatches} match(es)${results.length > 10 ? ' (showing first 10 files)' : ''}`,
3081
+ hint: results.length > 10 ? 'Too many results. Try adding file_extension filter or more specific pattern.' : undefined
3082
+ };
3083
+ }
3084
+ catch (error) {
3085
+ // Grep returns exit code 1 when no matches found
3086
+ if (error.status === 1 || error.message?.includes('No such file')) {
3087
+ return {
3088
+ success: true,
3089
+ pattern,
3090
+ totalFiles: 0,
3091
+ totalMatches: 0,
3092
+ files: [],
3093
+ message: 'No matches found'
3094
+ };
3095
+ }
3096
+ return {
3097
+ success: false,
3098
+ error: `Search failed: ${error.message}`
3099
+ };
3100
+ }
3101
+ }
2514
3102
  // File system utilities
2515
3103
  async function listFilesRecursive(dir, fileList = []) {
2516
3104
  const files = await fs.readdir(dir);
@@ -2573,7 +3161,7 @@ function getTestFilePath(sourceFile) {
2573
3161
  // No subdirectory: testDir/filename.test.ts
2574
3162
  testPath = path.join(CONFIG.testDir, testFileName);
2575
3163
  }
2576
- console.log(` ๐Ÿ“ Test file path: ${testPath}`);
3164
+ // console.log(` ๐Ÿ“ Test file path: ${testPath}`);
2577
3165
  return testPath;
2578
3166
  }
2579
3167
  // AI Provider implementations
@@ -2749,603 +3337,34 @@ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
2749
3337
  }
2750
3338
  // Main conversation loop
2751
3339
  async function generateTests(sourceFile) {
2752
- console.log(`\n๐Ÿ“ Generating tests for: ${sourceFile}\n`);
2753
- // Check file size and switch to function-by-function generation if large
2754
- try {
2755
- const fileContent = fsSync.readFileSync(sourceFile, 'utf-8');
2756
- const lineCount = fileContent.split('\n').length;
2757
- console.log(`๐Ÿ“Š Source file has ${lineCount} lines`);
2758
- if (lineCount > 200) {
2759
- console.log(`\nโšก File has more than 200 lines! Switching to function-by-function generation...\n`);
2760
- // Analyze file to get all functions
2761
- const result = analyzeFileAST(sourceFile);
2762
- if (result && result.success && result.analysis && result.analysis.functions && result.analysis.functions.length > 0) {
2763
- // Filter to only EXPORTED functions (public API)
2764
- const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
2765
- const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
2766
- if (functionNames.length === 0) {
2767
- console.log('โš ๏ธ No exported functions found in file. Falling back to regular generation.');
2768
- // Fall through to regular generation
2769
- }
2770
- else {
2771
- const totalFunctions = result.analysis.functions.length;
2772
- const internalFunctions = totalFunctions - exportedFunctions.length;
2773
- console.log(`โœ… Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
2774
- if (internalFunctions > 0) {
2775
- console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
2776
- }
2777
- // Use function-by-function generation
2778
- try {
2779
- return await generateTestsForFunctions(sourceFile, functionNames);
2780
- }
2781
- catch (funcError) {
2782
- // CRITICAL: Check if test file already exists with content
2783
- const testFilePath = getTestFilePath(sourceFile);
2784
- if (fsSync.existsSync(testFilePath)) {
2785
- const existingContent = fsSync.readFileSync(testFilePath, 'utf-8');
2786
- const hasExistingTests = existingContent.includes('describe(') || existingContent.includes('test(');
2787
- if (hasExistingTests) {
2788
- console.error('\nโŒ CRITICAL: Function-by-function generation failed, but test file already has tests!');
2789
- console.error(' Cannot fall back to file-wise generation as it would OVERWRITE existing tests.');
2790
- console.error(` Error: ${funcError.message}`);
2791
- console.error('\n Options:');
2792
- console.error(' 1. Fix the issue manually in the test file');
2793
- console.error(' 2. Delete the test file and regenerate from scratch');
2794
- console.error(' 3. Run function-wise generation again for remaining functions');
2795
- throw new Error(`Function-by-function generation failed with existing tests. Manual intervention required. Original error: ${funcError.message}`);
2796
- }
2797
- }
2798
- // No existing tests, safe to fall back
2799
- console.log('โš ๏ธ Function-by-function generation failed, but no existing tests found. Falling back to file-wise generation.');
2800
- throw funcError; // Re-throw to be caught by outer try-catch
2801
- }
2802
- }
2803
- }
2804
- else {
2805
- console.log('โš ๏ธ No functions found in file. Falling back to regular generation.');
2806
- if (result && !result.success) {
2807
- console.log(` Analysis error: ${result.error}`);
2808
- }
2809
- }
2810
- }
3340
+ // console.log(`\n๐Ÿ“ Generating tests for: ${sourceFile}\n`);
3341
+ // Analyze file to get all functions (with retry)
3342
+ let result = analyzeFileAST(sourceFile);
3343
+ // Retry once if failed
3344
+ if (!result.success) {
3345
+ // console.log('โš ๏ธ AST analysis failed, retrying once...');
3346
+ result = analyzeFileAST(sourceFile);
2811
3347
  }
2812
- catch (error) {
2813
- // Check if this is the "existing tests" protection error
2814
- if (error.message && error.message.includes('Manual intervention required')) {
2815
- // This is a critical error - don't proceed with generation
2816
- console.error(`\nโŒ Aborting: ${error.message}`);
2817
- throw error; // Re-throw to stop execution
2818
- }
2819
- console.log(`โš ๏ธ Could not check file size: ${error}. Proceeding with regular generation.`);
2820
- // Falls through to regular file-wise generation below
3348
+ // If still failed, throw error
3349
+ if (!result.success || !result.analysis || !result.analysis.functions) {
3350
+ throw new Error(`File analysis failed. Unable to extract functions from file. Error: ${result.error || 'unknown'}`);
2821
3351
  }
2822
- const testFilePath = getTestFilePath(sourceFile);
2823
- const messages = [
2824
- {
2825
- role: 'user',
2826
- content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests including edge cases for a TypeScript file.
2827
-
2828
- Source file: ${sourceFile}
2829
- Test file path: ${testFilePath}
2830
-
2831
- IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
2832
-
2833
- Your task (you MUST complete ALL steps):
2834
- 1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
2835
- - This provides metadata about all code structures without loading full file content
2836
- - CRITICAL: You have only 50 iterations to complete this task, so make sure you are using the tools efficiently.
2837
- - Do not over explore, use the tools to get the information you need and start generating tests.
2838
- 2. Use get_imports_ast tool to understand all dependencies
2839
- 3. For each dependency, use find_file(filePath) to locate the file and calculate_relative_path to get correct import paths for the test file
2840
- 4. For complex functions, use get_function_ast tool to get detailed information
2841
- - Returns complete function code WITH JSDoc comments
2842
- - Includes calledFunctions and calledMethods lists showing what the function calls
2843
- - Use this to fetch related helper functions if needed
2844
- - [CRITICAL]: If a function calls other functions from other files, use find_file + get_function_ast tools to locate them and check if they need to mocked, since they can be making api calls to external services.
2845
- 5. Use get_function_ast to get detailed information about the functions.
2846
- 6. For large test files (>5K lines), use get_file_preamble to see existing imports/mocks/setup blocks
2847
- - Automatically included when reading large test files
2848
- - Use before adding new test cases to avoid duplicate mocks/imports
2849
- - Particularly useful when updating existing test files with upsert_function_tests
2850
- - Captures complete multi-line mocks including complex jest.mock() statements
2851
- 7. For classes, use get_class_methods tool to extract all methods
2852
- 8. Use get_type_definitions tool to understand TypeScript types and interfaces
2853
- 9. Generate comprehensive Jest unit tests with:
2854
- - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors. Ensure tests fully mock the config module with all expected properties.
2855
- - If required:
2856
- - Mock database modules like '../database' or '../database/index' with virtual:true.
2857
- - Mock models, and any modules that access config or the database with virtual:true.
2858
- - Mock config properly with virtual:true.
2859
- - Mock isEmpty from lodash to return the expected values with virtual:true.
2860
- - Axios should be mocked with virtual:true.
2861
- - Use jest.mock() calls at the TOP of the file before any imports
2862
- - [CRITICAL]: Virtual modules should only be used for db/config/models/services/index/axios/routes files. You should not use virtual:true for any other files or helpers that exist in the source code. The actual helpers should never be mocked with virtual:true.
2863
- 9. REQUIRED: Write tests using upsert_function_tests tool for EACH function with REAL test code (NOT placeholders!)
2864
- - Call upsert_function_tests once for EACH exported function
2865
- - Ensure comprehensive mocks are included in the first function's test to set up the file
2866
- - DO NOT use ANY placeholder comments like:
2867
- * "// Mock setup", "// Assertions", "// Call function"
2868
- * "// Further tests...", "// Additional tests..."
2869
- * "// Similarly, write tests for..."
2870
- * "// Add more tests...", "// TODO", "// ..."
2871
- - Write ACTUAL working test code with real mocks, real assertions, real function calls
2872
- - Every test MUST have [MANDATORY]:
2873
- * Real setup code (mock functions, create test data)
2874
- * Real execution (call the function being tested)
2875
- * Real expect() assertions (at least one per test)
2876
- * null/undefined handling tests for all API responses
2877
- * Happy path scenarios
2878
- * Edge cases (null, undefined, empty arrays, etc.)
2879
- * Error conditions
2880
- * Async behavior (if applicable)
2881
- - Proper TypeScript types
2882
- - Write tests for EVERY exported function (minimum 3-5 tests per function)
2883
- - If source has 4 functions, test file MUST have 4 describe blocks with actual tests
2884
- - Example of COMPLETE test structure:
2885
- * Setup: Create mocks and test data
2886
- * Execute: Call the function being tested
2887
- * Assert: Use expect() to verify results
2888
- 10. REQUIRED: Run the tests using run_tests tool
2889
- 11. REQUIRED: If tests fail with import errors:
2890
- - Use find_file(filePath) tool to locate the file and calculate_relative_path to get correct import paths for the test file
2891
- - Use calculate_relative_path tool to get correct import path
2892
- - โœ… PRIMARY METHOD: Use search_replace_block to fix imports:
2893
- * Include 3-5 lines of context around the import to change
2894
- * Example: search_replace_block({
2895
- search: "import { oldImport } from './old-path';\nimport { other } from './other';",
2896
- replace: "import { oldImport, newImport } from './correct-path';\nimport { other } from './other';"
2897
- })
2898
- - ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding new imports:
2899
- * insert_at_position({ position: 'after_imports', content: "import { newImport } from './path';" })
2900
- - โš ๏ธ AVOID: Line-based tools (deprecated, fragile)
2901
- 12. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
2902
-
2903
- FIXABLE ERRORS (you should fix these):
2904
- - Wrong import paths
2905
- - Missing mocks
2906
- - Incorrect mock implementations
2907
- - Wrong assertions or test logic
2908
- - TypeScript compilation errors (syntax errors, bracket mismatches)
2909
- - Missing test setup/teardown
2910
- - Cannot read properties of undefined
2911
- - Test case failed to run. Use read_file_lines tool to read the specific problematic section and fix the issue.
2912
-
2913
- ๐Ÿ’ก TIP: For syntax errors or bracket mismatches:
2914
- - Use read_file to see the file content (it includes line numbers)
2915
- - Use search_replace_block to fix the problematic section
2916
- - Include 3-5 lines of context around the error to make search unique
2917
- - Example: search_replace_block({
2918
- search: "line before error\nproblematic code with syntax error\nline after",
2919
- replace: "line before error\ncorrected code\nline after"
2920
- })
2921
-
2922
- LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
2923
- - Function returns wrong type (e.g., undefined instead of object)
2924
- - Missing null/undefined checks in source code
2925
- - Logic errors in source code
2926
- - Unhandled promise rejections in source code
2927
-
2928
- 13. If errors are FIXABLE (AFTER test file is written):
2929
- - โœ… PRIMARY METHOD: Use search_replace_block (RECOMMENDED):
2930
- * Find the problematic code section
2931
- * Include 3-5 lines of context before/after to make search unique
2932
- * Replace with corrected version
2933
- * Example: search_replace_block({
2934
- file_path: "test.ts",
2935
- search: "const mock = jest.fn();\ntest('old test', () => {\n mock();",
2936
- replace: "const mock = jest.fn().mockResolvedValue({ data: 'test' });\ntest('fixed test', () => {\n mock();"
2937
- })
2938
- * Handles whitespace/indentation differences automatically!
2939
- - ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding mocks/imports at top:
2940
- * insert_at_position({ position: 'after_imports', content: "jest.mock('../database');" })
2941
- - โš ๏ธ AVOID: Line-based tools (deprecated) - they are fragile and prone to errors
2942
- - Then retry running tests
2943
- 14. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
2944
- - Provide failing test names, reason, and source code issue description
2945
- - The test file will be kept as-is with legitimate failing tests
2946
- - You are not allowed to call this tool for error - Test suite failed to run. You must ensure that test cases get executed. Fix any syntax or linting issues in the test file.
2947
- 15. REQUIRED: Repeat steps 10-14 until tests pass OR legitimate failures are reported
2948
- 16. REQUIRED: Ensure all functions are tested in the test file.
2949
- 17. CRITICAL: config and database modules must be mocked
2950
-
2951
- 18. Some known issues when running tests with fixes:
2952
- Route loading issue: Importing the controller triggered route setup because axios-helper imports from index.ts, which loads all routes. Routes referenced functions that weren't available during test initialization.
2953
- - Solution: Mocked index.ts to export only whitelistDomainsForHeaders without executing route setup code.
2954
- Axios mock missing required properties: The axios mock didn't include properties needed by axios-retry (like interceptors).
2955
- - Solution: Created a createMockAxiosInstance function that returns a mock axios instance with interceptors, defaults, and HTTP methods.
2956
- axios-retry not mocked: axios-retry was trying to modify axios instances during module initialization.
2957
- - Solution: Added a mock for axios-retry to prevent it from executing during tests.
2958
- Routes file execution: The routes file was being executed when the controller was imported.
2959
- - Solution: Mocked the routes file to return a simple Express router without executing route definitions.
2960
-
2961
- CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
2962
-
2963
- 19. [ALWAYS FOLLOW] Write Jest test cases following these MANDATORY patterns to prevent mock pollution:
2964
-
2965
- SECTION 1: Top-Level Mock Setup
2966
- Declare ALL jest.mock() calls at the top of the file, outside any describe blocks
2967
- Mock EVERY function and module the controller/function uses
2968
- Load the controller/module under test ONCE in beforeAll()
2969
- [MANDATORY] Always use calculate_relative_path tool to get the correct import path for the module to be used in jest.mock() calls.
2970
-
2971
- SECTION 2: Module References in Describe Blocks (CRITICAL)
2972
- Declare ALL module references as let variables at the top of each describe block
2973
- NEVER use const for module requires inside describe blocks
2974
- Re-assign these modules inside beforeEach AFTER jest.clearAllMocks()
2975
- This ensures each test gets fresh module references with clean mocks
2976
-
2977
- SECTION 3: BeforeEach Pattern (MANDATORY)
2978
- Pattern Structure:
2979
-
2980
- Declare let variables for all modules at top of describe block
2981
- Load controller once in beforeAll
2982
- In beforeEach: clear mocks first, then re-assign all modules, then reset all mock implementations
2983
- In afterEach: restore all mocks for extra safety
2984
- In individual tests: only override specific mocks needed for that test
2985
-
2986
- Example Pattern:
2987
-
2988
- Declare: let controller, let helperModule, let responseHelper, let otherDependency
2989
- beforeAll: controller = require path to controller
2990
- beforeEach step 1: jest.clearAllMocks()
2991
- beforeEach step 2: Re-assign all modules with require statements
2992
- beforeEach step 3: Set default mock implementations for all mocked functions
2993
- afterEach: jest.restoreAllMocks()
2994
- In tests: Override only what changes for that specific test case
2995
-
2996
- SECTION 4: What to NEVER Do
2997
-
2998
- Never use const for module requires inside describe blocks
2999
- Never rely on top-level module requires for mocking within tests
3000
- Never share module references across multiple describe blocks
3001
- Never skip re-assigning modules in beforeEach
3002
- Never mutate module exports directly, always use mockImplementation instead
3003
- Never forget to clear mocks before getting fresh references
3004
-
3005
- SECTION 5: Key Principles
3006
-
3007
- Isolation: Each test has its own mock instances via fresh requires
3008
- Cleanup: Always clear and restore mocks between tests
3009
- Explicit: Make all dependencies explicit in beforeEach
3010
- Fresh References: Re-require modules after clearing to get clean mocks
3011
- Default Behaviors: Set up sensible defaults in beforeEach, override in individual tests
3012
-
3013
- SECTION 6: Verification Steps
3014
-
3015
- Run tests multiple times with runInBand flag
3016
- Run tests in random order with randomize flag
3017
- Tests should pass consistently regardless of execution order
3018
- Each test should be completely independent
3019
-
3020
- SECTION 7: Critical Reminders
3021
-
3022
- Always add missing mock initializations in relevant beforeEach blocks
3023
- Ensure all mocked functions have default behaviors set in beforeEach
3024
- Re-require modules after jest.clearAllMocks() to get fresh mock references
3025
- Use let not const for all module references inside describe blocks
3026
- Load the actual controller or module under test only once in beforeAll
3027
-
3028
- Apply these patterns to ALL test files to ensure zero mock pollution between test suites and individual test cases.
3029
-
3030
- START NOW by calling the analyze_file_ast tool with the source file path.`
3031
- }
3032
- ];
3033
- let iterations = 0;
3034
- const maxIterations = 100;
3035
- let testFileWritten = false;
3036
- let allToolResults = [];
3037
- let legitimateFailureReported = false;
3038
- let lastTestError = '';
3039
- let sameErrorCount = 0;
3040
- while (iterations < maxIterations) {
3041
- iterations++;
3042
- if (iterations === 1) {
3043
- console.log(`\n๐Ÿค– AI is analyzing your code...`);
3044
- }
3045
- else if (iterations % 5 === 0) {
3046
- console.log(`\n๐Ÿค– AI is still working (step ${iterations})...`);
3047
- }
3048
- const response = await callAI(messages, TOOLS);
3049
- if (response.content) {
3050
- const content = response.content; // Store for TypeScript
3051
- // Only show AI message if it's making excuses (for debugging), otherwise skip
3052
- // Detect if AI is making excuses instead of using tools
3053
- const excusePatterns = [
3054
- /unable to proceed/i,
3055
- /cannot directly/i,
3056
- /constrained by/i,
3057
- /simulated environment/i,
3058
- /limited to providing/i,
3059
- /beyond my capabilities/i,
3060
- /can't execute/i
3061
- ];
3062
- const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
3063
- if (isMakingExcuses) {
3064
- console.log('\nโš ๏ธ AI is making excuses! Forcing it to use tools...');
3065
- // Don't add the excuse to conversation, override with command
3066
- messages.push({
3067
- role: 'user',
3068
- content: 'STOP making excuses! You CAN use the tools. Use search_replace_block or insert_at_position NOW to fix the test file. Add proper mocks to prevent database initialization errors.'
3069
- });
3070
- continue;
3071
- }
3072
- messages.push({ role: 'assistant', content });
3073
- }
3074
- if (!response.toolCalls || response.toolCalls.length === 0) {
3075
- // Don't stop unless tests actually passed or legitimate failure reported
3076
- const lastTestRun = allToolResults[allToolResults.length - 1];
3077
- const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
3078
- if (legitimateFailureReported) {
3079
- console.log('\nโœ… Test generation complete (with legitimate failures reported)');
3080
- break;
3081
- }
3082
- if (testFileWritten && testsActuallyPassed) {
3083
- console.log('\nโœ… Test generation complete!');
3084
- break;
3085
- }
3086
- // If no tools called, prompt to continue with specific action
3087
- console.log('\nโš ๏ธ No tool calls. Prompting AI to continue...');
3088
- if (!testFileWritten) {
3089
- messages.push({
3090
- role: 'user',
3091
- content: 'You have not written the test file yet. Use upsert_function_tests tool NOW with complete test code (not placeholders) for each function.'
3092
- });
3093
- }
3094
- else {
3095
- messages.push({
3096
- role: 'user',
3097
- content: `STOP talking and USE TOOLS!
3098
-
3099
- If tests are failing:
3100
- - FIXABLE errors (imports, mocks, assertions):
3101
- โœ… PRIMARY: Use search_replace_block with context (handles whitespace automatically!)
3102
- ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding imports/mocks
3103
- โš ๏ธ AVOID: Line-based tools (deprecated, fragile)
3104
- run_tests tool to run the tests and check if the tests pass.
3105
- - LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
3106
-
3107
- Example: search_replace_block({ search: "old code with context...", replace: "new fixed code..." })`
3108
- });
3109
- }
3110
- continue;
3111
- }
3112
- // Execute all tool calls
3113
- const toolResults = [];
3114
- for (const toolCall of response.toolCalls) {
3115
- const result = await executeTool(toolCall.name, toolCall.input);
3116
- const toolResult = {
3117
- id: toolCall.id,
3118
- name: toolCall.name,
3119
- result
3120
- };
3121
- toolResults.push(toolResult);
3122
- allToolResults.push(toolResult);
3123
- // Track if legitimate failure was reported
3124
- if (toolCall.name === 'report_legitimate_failure' && result.success) {
3125
- legitimateFailureReported = true;
3126
- console.log('\nโœ… Legitimate failure acknowledged. Stopping test fixes.');
3127
- console.log(` Recommendation: ${result.recommendation}`);
3128
- }
3129
- // Track if test file was written
3130
- if (toolCall.name === 'upsert_function_tests') {
3131
- if (result.success) {
3132
- testFileWritten = true;
3133
- console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
3134
- }
3135
- }
3136
- // Detect syntax errors from validation
3137
- if (result.syntaxError && result.location) {
3138
- console.log(`\nโŒ Syntax error introduced at line ${result.location.line}!`);
3139
- messages.push({
3140
- role: 'user',
3141
- content: `๐Ÿšจ SYNTAX ERROR DETECTED at line ${result.location.line}:${result.location.column}
3142
-
3143
- ${result.error}
3144
-
3145
- ๐Ÿ’ก ${result.suggestion}
3146
-
3147
- Your last modification created invalid syntax and was ROLLED BACK automatically.
3148
-
3149
- To fix this:
3150
- 1. Use read_file to see the current file content (includes line numbers)
3151
- 2. Find the section you need to modify around line ${result.location.line}
3152
- 3. Use search_replace_block with correct syntax:
3153
- - Include 3-5 lines of context around the target
3154
- - Ensure your replacement has valid syntax (matching brackets, quotes, etc.)
3155
- - Double-check for missing semicolons, commas, or closing brackets
3156
-
3157
- Example:
3158
- search_replace_block({
3159
- file_path: "${toolCall.input.file_path || toolCall.input.test_file_path || 'test file'}",
3160
- search: "valid context from file\nline with issue\nmore context",
3161
- replace: "valid context from file\nCORRECTED line with proper syntax\nmore context"
3162
- })
3163
-
3164
- Start NOW by reading the file around line ${result.location.line}!`
3165
- });
3166
- }
3167
- // Detect repeated errors (suggests legitimate failure)
3168
- if (toolCall.name === 'run_tests' && !result.success) {
3169
- const errorOutput = result.output || result.error || '';
3170
- const currentError = errorOutput.substring(0, 300); // First 300 chars as signature
3171
- if (currentError === lastTestError) {
3172
- sameErrorCount++;
3173
- console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
3174
- if (sameErrorCount >= 3) {
3175
- console.log('\n๐Ÿšจ Same error repeated 3+ times! ');
3176
- messages.push({
3177
- role: 'user',
3178
- content: `The same test error has occurred ${sameErrorCount} times in a row!
3179
-
3180
-
3181
- Analyze the error and determine:
3182
- 1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
3183
- 2. Use available tools file read_file_lines to read the current state of file.
3184
- 2. Or is this a LEGITIMATE source code bug?
3185
-
3186
- If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
3187
- If FIXABLE: Make one more attempt to fix it.`
3188
- });
3189
- }
3190
- }
3191
- else {
3192
- lastTestError = currentError;
3193
- sameErrorCount = 1;
3194
- }
3195
- }
3196
- // Detect import path errors
3197
- if (toolCall.name === 'run_tests' && !result.success) {
3198
- const errorOutput = result.output || result.error || '';
3199
- // Check for module not found errors
3200
- const moduleNotFoundMatch = errorOutput.match(/Cannot find module ['"]([^'"]+)['"]/);
3201
- const tsModuleErrorMatch = errorOutput.match(/TS2307.*Cannot find module ['"]([^'"]+)['"]/);
3202
- if (moduleNotFoundMatch || tsModuleErrorMatch) {
3203
- const missingModule = moduleNotFoundMatch?.[1] || tsModuleErrorMatch?.[1];
3204
- console.log(`\n๐Ÿ” Import error detected: Cannot find module "${missingModule}"`);
3205
- // Extract filename from the path
3206
- const filename = missingModule?.split('/').pop();
3207
- messages.push({
3208
- role: 'user',
3209
- content: `Import path error detected! Module not found: "${missingModule}"
3210
-
3211
- โœ… FIX WITH SEARCH-REPLACE:
3212
-
3213
- Step 1: find_file tool to search for "${filename}" in the repository
3214
- Step 2: calculate_relative_path tool to get correct import path
3215
- Step 3: Fix using search_replace_block:
3216
- a) Include the broken import line + 2-3 surrounding lines for context
3217
- b) Replace with corrected import using the right path
3218
- c) The tool handles whitespace/indentation automatically!
3219
-
3220
- Example workflow:
3221
- 1. find_file({ filename: "${filename}.ts" })
3222
- 2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
3223
- 3. search_replace_block({
3224
- file_path: "${testFilePath}",
3225
- search: "import { something } from './other';\nimport { broken } from '${missingModule}';\nimport { another } from './path';",
3226
- replace: "import { something } from './other';\nimport { fixed } from './correct-path';\nimport { another } from './path';"
3227
- })
3228
-
3229
- Start NOW with find_file!`
3230
- });
3231
- }
3232
- // Check for database initialization errors
3233
- const isDatabaseError = /Cannot read properties of undefined.*reading|database|config|SSL|CA|HOST/i.test(errorOutput);
3234
- if (isDatabaseError) {
3235
- console.log('\n๐Ÿ” Database initialization error detected! Need to add mocks...');
3236
- messages.push({
3237
- role: 'user',
3238
- content: `The test is failing because the source file imports modules that initialize database connections.
3239
-
3240
- โœ… FIX WITH SEARCH-REPLACE:
3241
-
3242
- Option 1 (Recommended): Use insert_at_position to add mocks at beginning:
3243
- insert_at_position({
3244
- file_path: "${testFilePath}",
3245
- position: "beginning",
3246
- content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../database/index', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\n"
3247
- })
3248
-
3249
- Option 2: If file already has some mocks, use search_replace_block to add more:
3250
- search_replace_block({
3251
- file_path: "${testFilePath}",
3252
- search: "jest.mock('./existing-mock');\n\nimport { something }",
3253
- replace: "jest.mock('./existing-mock');\njest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n\nimport { something }"
3254
- })
3255
-
3256
- โš ๏ธ Mocks MUST be at the TOP before any imports!
3257
-
3258
- Start NOW with insert_at_position!`
3259
- });
3260
- }
3261
- }
3262
- }
3263
- // Add tool results to conversation based on provider
3264
- if (CONFIG.aiProvider === 'claude') {
3265
- messages.push({
3266
- role: 'assistant',
3267
- content: response.toolCalls.map(tc => ({
3268
- type: 'tool_use',
3269
- id: tc.id,
3270
- name: tc.name,
3271
- input: tc.input
3272
- }))
3273
- });
3274
- messages.push({
3275
- role: 'user',
3276
- content: toolResults.map(tr => ({
3277
- type: 'tool_result',
3278
- tool_use_id: tr.id,
3279
- content: JSON.stringify(tr.result)
3280
- }))
3281
- });
3282
- }
3283
- else if (CONFIG.aiProvider === 'openai') {
3284
- messages.push({
3285
- role: 'assistant',
3286
- tool_calls: response.toolCalls.map(tc => ({
3287
- id: tc.id,
3288
- type: 'function',
3289
- function: {
3290
- name: tc.name,
3291
- arguments: JSON.stringify(tc.input)
3292
- }
3293
- }))
3294
- });
3295
- for (const tr of toolResults) {
3296
- messages.push({
3297
- role: 'tool',
3298
- tool_call_id: tr.id,
3299
- content: JSON.stringify(tr.result)
3300
- });
3301
- }
3302
- }
3303
- else {
3304
- // Gemini - use proper function call format
3305
- for (const toolCall of response.toolCalls) {
3306
- // Add model's function call
3307
- messages.push({
3308
- role: 'model',
3309
- functionCall: {
3310
- name: toolCall.name,
3311
- args: toolCall.input
3312
- }
3313
- });
3314
- // Add user's function response
3315
- const result = toolResults.find(tr => tr.name === toolCall.name);
3316
- messages.push({
3317
- role: 'user',
3318
- functionResponse: {
3319
- name: toolCall.name,
3320
- response: result?.result
3321
- }
3322
- });
3323
- }
3324
- }
3325
- // Check if legitimate failure was reported
3326
- if (legitimateFailureReported) {
3327
- console.log('\nโœ… Stopping iteration: Legitimate failure reported.');
3328
- break;
3329
- }
3330
- // Check if tests were run and passed
3331
- const testRun = toolResults.find(tr => tr.name === 'run_tests');
3332
- if (testRun?.result.passed) {
3333
- console.log('\n๐ŸŽ‰ All tests passed!');
3334
- break;
3335
- }
3336
- }
3337
- if (iterations >= maxIterations) {
3338
- console.log('\nโš ๏ธ Reached maximum iterations. Tests may not be complete.');
3339
- }
3340
- if (!testFileWritten) {
3341
- console.log('\nโŒ WARNING: Test file was never written! The AI may not have used the tools correctly.');
3342
- console.log(' Try running again or check your API key and connectivity.');
3352
+ // Filter to only EXPORTED functions
3353
+ const exportedFunctions = result.analysis.functions.filter((f) => f.exported);
3354
+ const functionNames = exportedFunctions.map((f) => f.name).filter((name) => name);
3355
+ // Error if no exported functions
3356
+ if (functionNames.length === 0) {
3357
+ throw new Error('No exported functions found in file. Cannot generate tests.');
3343
3358
  }
3344
- else if (legitimateFailureReported) {
3345
- console.log('\n๐Ÿ“‹ Test file created with legitimate failures documented.');
3346
- console.log(' These failures indicate bugs in the source code that need to be fixed.');
3359
+ // Log what we found
3360
+ const totalFunctions = result.analysis.functions.length;
3361
+ const internalFunctions = totalFunctions - exportedFunctions.length;
3362
+ // console.log(`โœ… Found ${functionNames.length} exported function(s): ${functionNames.join(', ')}`);
3363
+ if (internalFunctions > 0) {
3364
+ // console.log(` (Skipping ${internalFunctions} internal/helper function(s) - only testing public API)`);
3347
3365
  }
3348
- return testFilePath;
3366
+ // Always use function-by-function generation
3367
+ return await generateTestsForFunctions(sourceFile, functionNames);
3349
3368
  }
3350
3369
  // Interactive CLI
3351
3370
  async function promptUser(question) {
@@ -3428,14 +3447,233 @@ async function generateTestsForFolder() {
3428
3447
  * @returns true if tests passed, false if legitimate failure reported
3429
3448
  */
3430
3449
  async function generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists) {
3431
- const messages = [
3432
- {
3433
- role: 'user',
3434
- content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
3450
+ // Set the expected test file path globally to prevent AI from creating per-function files
3451
+ EXPECTED_TEST_FILE_PATH = testFilePath;
3452
+ const testEnv = CONFIG.testEnv;
3453
+ let messages;
3454
+ if (testEnv == "vitest") {
3455
+ messages = [
3456
+ {
3457
+ role: "user",
3458
+ content: `You are a senior developer generating production-ready Vitest unit tests for TypeScript.
3459
+
3460
+ ## TARGET
3461
+ Function: ${functionName} | Source: ${sourceFile}
3462
+ Test File: ${testFilePath} (Exists: ${testFileExists})
3463
+
3464
+ ## WORKFLOW
3465
+
3466
+ ### 1. ANALYSIS (Execute in order)
3467
+ 1. analyze_file_ast(${sourceFile}, "${functionName}") โ†’ target function metadata
3468
+ 2. get_function_ast(${sourceFile}, "${functionName}") โ†’ implementation
3469
+ 3. get_imports_ast(${sourceFile}) โ†’ trace ALL dependencies
3470
+ 4. get_file_preamble(${testFilePath}) โ†’ existing mocks/imports (if exists)
3471
+ 5. For each dependency: find_file() โ†’ get_function_ast() โ†’ understand behavior
3472
+ 6. calculate_relative_path(from: ${testFilePath}, to: import_path) โ†’ all imports
3473
+
3474
+ **Dependency Tracing (CRITICAL):**
3475
+ - Map EVERY function call to its import source (verify with get_imports_ast)
3476
+ - Create vi.mock() for EVERY imported module (even conditional usage)
3477
+ - Export ALL used functions from each vi.mock block
3478
+ - Set return values in beforeEach for ALL mocked functions
3479
+ - Auto-detect required named exports from AST to prevent "X is not defined"
3480
+
3481
+ ### 2. FILE STRUCTURE (STRICT ORDER)
3482
+
3483
+
3484
+ // 1. MOCKS (before ANY imports)
3485
+ vi.mock('module-path');
3486
+ vi.mock('database/index', () => ({ db: { query: vi.fn() } }));
3487
+ vi.mock('env', () => ({ _ENV: { KEY: 'test' } }));
3488
+
3489
+ // 2. IMPORTS
3490
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3491
+ import type { TypeOnly } from './types';
3492
+ import { ${functionName} } from 'calculated-path';
3493
+ import { dependency } from 'dependency-path';
3494
+
3495
+ // 3. TYPED MOCKS
3496
+ const mockDep = vi.mocked(dependency);
3497
+
3498
+ // 4. TESTS
3499
+ describe('${functionName}', () => {
3500
+ beforeEach(() => {
3501
+ // clearMocks: true in config handles cleanup
3502
+ mockDep.mockResolvedValue(defaultValue);
3503
+ });
3504
+
3505
+ it('should_behavior_when_condition', async () => {
3506
+ // ARRANGE
3507
+ const input = { id: 1 };
3508
+ mockDep.mockResolvedValueOnce(specificValue);
3509
+
3510
+ // ACT
3511
+ const result = await ${functionName}(input);
3512
+
3513
+ // ASSERT
3514
+ expect(result).toEqual(expected);
3515
+ expect(mockDep).toHaveBeenCalledWith(input);
3516
+ expect(mockDep).toHaveBeenCalledTimes(1);
3517
+ });
3518
+ });
3519
+
3520
+
3521
+ ### 3. COVERAGE (Minimum 5 tests)
3522
+
3523
+ 1. **Happy Path** (1-2): Valid inputs โ†’ expected outputs
3524
+ 2. **Edge Cases** (2-3): Empty/null/undefined/0/boundaries/special chars
3525
+ 3. **Error Handling** (1-2): Invalid inputs, dependency failures
3526
+ - Sync: expect(() => fn()).toThrow(ErrorClass)
3527
+ - Async: await expect(fn()).rejects.toThrow(ErrorClass)
3528
+ 4. **Branch Coverage**: Each conditional path tested
3529
+ 5. **Async Behavior**: Promise resolution/rejection (if applicable)
3530
+
3531
+ ### 4. MOCK STRATEGY
3532
+
3533
+ **ALWAYS Mock:**
3534
+ - External modules (fs, http, database)
3535
+ - Modules with side effects (logging, analytics)
3536
+ - database/index, env, WinstonLogger
3537
+
3538
+ **Consider NOT Mocking:**
3539
+ - Pure utility functions from same codebase (test integration)
3540
+ - Type imports: import type { X } (NEVER mock)
3541
+
3542
+ **Mock Pattern:**
3543
+ - Module-level: vi.mock('path') โ†’ const mockFn = vi.mocked(importedFn)
3544
+ - NEVER: vi.spyOn(exports, 'fn') or global wrappers
3545
+ - Hoist mock functions for use in both factory and tests
3546
+
3547
+ ### 5. ASSERTIONS
3548
+
3549
+ **Priority Order:**
3550
+ 1. **Exact**: toEqual(), toBe() for primitives
3551
+ 2. **Partial**: toMatchObject() for subset matching
3552
+ 3. **Structural**: expect.objectContaining(), expect.any(Type)
3553
+ 4. **Specific Checks**: toBeDefined(), toBeNull(), toHaveLength(n)
3554
+ 5. **Mock Verification**: toHaveBeenCalledWith(), toHaveBeenCalledTimes()
3555
+
3556
+ **NEVER:**
3557
+ - toBeTruthy() for object existence (use toBeDefined())
3558
+ - Snapshots for dates, random values, or as primary assertions
3559
+
3560
+ ### 6. CRITICAL RULES
3561
+
3562
+ **MUST:**
3563
+ - โœ… ALL vi.mock() before imports
3564
+ - โœ… Use calculate_relative_path for ALL imports
3565
+ - โœ… Test file exists: ${testFileExists} - if the test file exist, alway check the mock and imports already present in the test file, using get_file_preamble tool. Make sure you do not duplicate mocks and mocks and imports are added at correct position.
3566
+ - โœ… When editing existing file: UPDATE existing vi.mock, NEVER duplicate
3567
+ - โœ… Test names: should_behavior_when_condition
3568
+ - โœ… AAA pattern with comments
3569
+ - โœ… Each test = one behavior
3570
+ - โœ… Import types separately: import type { Config }
3571
+ - โœ… Use vi.mocked<typeof module>() for full type inference
3572
+ - โœ… Mock internal non-exported functions
3573
+ - โœ… Use vi.useFakeTimers() for time-dependent tests
3574
+ - โœ… Test cases expectations must match source code
3575
+
3576
+
3577
+ **NEVER:**
3578
+ - โŒ Mock after imports
3579
+ - โŒ Shared state between tests
3580
+ - โŒ Multiple behaviors in one test
3581
+ - โŒ Generic test names ("test1", "works")
3582
+ - โŒ Manual cleanup (vi.clearAllMocks in tests - config handles it)
3583
+ - โŒ Environment dependencies without mocks
3584
+ - โŒ Use require() (ES imports only)
3585
+ - โŒ Reference function from wrong module (verify with get_imports_ast)
3586
+ - โŒ Change existing mocks in ways that break other tests\
3587
+
3588
+
3589
+ ### 7. EXECUTION
3590
+
3591
+ upsert_function_tests({
3592
+ test_file_path: "${testFilePath}",
3593
+ function_name: "${functionName}",
3594
+ new_test_content: "..." // complete test code
3595
+ });
3596
+
3597
+ run_tests({
3598
+ test_file_path: "${testFilePath}",
3599
+ function_names: ["${functionName}"]
3600
+ });
3601
+
3602
+
3603
+ ### 8. FAILURE DEBUGGING
3604
+
3605
+ **Import Errors:**
3606
+ - Recalculate paths with calculate_relative_path
3607
+ - Check barrel exports (index.ts redirects)
3608
+ - Verify source file exports function
3609
+
3610
+ **Mock Errors:**
3611
+ - Add missing vi.mock() at top
3612
+ - Ensure all used functions exported from mock
3613
+ - Verify mock setup in beforeEach
3614
+
3615
+ **Type Errors:**
3616
+ - Import types with import type
3617
+ - Check mock return types match signatures
3618
+ - Use proper generic constraints
3619
+
3620
+ **Assertion Failures:**
3621
+ - Log mock calls: console.log(mockFn.mock.calls)
3622
+ - Check execution path (add temp logs in source)
3623
+ - Verify input data types
3624
+ - Check async/await usage
3625
+ - Validate prerequisite mocks return expected values
3626
+
3627
+ ** If fails, categorize:
3628
+
3629
+ **[MUST] FIXABLE** โ†’ Fix these:
3630
+ | Error | Fix Method |
3631
+ |-------|-----------|
3632
+ | Wrong imports | find_file(fileName) to get the file path + calculate_relative_path + search_replace_block |
3633
+ | Missing mocks | insert_at_position |
3634
+ | Syntax errors | search_replace_block (3-5 lines context) |
3635
+ | Mock pollution | Fix beforeEach pattern |
3636
+ | "Test suite failed to run" | get_file_preamble + fix imports/mocks |
3637
+ | "Cannot find module" | calculate_relative_path |
3638
+
3639
+ **Process:**
3640
+ 1. Read FULL error message
3641
+ 2. Identify error type (import/mock/assertion/type)
3642
+ 3. Make focused fix using available tools
3643
+ 4. Iterate until ALL PASS
3644
+
3645
+ ### 9. QUALITY CHECKLIST
3646
+ - [ ] Independent tests (no execution order dependency)
3647
+ - [ ] Fast (<100ms per test, no real I/O)
3648
+ - [ ] Readable (AAA, descriptive names)
3649
+ - [ ] Focused (one behavior per test)
3650
+ - [ ] Deterministic (same input = same output)
3651
+ - [ ] Type-safe (no any, proper generics)
3652
+ - [ ] Complete coverage (all paths tested)
3653
+ - [ ] No duplicate declarations
3654
+ - [ ] Existing tests still pass
3655
+
3656
+ ## START
3657
+ \`analyze_file_ast\` โ†’ gather context โ†’ **write tests immediately** โ†’ verify โ†’ run โ†’ fix โ†’ complete.
3658
+
3659
+ **[CRITICAL]** NEVER change existing mocks such that other tests fail. Use test-specific overrides with mockReturnValueOnce.
3660
+ You must be efficient and fast in your approach, do not overthink the problem.`,
3661
+ },
3662
+ ];
3663
+ }
3664
+ else {
3665
+ messages = [
3666
+ {
3667
+ role: "user",
3668
+ content: `You are an expert software test engineer. Generate comprehensive Jest unit tests for: ${functionName} in ${sourceFile}.
3669
+ [Critical] Be prompt and efficient in your response. Make sure the test case file is typed and complete. Be as fast as possible in your repsonse.
3670
+ [Critical] You cannot remove or modify the existing mocks and imports in the test file since other test may be using it. You can only add new mocks and imports for the new test cases.
3435
3671
 
3436
3672
  ## CONTEXT
3437
3673
  Test file: ${testFilePath} | Exists: ${testFileExists}
3438
3674
 
3675
+ โš ๏ธ CRITICAL: You MUST use this EXACT test file path: ${testFilePath}
3676
+
3439
3677
  ---
3440
3678
 
3441
3679
  ## EXECUTION PLAN
@@ -3448,8 +3686,9 @@ Test file: ${testFilePath} | Exists: ${testFileExists}
3448
3686
  - Same file: get_function_ast(${sourceFile},{functionName})
3449
3687
  - Other file [Can take reference from the imports of the ${sourceFile} file for the file name that has the required function]: find_file(filename) to get file path -> get_function_ast({file_path},{functionName}) + check for external calls
3450
3688
  4. get_imports_ast โ†’ all dependencies
3451
- 5. calculate_relative_path for each import
3689
+ 5. calculate_relative_path(from: ${testFilePath}, to: import_path) โ†’ all imports, accpets multiple comma separated 'to' paths. Use exact path returned by this tool for all imports.
3452
3690
  6. get_file_preamble โ†’ imports and mocks already declared in the file
3691
+ 7. search_codebase โ†’ look for relevant context in codebase.
3453
3692
  \\\`\\\`\\\`
3454
3693
 
3455
3694
  **Phase 1.1: Execution Path Tracing (CRITICAL FOR SUCCESS)**
@@ -3461,7 +3700,7 @@ Test file: ${testFilePath} | Exists: ${testFileExists}
3461
3700
 
3462
3701
  **Phase 2: Test Generation**
3463
3702
 
3464
- Mock Pattern (CRITICAL - Top of file, before imports):
3703
+ Mock Pattern (CRITICAL - Top of file):
3465
3704
  \\\`\\\`\\\`typescript
3466
3705
  // ===== MOCKS (BEFORE IMPORTS) =====
3467
3706
  jest.mock('config', () => ({
@@ -3469,14 +3708,19 @@ jest.mock('config', () => ({
3469
3708
  AUTH: { JWT_KEY: 'test', COOKIE_DATA_ONE_YEAR: 31536000000 },
3470
3709
  USER_DEL_SECRET: 'secret'
3471
3710
  })
3472
- }), { virtual: true });
3711
+ }), { virtual: true });
3712
+
3713
+ // virtual:true ONLY for config, db, models, routes, services, axios, newrelic, GOOGLE_CLOUD_STORAGE, winston, logger, etc.
3473
3714
 
3474
- jest.mock('../path/from/calculate_relative_path');
3475
- // Never virtual:true for actual source helpers!
3476
- // โš ๏ธ CRITICAL: Mock ALL dependencies at top level, even if unused
3715
+ jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
3477
3716
 
3478
3717
  // ===== IMPORTS =====
3479
3718
  import { functionName } from '../controller';
3719
+ import { dependencyMethod } from '../helpers/dependency';
3720
+
3721
+ // ===== TYPED MOCKS =====
3722
+ const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
3723
+
3480
3724
  \\\`\\\`\\\`
3481
3725
 
3482
3726
  Requirements (5+ tests minimum):
@@ -3486,121 +3730,93 @@ Requirements (5+ tests minimum):
3486
3730
  - โฑ๏ธ Async behavior
3487
3731
  - ๐Ÿ” API null/undefined handling
3488
3732
 
3489
- **Phase 3: Anti-Pollution Pattern (MANDATORY)**
3490
-
3491
- ### Step 1: Mock Setup (Top of File)
3492
- // ===== MOCKS (BEFORE IMPORTS) =====
3493
- jest.mock('config', () => ({
3494
- get: () => ({ KEY: 'value' })
3495
- }), { virtual: true }); // virtual:true ONLY for config, db, models
3496
-
3497
- jest.mock('../helpers/dependency'); // NO virtual:true for regular modules
3498
-
3499
- // ===== IMPORTS =====
3500
- import { functionName } from '../controller';
3501
- import { dependencyMethod } from '../helpers/dependency';
3733
+ /**
3734
+ * Phase 3: Anti-Pollution Pattern (MUST FOLLOW EXACTLY THIS PATTERN, NO VARIATIONS)
3735
+ */
3502
3736
 
3503
- // ===== TYPED MOCKS =====
3504
- const mockDependencyMethod = dependencyMethod as jest.MockedFunction<typeof dependencyMethod>;
3737
+ \\\`\\\`\\\`typescript
3738
+ // ===== GLOBAL CLEANUP (Near top, outside describe blocks) =====
3739
+ afterEach(() => {
3740
+ jest.restoreAllMocks(); // Automatically restores ALL spies
3741
+ });
3505
3742
 
3506
- ### Step 2: Test Structure
3743
+ // ===== TESTS =====
3507
3744
  describe('functionName', () => {
3508
3745
  beforeEach(() => {
3509
- // ALWAYS first line
3510
- jest.clearAllMocks();
3746
+ jest.resetAllMocks(); // Resets ALL mocks (call history + implementations)
3511
3747
 
3512
- // Set defaults for THIS describe block only
3513
- mockDependencyMethod.mockResolvedValue({ status: 'success' });
3748
+ // Set fresh defaults for THIS describe block only
3749
+ mockDep1.mockResolvedValue({ status: 'success' });
3750
+ mockDep2.mockReturnValue(true);
3514
3751
  });
3515
3752
 
3516
3753
  test('happy path', async () => {
3517
- // Override default for this test only
3518
- mockDependencyMethod.mockResolvedValueOnce({ id: 123 });
3754
+ mockDep1.mockResolvedValueOnce({ id: 123 }); // Override for this test only
3519
3755
 
3520
3756
  const result = await functionName();
3521
3757
 
3522
3758
  expect(result).toEqual({ id: 123 });
3523
- expect(mockDependencyMethod).toHaveBeenCalledWith(expect.objectContaining({
3524
- param: 'value'
3525
- }));
3759
+ expect(mockDep1).toHaveBeenCalledWith(expect.objectContaining({ param: 'value' }));
3526
3760
  });
3527
3761
 
3528
3762
  test('error case', async () => {
3529
- mockDependencyMethod.mockRejectedValueOnce(new Error('fail'));
3530
-
3531
- const result = await functionName();
3532
-
3533
- expect(result).toEqual({});
3763
+ mockDep1.mockRejectedValueOnce(new Error('fail'));
3764
+ await expect(functionName()).rejects.toThrow('fail');
3534
3765
  });
3535
3766
  });
3536
3767
 
3537
- describe('anotherFunction', () => {
3538
- beforeEach(() => {
3539
- jest.clearAllMocks();
3540
-
3541
- // Different defaults for different function
3542
- mockDependencyMethod.mockResolvedValue({ status: 'pending' });
3543
- });
3544
-
3545
- // ... tests
3546
- });
3547
-
3548
-
3549
- ### Step 3: Internal Function Mocking (When Needed)
3550
-
3768
+ // ===== INTERNAL SPIES (When testing same-file function calls) =====
3551
3769
  describe('functionWithInternalCalls', () => {
3552
3770
  let internalFnSpy: jest.SpyInstance;
3553
3771
 
3554
3772
  beforeEach(() => {
3555
- jest.clearAllMocks();
3773
+ jest.resetAllMocks();
3556
3774
 
3775
+ // โœ… EXCEPTION: require() needed here for spying on same module
3557
3776
  const controller = require('../controller');
3558
- internalFnSpy = jest.spyOn(controller, 'internalFunction')
3559
- .mockResolvedValue(undefined);
3560
- });
3561
-
3562
- afterEach(() => {
3563
- internalFnSpy.mockRestore();
3777
+ internalFnSpy = jest.spyOn(controller, 'internalFunction').mockResolvedValue(undefined);
3564
3778
  });
3779
+
3780
+ // No manual restore needed - global afterEach handles it
3565
3781
 
3566
3782
  test('calls internal function', async () => {
3567
3783
  await functionWithInternalCalls();
3568
3784
  expect(internalFnSpy).toHaveBeenCalled();
3569
3785
  });
3570
3786
  });
3787
+ \\\`\\\`\\\`
3571
3788
 
3572
- ### CRITICAL RULES:
3789
+ ### CRITICAL RULES (Prevent Mock Pollution):
3573
3790
  **DO โœ…**
3574
- 1. **Mock at file top** - All \`jest.mock()\` calls before any imports
3575
- 2. **Import directly** - Use \`import { fn } from 'module'\` (never \`require()\`)
3576
- 3. **Type all mocks** - \`const mockFn = fn as jest.MockedFunction<typeof fn>\`
3577
- 4. **Clear first always** - \`jest.clearAllMocks()\` as first line in every \`beforeEach()\`
3578
- 5. **Isolate describe defaults** - Each \`describe\` block sets its own mock defaults
3579
- 6. **Override with mockOnce** - Use \`mockResolvedValueOnce/mockReturnValueOnce\` in tests
3580
- 7. **Restore spies** - Use \`jest.spyOn()\` with \`mockRestore()\` for internal function spies
3581
- 8. **Use calculate_relative_path** - For all import and mock paths
3791
+ 1. \`jest.resetAllMocks()\` as FIRST line in every \`beforeEach()\` (not clearAllMocks)
3792
+ 2. Global \`afterEach(() => jest.restoreAllMocks())\` near top of test file
3793
+ 3. Set mock defaults in each \`describe\` block's \`beforeEach()\` independently
3794
+ 4. Override with \`mockResolvedValueOnce/mockReturnValueOnce\` in individual tests
3795
+ 5. Type all mocks: \`const mockFn = fn as jest.MockedFunction<typeof fn>\`
3796
+ 6. All \`jest.mock()\` at top before imports (use calculate_relative_path for paths)
3797
+ 7. Check for existing mocks with \`get_file_preamble\` tool before adding duplicates
3582
3798
 
3583
3799
  **DON'T โŒ**
3584
- 1. **Re-require modules** - Never use \`require()\` in \`beforeEach()\` or tests
3585
- 2. **Check mock existence** - Never \`if (!mockFn)\` - indicates broken setup
3586
- 3. **Share mock state** - Don't rely on defaults from other \`describe\` blocks
3587
- 4. **Skip jest.clearAllMocks()** - Missing this is the #1 cause of pollution
3588
- 5. **Use virtual:true everywhere** - Only for: config, db, models, routes, services
3589
- 6. **Forget to restore spies** - Always pair \`jest.spyOn()\` with \`mockRestore()\`
3590
-
3591
- ### Why This Works:
3592
- - **No pollution**: Each describe block sets its own defaults in beforeEach
3593
- - **No conflicts**: clearAllMocks() resets all mock state
3594
- - **Type safety**: TypeScript catches mock mismatches
3595
- - **Predictable**: Tests run in any order with same results
3800
+ 1. Use \`jest.clearAllMocks()\` (only clears history, not implementations) โ†’ Use \`resetAllMocks()\`
3801
+ 2. Manually \`.mockReset()\` individual mocks โ†’ \`resetAllMocks()\` handles all
3802
+ 3. Share mock state between \`describe\` blocks โ†’ Each block sets its own defaults
3803
+ 4. Use \`require()\` except when creating spies on same module being tested
3804
+ 5. Use \`virtual:true\` for regular files โ†’ Only for: config, db, models, services (modules not in filesystem)
3805
+ 6. Forget global \`afterEach(() => jest.restoreAllMocks())\` โ†’ Causes spy pollution
3806
+
3807
+
3596
3808
 
3597
3809
  **Phase 4: Write Tests**
3810
+ โš ๏ธ CRITICAL REQUIREMENT: Use EXACTLY this test file path: "${testFilePath}"
3811
+ DO NOT modify the path. DO NOT create ${functionName}.test.ts or any other variation.
3812
+
3598
3813
  โ†’ upsert_function_tests({
3599
- test_file_path: "${testFilePath}",
3814
+ test_file_path: "${testFilePath}", // โš ๏ธ USE THIS EXACT PATH - DO NOT CHANGE!
3600
3815
  function_name: "${functionName}",
3601
3816
  new_test_content: "describe('${functionName}', () => {...})"
3602
3817
  })
3603
3818
  This will automatically replace the existing test cases for the function with the new test cases or add new test cases if the function is not found in the test file.
3819
+ All functions from the same source file MUST share the same test file.
3604
3820
 
3605
3821
 
3606
3822
 
@@ -3636,14 +3852,8 @@ This will automatically replace the existing test cases for the function with th
3636
3852
  | "Test suite failed to run" | get_file_preamble + fix imports/mocks |
3637
3853
  | "Cannot find module" | calculate_relative_path |
3638
3854
 
3639
- **LEGITIMATE** โ†’ Report, don't fix:
3640
- - Source returns wrong type
3641
- - Missing null checks in source
3642
- - Logic errors in source
3643
-
3644
- โ›” NEVER report "Test suite failed to run" as legitimate
3645
3855
 
3646
- 3๏ธโƒฃ Repeat until: โœ… Pass OR ๐Ÿ“‹ Legitimate failure (report_legitimate_failure)
3856
+ 3๏ธโƒฃ Repeat until: โœ… All test cases pass
3647
3857
 
3648
3858
  ---
3649
3859
 
@@ -3655,10 +3865,14 @@ This will automatically replace the existing test cases for the function with th
3655
3865
  - Ensure test independence (no pollution)
3656
3866
  - Fix test bugs, report source bugs
3657
3867
  - [CRITICAL] Each test suite should be completely self-contained and not depend on or affect any other test suite's state.
3868
+ - Test file exists: ${testFileExists} - if the test file exist, always check the mock and imports already present in the test file, using get_file_preamble tool. Make sure you do not duplicate mocks and mocks and imports are added at correct position.
3869
+ - Mocking of winston logger or any other external dependeny is critical and mandatory.
3870
+ - Use search_codebase tool to look for relevant context in codebase quickly.
3658
3871
 
3659
- **START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file.`
3660
- }
3661
- ];
3872
+ **START:** Call analyze_file_ast on ${sourceFile} now. This will give you the file structure and the functions in the file. Analyze deeply and write test cases only when you are sure about the function and the dependencies. Make sure the written test cases run and pass on first attempt.`,
3873
+ },
3874
+ ];
3875
+ }
3662
3876
  let iterations = 0;
3663
3877
  const maxIterations = 100;
3664
3878
  let testFileWritten = false;
@@ -3667,16 +3881,25 @@ This will automatically replace the existing test cases for the function with th
3667
3881
  let lastTestError = '';
3668
3882
  let sameErrorCount = 0;
3669
3883
  while (iterations < maxIterations) {
3670
- // console.log('USING CLAUDE PROMPT original 16')
3671
3884
  iterations++;
3672
3885
  if (iterations === 1) {
3673
- console.log(`\n๐Ÿค– AI is analyzing selected functions...`);
3886
+ if (!globalSpinner || !globalSpinner.isSpinning) {
3887
+ globalSpinner = ora('๐Ÿค– AI is analyzing selected functions...').start();
3888
+ }
3889
+ else {
3890
+ globalSpinner.text = '๐Ÿค– AI is analyzing selected functions...';
3891
+ }
3674
3892
  }
3675
3893
  else if (iterations % 5 === 0) {
3676
- console.log(`\n๐Ÿค– AI is still working (step ${iterations})...`);
3894
+ if (globalSpinner) {
3895
+ globalSpinner.text = `๐Ÿค– AI is still working (step ${iterations})...`;
3896
+ }
3677
3897
  }
3678
- const response = await callAI(messages, TOOLS);
3898
+ // console.log('messages', messages);
3899
+ // console.log('TOOLS_FOR_TEST_GENERATION', TOOLS_FOR_TEST_GENERATION);
3900
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3679
3901
  // console.log('response from AI', JSON.stringify(response, null, 2));
3902
+ // console.log('TEst file path', testFilePath);
3680
3903
  if (response.content) {
3681
3904
  const content = response.content;
3682
3905
  // Only show AI message if it's making excuses (for debugging), otherwise skip
@@ -3690,7 +3913,7 @@ This will automatically replace the existing test cases for the function with th
3690
3913
  /beyond my capabilities/i,
3691
3914
  /can't execute/i
3692
3915
  ];
3693
- const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
3916
+ const isMakingExcuses = excusePatterns.some(pattern => typeof content === 'string' && pattern.test(content));
3694
3917
  if (isMakingExcuses) {
3695
3918
  console.log('\nโš ๏ธ AI is making excuses! Forcing it to use tools...');
3696
3919
  messages.push({
@@ -3740,7 +3963,7 @@ This works for both NEW and EXISTING test files!`
3740
3963
  4. Then run_tests to verify
3741
3964
 
3742
3965
  ๐Ÿ“Œ ALTERNATIVE: Use insert_at_position for adding imports/mocks
3743
- - insert_at_position({ position: 'after_imports', content: "jest.mock('../module');" })
3966
+ - insert_at_position({ position: 'after_imports', content: "vi.mock('../module');" })
3744
3967
 
3745
3968
  โš ๏ธ SECONDARY: Use upsert_function_tests for function-level rewrites
3746
3969
 
@@ -3772,16 +3995,13 @@ Start NOW with search_replace_block or insert_at_position!`
3772
3995
  const currentError = errorOutput.substring(0, 300);
3773
3996
  if (currentError === lastTestError) {
3774
3997
  sameErrorCount++;
3775
- console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
3998
+ // console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
3776
3999
  if (sameErrorCount >= 3) {
3777
- console.log('\n๐Ÿšจ Same error repeated 3+ times! ');
4000
+ // console.log('\n๐Ÿšจ Same error repeated 3+ times! ');
3778
4001
  messages.push({
3779
4002
  role: 'user',
3780
4003
  content: `The same test error has occurred ${sameErrorCount} times in a row!
3781
-
3782
-
3783
- If this is a source code bug: Call report_legitimate_failure tool NOW.
3784
- If this is still fixable: Make focused attempt to fix it.`
4004
+ Make focused attempt to fix the tests using the tools available.`
3785
4005
  });
3786
4006
  }
3787
4007
  }
@@ -3794,7 +4014,7 @@ If this is still fixable: Make focused attempt to fix it.`
3794
4014
  if (toolCall.name === 'upsert_function_tests') {
3795
4015
  if (result.success) {
3796
4016
  testFileWritten = true;
3797
- console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
4017
+ // console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
3798
4018
  messages.push({
3799
4019
  role: 'user',
3800
4020
  content: `Test files are written successfully. Please use run_tests tool to verify the tests. If the tests fail, please make focused attempts to fix the tests using the tools available.`
@@ -3880,12 +4100,194 @@ If this is still fixable: Make focused attempt to fix it.`
3880
4100
  console.log('\n๐Ÿ“‹ Test file updated with legitimate failures documented.');
3881
4101
  console.log(' These failures indicate bugs in the source code that need to be fixed.');
3882
4102
  }
4103
+ // Clear the expected test file path (cleanup)
4104
+ EXPECTED_TEST_FILE_PATH = null;
3883
4105
  // Return true if tests passed, false if legitimate failure reported
3884
4106
  // Get the LAST test run result (not the first) to check final status
3885
4107
  const testRuns = allToolResults.filter(tr => tr.name === 'run_tests');
3886
4108
  const lastTestRun = testRuns.length > 0 ? testRuns[testRuns.length - 1] : null;
3887
4109
  return !legitimateFailureReported && (lastTestRun?.result?.passed || false);
3888
4110
  }
4111
+ /**
4112
+ * Smart validation that fixes failing tests
4113
+ * - Runs full test suite
4114
+ * - For failures, attempts to fix all failing tests using AI
4115
+ */
4116
+ async function smartValidateTestSuite(sourceFile, testFilePath, functionNames) {
4117
+ console.log(`\n${'='.repeat(80)}`);
4118
+ console.log(`๐Ÿ” VALIDATION: Running full test suite (${functionNames.length} function(s))`);
4119
+ console.log(`${'='.repeat(80)}\n`);
4120
+ // Run tests for entire file (no function filter)
4121
+ let fullSuiteResult;
4122
+ if (CONFIG.testEnv == 'vitest') {
4123
+ fullSuiteResult = runTestsVitest(testFilePath);
4124
+ }
4125
+ else {
4126
+ fullSuiteResult = runTestsJest(testFilePath);
4127
+ }
4128
+ if (fullSuiteResult.passed) {
4129
+ console.log(`\nโœ… Full test suite passed! All ${functionNames.length} function(s) working together correctly.`);
4130
+ return;
4131
+ }
4132
+ console.log(`\nโš ๏ธ Full test suite has failures. Attempting to fix failing tests...\n`);
4133
+ // Parse failing test names from Vitest output
4134
+ let failingTests;
4135
+ if (CONFIG.testEnv == 'vitest') {
4136
+ failingTests = parseFailingTestNamesVitest(fullSuiteResult.output);
4137
+ }
4138
+ else {
4139
+ failingTests = parseFailingTestNamesJest(fullSuiteResult.output);
4140
+ }
4141
+ console.log(`\n๐Ÿ“Š Debug: Found ${failingTests.length} failing test(s) from output`);
4142
+ if (failingTests.length === 0) {
4143
+ console.log('โš ๏ธ Could not parse specific failing test names from output.');
4144
+ console.log(' Attempting general fix based on full error output...\n');
4145
+ // Fallback: Still attempt to fix using the full output even without parsed test names
4146
+ await fixFailingTests(sourceFile, testFilePath, functionNames, [], fullSuiteResult.output);
4147
+ return;
4148
+ }
4149
+ console.log(`Found ${failingTests.length} failing test(s): ${failingTests.join(', ')}\n`);
4150
+ // Attempt to fix all failing tests
4151
+ await fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteResult.output);
4152
+ }
4153
+ /**
4154
+ * Fix failing tests using AI
4155
+ * Attempts to fix all test issues including pollution, imports, mocks, etc.
4156
+ */
4157
+ async function fixFailingTests(sourceFile, testFilePath, functionNames, failingTests, fullSuiteOutput) {
4158
+ const messages = [
4159
+ {
4160
+ role: 'user',
4161
+ content: `You are fixing FAILING TESTS in the Vitest test suite.
4162
+
4163
+ Source file: ${sourceFile}
4164
+ Test file: ${testFilePath}
4165
+ Functions tested: ${functionNames.join(', ')}
4166
+
4167
+ FAILING TESTS:
4168
+ ${failingTests.map(t => `- ${t}`).join('\n')}
4169
+
4170
+ Full suite output:
4171
+ ${fullSuiteOutput}
4172
+
4173
+ YOUR TASK - Fix all failing tests:
4174
+
4175
+ COMMON ISSUES TO FIX:
4176
+ - Mock state bleeding between describe blocks
4177
+ - Missing vitest imports (describe, it, expect, beforeEach, vi)
4178
+ - Incorrect mock typing (use MockedFunction from vitest)
4179
+ - beforeEach not setting up mocks properly
4180
+ - Missing or incorrect imports
4181
+ - Mock implementation issues
4182
+ - Incorrect test assertions
4183
+ - Test logic errors
4184
+
4185
+ NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled.
4186
+
4187
+ TOOLS TO USE:
4188
+ 1. get_file_preamble - See current setup
4189
+ 2. search_replace_block - Fix specific sections (preferred)
4190
+ 3. insert_at_position - Add missing imports/mocks
4191
+ 4. run_tests - Verify fixes
4192
+
4193
+ START by calling get_file_preamble to see the current test structure.`
4194
+ }
4195
+ ];
4196
+ let iterations = 0;
4197
+ const maxIterations = 100; // Limit iterations for test fixes
4198
+ while (iterations < maxIterations) {
4199
+ iterations++;
4200
+ console.log(`\n๐Ÿ”ง Test fix attempt ${iterations}/${maxIterations}...`);
4201
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
4202
+ if (response.content) {
4203
+ messages.push({ role: 'assistant', content: response.content });
4204
+ }
4205
+ if (!response.toolCalls || response.toolCalls.length === 0) {
4206
+ // AI stopped - check if tests pass now
4207
+ let finalTest;
4208
+ if (CONFIG.testEnv == 'vitest') {
4209
+ finalTest = runTestsVitest(testFilePath);
4210
+ }
4211
+ else {
4212
+ finalTest = runTestsJest(testFilePath);
4213
+ }
4214
+ if (finalTest.passed) {
4215
+ console.log('\nโœ… Tests fixed! Full test suite now passes.');
4216
+ return;
4217
+ }
4218
+ console.log('\nโš ๏ธ AI stopped but tests still failing.');
4219
+ break;
4220
+ }
4221
+ // Execute tool calls
4222
+ const toolResults = [];
4223
+ for (const toolCall of response.toolCalls) {
4224
+ const result = await executeTool(toolCall.name, toolCall.input);
4225
+ toolResults.push({ id: toolCall.id, name: toolCall.name, result });
4226
+ // Check if tests passed
4227
+ if (toolCall.name === 'run_tests' && result.passed) {
4228
+ console.log('\nโœ… Tests fixed! Full test suite now passes.');
4229
+ return;
4230
+ }
4231
+ }
4232
+ // Add tool results to conversation
4233
+ if (CONFIG.aiProvider === 'claude') {
4234
+ messages.push({
4235
+ role: 'assistant',
4236
+ content: response.toolCalls.map(tc => ({
4237
+ type: 'tool_use',
4238
+ id: tc.id,
4239
+ name: tc.name,
4240
+ input: tc.input
4241
+ }))
4242
+ });
4243
+ messages.push({
4244
+ role: 'user',
4245
+ content: toolResults.map(tr => ({
4246
+ type: 'tool_result',
4247
+ tool_use_id: tr.id,
4248
+ content: JSON.stringify(tr.result)
4249
+ }))
4250
+ });
4251
+ }
4252
+ else if (CONFIG.aiProvider === 'openai') {
4253
+ messages.push({
4254
+ role: 'assistant',
4255
+ tool_calls: response.toolCalls.map(tc => ({
4256
+ id: tc.id,
4257
+ type: 'function',
4258
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
4259
+ }))
4260
+ });
4261
+ for (const tr of toolResults) {
4262
+ messages.push({
4263
+ role: 'tool',
4264
+ tool_call_id: tr.id,
4265
+ content: JSON.stringify(tr.result)
4266
+ });
4267
+ }
4268
+ }
4269
+ else {
4270
+ for (const toolCall of response.toolCalls) {
4271
+ messages.push({
4272
+ role: 'model',
4273
+ functionCall: {
4274
+ name: toolCall.name,
4275
+ args: toolCall.input
4276
+ }
4277
+ });
4278
+ const result = toolResults.find(tr => tr.name === toolCall.name);
4279
+ messages.push({
4280
+ role: 'user',
4281
+ functionResponse: {
4282
+ name: toolCall.name,
4283
+ response: result?.result
4284
+ }
4285
+ });
4286
+ }
4287
+ }
4288
+ }
4289
+ console.log('\nโš ๏ธ Could not automatically fix all failing tests. Manual review may be needed.');
4290
+ }
3889
4291
  /** [Not useful, introduce side-effect]
3890
4292
  * Validate and fix the complete test file after all functions are processed
3891
4293
  * Runs full test suite and fixes file-level issues (mock pollution, imports, etc)
@@ -3895,7 +4297,13 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
3895
4297
  console.log(`๐Ÿ” FINAL VALIDATION: Running complete test suite`);
3896
4298
  console.log(`${'='.repeat(80)}\n`);
3897
4299
  // Run tests for entire file (no function filter)
3898
- const testResult = runTests(testFilePath);
4300
+ let testResult;
4301
+ if (CONFIG.testEnv == 'vitest') {
4302
+ testResult = runTestsVitest(testFilePath);
4303
+ }
4304
+ else {
4305
+ testResult = runTestsJest(testFilePath);
4306
+ }
3899
4307
  if (testResult.passed) {
3900
4308
  console.log(`\nโœ… Complete test suite passed! All ${functionNames.length} functions working together correctly.`);
3901
4309
  return;
@@ -3905,7 +4313,7 @@ async function validateAndFixCompleteTestFile(sourceFile, testFilePath, function
3905
4313
  const messages = [
3906
4314
  {
3907
4315
  role: 'user',
3908
- content: `You are a senior software engineer fixing file-level integration issues in a Jest test file.
4316
+ content: `You are a senior software engineer fixing file-level integration issues in a Vitest test file.
3909
4317
 
3910
4318
  Source file: ${sourceFile}
3911
4319
  Test file: ${testFilePath}
@@ -3918,7 +4326,7 @@ CONTEXT:
3918
4326
  * Mock pollution between test suites
3919
4327
  * Shared state not being cleaned up
3920
4328
  * Import/mock ordering issues
3921
- * beforeEach/afterEach missing or incorrect
4329
+ * beforeEach setup issues
3922
4330
  * Mock implementations interfering with each other
3923
4331
 
3924
4332
  TEST OUTPUT:
@@ -3929,35 +4337,27 @@ YOUR TASK:
3929
4337
  2. Identify file-level issues (NOT individual function logic issues)
3930
4338
  3. Fix using search_replace_block or insert_at_position tools
3931
4339
  4. Run tests again with run_tests tool
3932
- 5. Repeat until tests pass OR you determine failures are legitimate source code bugs
4340
+ 5. Repeat until all test cases pass
3933
4341
 
3934
4342
  COMMON FILE-LEVEL ISSUES TO CHECK:
3935
- - โŒ Missing jest.clearAllMocks() in beforeEach
3936
- - โŒ Mocks not being reset between test suites
3937
- - โŒ Module requires inside describe blocks (should use let + beforeEach)
3938
- - โŒ Missing virtual:true for config/database/models mocks
3939
- - โŒ beforeEach hooks not clearing all shared state
4343
+ - โŒ Missing vitest imports (describe, it, expect, beforeEach, vi)
4344
+ - โŒ Module imports inside describe blocks (use await import() in beforeEach)
4345
+ - โŒ beforeEach hooks not setting up mocks properly
3940
4346
  - โŒ Test suites depending on execution order
3941
4347
 
4348
+ NOTE: vitest.config.ts should have clearMocks/restoreMocks enabled for auto-cleanup.
4349
+
3942
4350
  FIXABLE ISSUES (you should fix):
3943
4351
  - Mock pollution between test suites
3944
- - Missing cleanup in beforeEach/afterEach
3945
4352
  - Incorrect mock setup at file level
3946
4353
  - Import ordering issues
3947
- - Missing jest.clearAllMocks()
3948
-
3949
- LEGITIMATE FAILURES (report these):
3950
- - Source code bugs causing actual logic errors
3951
- - Missing null checks in source code
3952
- - Wrong return types from source functions
3953
- - Use report_legitimate_failure tool for these
4354
+ - Missing vitest imports
3954
4355
 
3955
4356
  CRITICAL RULES:
3956
4357
  1. DO NOT change individual test logic (they passed individually!)
3957
4358
  2. Focus ONLY on file-level integration issues
3958
4359
  3. Use search_replace_block to fix specific sections
3959
4360
  4. Preserve all existing test cases
3960
- 5. If failures are due to source code bugs, call report_legitimate_failure and STOP
3961
4361
 
3962
4362
  START by calling get_file_preamble to understand current file structure.`
3963
4363
  }
@@ -3969,13 +4369,19 @@ START by calling get_file_preamble to understand current file structure.`
3969
4369
  while (iterations < maxIterations) {
3970
4370
  iterations++;
3971
4371
  console.log(`\n๐Ÿ”ง Fixing attempt ${iterations}/${maxIterations}...`);
3972
- const response = await callAI(messages, TOOLS);
4372
+ const response = await callAI(messages, TOOLS_FOR_TEST_GENERATION);
3973
4373
  if (response.content) {
3974
4374
  messages.push({ role: 'assistant', content: response.content });
3975
4375
  }
3976
4376
  if (!response.toolCalls || response.toolCalls.length === 0) {
3977
4377
  // AI stopped without fixing - check if tests pass now
3978
- const finalTest = runTests(testFilePath);
4378
+ let finalTest;
4379
+ if (CONFIG.testEnv == 'vitest') {
4380
+ finalTest = runTestsVitest(testFilePath);
4381
+ }
4382
+ else {
4383
+ finalTest = runTestsJest(testFilePath);
4384
+ }
3979
4385
  if (finalTest.passed) {
3980
4386
  console.log('\nโœ… Complete test suite now passes!');
3981
4387
  return;
@@ -3983,7 +4389,7 @@ START by calling get_file_preamble to understand current file structure.`
3983
4389
  console.log('\nโš ๏ธ AI stopped but tests still failing. Prompting to continue...');
3984
4390
  messages.push({
3985
4391
  role: 'user',
3986
- content: 'Tests are still failing! Use tools to fix or call report_legitimate_failure if these are source code bugs.'
4392
+ content: 'Tests are still failing! Use tools to fix.'
3987
4393
  });
3988
4394
  continue;
3989
4395
  }
@@ -4073,43 +4479,74 @@ START by calling get_file_preamble to understand current file structure.`
4073
4479
  * Generate tests for multiple functions, one at a time
4074
4480
  */
4075
4481
  async function generateTestsForFunctions(sourceFile, functionNames) {
4076
- console.log(`\n๐Ÿ“ Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
4482
+ // console.log(`\n๐Ÿ“ Generating tests for ${functionNames.length} selected function(s) in: ${sourceFile}\n`);
4077
4483
  const testFilePath = getTestFilePath(sourceFile);
4078
4484
  let testFileExists = fsSync.existsSync(testFilePath);
4485
+ // Read validation interval from config
4486
+ const validationInterval = CONFIG.validationInterval;
4079
4487
  // Process each function one at a time
4080
4488
  for (let i = 0; i < functionNames.length; i++) {
4081
4489
  const functionName = functionNames[i];
4490
+ // Clear spinner before showing section header
4491
+ if (globalSpinner && globalSpinner.isSpinning) {
4492
+ globalSpinner.stop();
4493
+ globalSpinner = null;
4494
+ }
4082
4495
  console.log(`\n${'='.repeat(80)}`);
4083
4496
  console.log(`Processing function ${i + 1}/${functionNames.length}: ${functionName}`);
4084
4497
  console.log(`${'='.repeat(80)}\n`);
4085
4498
  const passed = await generateTestForSingleFunction(sourceFile, functionName, testFilePath, testFileExists);
4086
4499
  // After first function completes, test file will exist for subsequent functions
4087
4500
  testFileExists = true;
4501
+ // Clear spinner before showing completion
4502
+ if (globalSpinner && globalSpinner.isSpinning) {
4503
+ globalSpinner.stop();
4504
+ globalSpinner = null;
4505
+ }
4088
4506
  if (passed) {
4089
4507
  console.log(`\nโœ… Function '${functionName}' tests completed successfully!`);
4090
4508
  }
4091
4509
  else {
4092
4510
  console.log(`\nโš ๏ธ Function '${functionName}' completed with issues. Continuing to next function...`);
4093
4511
  }
4512
+ // Periodic validation checkpoint (only if validation is enabled in config)
4513
+ if (validationInterval !== undefined && validationInterval !== null) {
4514
+ const isPeriodicCheckpoint = validationInterval > 0 && (i + 1) % validationInterval === 0;
4515
+ const isFinalFunction = i === functionNames.length - 1;
4516
+ if (isPeriodicCheckpoint || isFinalFunction) {
4517
+ console.log(`\n${'โ”€'.repeat(80)}`);
4518
+ console.log(`๐Ÿ“Š CHECKPOINT ${i + 1}/${functionNames.length}: Running full suite validation...`);
4519
+ console.log(`${'โ”€'.repeat(80)}`);
4520
+ await smartValidateTestSuite(sourceFile, testFilePath, functionNames.slice(0, i + 1));
4521
+ }
4522
+ }
4094
4523
  await new Promise(resolve => setTimeout(resolve, 5000));
4095
4524
  }
4096
4525
  console.log(`\n${'='.repeat(80)}`);
4097
4526
  console.log(`โœ… All ${functionNames.length} function(s) processed!`);
4098
4527
  console.log(`${'='.repeat(80)}\n`);
4099
- // FINAL VALIDATION: Run complete test suite and fix file-level issues
4100
- // await validateAndFixCompleteTestFile(sourceFile, testFilePath, functionNames);
4101
4528
  return testFilePath;
4102
4529
  }
4103
4530
  async function generateTestsForFunction() {
4104
4531
  console.log('\n๐ŸŽฏ Function-wise Test Generation\n');
4105
4532
  // List all files
4106
- console.log('๐Ÿ“‚ Scanning repository...\n');
4533
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4534
+ globalSpinner = ora('๐Ÿ“‚ Scanning repository...').start();
4535
+ }
4536
+ else {
4537
+ globalSpinner.text = '๐Ÿ“‚ Scanning repository...';
4538
+ }
4107
4539
  const files = await listFilesRecursive('.');
4108
4540
  if (files.length === 0) {
4109
- console.log('No source files found!');
4541
+ globalSpinner.fail('No source files found!');
4542
+ globalSpinner = null;
4110
4543
  return;
4111
4544
  }
4112
- console.log('Select a file:\n');
4545
+ globalSpinner.text = `Found ${files.length} source file(s)`;
4546
+ await new Promise(resolve => setTimeout(resolve, 200));
4547
+ globalSpinner.stop();
4548
+ globalSpinner = null;
4549
+ console.log('\nSelect a file:\n');
4113
4550
  files.forEach((file, index) => {
4114
4551
  console.log(`${index + 1}. ${file}`);
4115
4552
  });
@@ -4145,6 +4582,11 @@ async function generateTestsForFunction() {
4145
4582
  }
4146
4583
  console.log(`\nโœ… Selected functions: ${selectedFunctions.join(', ')}\n`);
4147
4584
  await generateTestsForFunctions(selectedFile, selectedFunctions);
4585
+ // Clear spinner before final message
4586
+ if (globalSpinner && globalSpinner.isSpinning) {
4587
+ globalSpinner.stop();
4588
+ globalSpinner = null;
4589
+ }
4148
4590
  console.log('\nโœจ Done!');
4149
4591
  }
4150
4592
  /**
@@ -4272,7 +4714,7 @@ Return ONLY the JSON array, nothing else.`;
4272
4714
  return [];
4273
4715
  }
4274
4716
  // Parse AI response to extract function names
4275
- const content = response.content.trim();
4717
+ const content = typeof response.content === 'string' ? response.content.trim() : JSON.stringify(response.content || '');
4276
4718
  // console.log(` ๐Ÿค– AI response: ${content}`);
4277
4719
  // Try to extract JSON array from response
4278
4720
  const jsonMatch = content.match(/\[.*\]/s);
@@ -4304,16 +4746,25 @@ Return ONLY the JSON array, nothing else.`;
4304
4746
  * Auto-generate tests for changed functions detected via git diff
4305
4747
  */
4306
4748
  async function autoGenerateTests() {
4307
- console.log('๐Ÿ” Scanning git changes...\n');
4749
+ if (!globalSpinner) {
4750
+ globalSpinner = ora('๐Ÿ” Scanning git changes...').start();
4751
+ }
4752
+ else {
4753
+ globalSpinner.text = '๐Ÿ” Scanning git changes...';
4754
+ globalSpinner.start();
4755
+ }
4308
4756
  try {
4309
4757
  // Get all changes from git diff
4310
4758
  const { fullDiff, files } = await getGitDiff();
4311
4759
  if (files.length === 0) {
4760
+ globalSpinner.stop();
4761
+ globalSpinner = null;
4312
4762
  console.log('โœ… No changes detected in source files.');
4313
4763
  console.log(' (Only staged and unstaged changes are checked)');
4314
4764
  return;
4315
4765
  }
4316
- console.log(`๐Ÿ“ Found changes in ${files.length} file(s)\n`);
4766
+ globalSpinner.text = `Found changes in ${files.length} file(s)`;
4767
+ await new Promise(resolve => setTimeout(resolve, 200));
4317
4768
  let totalFunctions = 0;
4318
4769
  let processedFiles = 0;
4319
4770
  let errorFiles = 0;
@@ -4322,25 +4773,41 @@ async function autoGenerateTests() {
4322
4773
  const { filePath, diff } = fileInfo;
4323
4774
  // Check if file exists
4324
4775
  if (!fsSync.existsSync(filePath)) {
4325
- console.log(`โญ๏ธ Skipping ${filePath} (file not found)`);
4776
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4777
+ globalSpinner = ora(`โญ๏ธ Skipping ${filePath} (file not found)`).start();
4778
+ }
4779
+ else {
4780
+ globalSpinner.text = `โญ๏ธ Skipping ${filePath} (file not found)`;
4781
+ }
4782
+ await new Promise(resolve => setTimeout(resolve, 100));
4326
4783
  continue;
4327
4784
  }
4328
- console.log(`\n๐Ÿ”„ Processing: ${filePath}`);
4329
- console.log(` ๐Ÿค– Analyzing diff with AI...`);
4785
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4786
+ globalSpinner = ora(`๐Ÿ”„ Processing: ${path.basename(filePath)}`).start();
4787
+ }
4788
+ else {
4789
+ globalSpinner.text = `๐Ÿ”„ Processing: ${path.basename(filePath)}`;
4790
+ }
4330
4791
  // Use AI to extract changed function names from diff
4792
+ globalSpinner.text = `๐Ÿค– Analyzing diff with AI: ${path.basename(filePath)}`;
4331
4793
  const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4332
4794
  // console.log('Changed functions are', changedFunctions);
4333
4795
  if (changedFunctions.length === 0) {
4334
- console.log(` โญ๏ธ No exported functions changed`);
4796
+ globalSpinner.text = `โญ๏ธ No exported functions changed in ${path.basename(filePath)}`;
4797
+ await new Promise(resolve => setTimeout(resolve, 150));
4335
4798
  continue;
4336
4799
  }
4337
- console.log(` ๐Ÿ“ฆ Changed functions: ${changedFunctions.join(', ')}`);
4800
+ globalSpinner.text = `Found ${changedFunctions.length} function(s): ${changedFunctions.join(', ')}`;
4801
+ await new Promise(resolve => setTimeout(resolve, 200));
4338
4802
  try {
4339
4803
  // Use existing generateTestsForFunctions
4340
4804
  await generateTestsForFunctions(filePath, changedFunctions);
4341
4805
  processedFiles++;
4342
4806
  totalFunctions += changedFunctions.length;
4343
- console.log(` โœ… Tests generated successfully`);
4807
+ if (globalSpinner) {
4808
+ globalSpinner.text = `Tests generated for ${path.basename(filePath)}`;
4809
+ await new Promise(resolve => setTimeout(resolve, 200));
4810
+ }
4344
4811
  }
4345
4812
  catch (error) {
4346
4813
  errorFiles++;
@@ -4351,6 +4818,11 @@ async function autoGenerateTests() {
4351
4818
  // Summary
4352
4819
  console.log('\n' + '='.repeat(60));
4353
4820
  console.log('๐Ÿ“Š Auto-Generation Summary');
4821
+ // Clear spinner before final summary
4822
+ if (globalSpinner && globalSpinner.isSpinning) {
4823
+ globalSpinner.stop();
4824
+ globalSpinner = null;
4825
+ }
4354
4826
  console.log('='.repeat(60));
4355
4827
  console.log(`โœ… Successfully processed: ${processedFiles} file(s)`);
4356
4828
  console.log(`๐Ÿ“ Functions tested: ${totalFunctions}`);
@@ -4369,11 +4841,526 @@ async function autoGenerateTests() {
4369
4841
  throw error;
4370
4842
  }
4371
4843
  }
4844
+ /**
4845
+ * Review code changes for quality, bugs, performance, and security issues
4846
+ */
4847
+ async function reviewChangedFiles() {
4848
+ if (!globalSpinner) {
4849
+ globalSpinner = ora('๐Ÿ” Scanning git changes for review...').start();
4850
+ }
4851
+ else {
4852
+ globalSpinner.text = '๐Ÿ” Scanning git changes for review...';
4853
+ globalSpinner.start();
4854
+ }
4855
+ try {
4856
+ // Get all changes from git diff
4857
+ const { fullDiff, files } = await getGitDiff();
4858
+ if (files.length === 0) {
4859
+ globalSpinner.stop();
4860
+ globalSpinner = null;
4861
+ console.log('โœ… No changes detected in source files.');
4862
+ console.log(' (Only staged and unstaged changes are checked)');
4863
+ return;
4864
+ }
4865
+ globalSpinner.text = `Found changes in ${files.length} file(s) to review`;
4866
+ await new Promise(resolve => setTimeout(resolve, 200));
4867
+ // Collect all changed files and their functions
4868
+ const filesToReview = [];
4869
+ for (const fileInfo of files) {
4870
+ const { filePath, diff } = fileInfo;
4871
+ // Check if file exists
4872
+ if (!fsSync.existsSync(filePath)) {
4873
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4874
+ globalSpinner = ora(`โญ๏ธ Skipping ${filePath} (file not found)`).start();
4875
+ }
4876
+ else {
4877
+ globalSpinner.text = `โญ๏ธ Skipping ${filePath} (file not found)`;
4878
+ }
4879
+ await new Promise(resolve => setTimeout(resolve, 100));
4880
+ continue;
4881
+ }
4882
+ if (!globalSpinner || !globalSpinner.isSpinning) {
4883
+ globalSpinner = ora(`๐Ÿ”„ Analyzing: ${filePath}`).start();
4884
+ }
4885
+ else {
4886
+ globalSpinner.text = `๐Ÿ”„ Analyzing: ${filePath}`;
4887
+ }
4888
+ // Use AI to extract changed function names from diff
4889
+ const changedFunctions = await getChangedFunctionsFromDiff(filePath, diff);
4890
+ if (changedFunctions.length === 0) {
4891
+ globalSpinner.text = `โญ๏ธ No exported functions changed in ${path.basename(filePath)}`;
4892
+ await new Promise(resolve => setTimeout(resolve, 150));
4893
+ continue;
4894
+ }
4895
+ globalSpinner.text = `Found ${changedFunctions.length} changed function(s) in ${path.basename(filePath)}`;
4896
+ await new Promise(resolve => setTimeout(resolve, 200));
4897
+ filesToReview.push({ filePath, diff, functions: changedFunctions });
4898
+ }
4899
+ if (filesToReview.length === 0) {
4900
+ console.log('\nโœ… No functions to review.');
4901
+ return;
4902
+ }
4903
+ // Generate unified review for all changes
4904
+ try {
4905
+ await generateUnifiedCodeReview(filesToReview);
4906
+ // Ensure spinner is cleared before final message
4907
+ if (globalSpinner && globalSpinner.isSpinning) {
4908
+ globalSpinner.stop();
4909
+ globalSpinner = null;
4910
+ }
4911
+ console.log('\nโœ… Code review completed!');
4912
+ }
4913
+ catch (error) {
4914
+ console.error(`\nโŒ Error during review: ${error.message}`);
4915
+ }
4916
+ // Summary
4917
+ console.log('\n' + '='.repeat(60));
4918
+ console.log('๐Ÿ“Š Code Review Summary');
4919
+ console.log('='.repeat(60));
4920
+ console.log(`โœ… Files reviewed: ${filesToReview.length}`);
4921
+ console.log(`๐Ÿ“ Total functions: ${filesToReview.reduce((sum, f) => sum + f.functions.length, 0)}`);
4922
+ console.log(`๐Ÿ“ Review saved to: reviews/code_review.md`);
4923
+ console.log('='.repeat(60));
4924
+ console.log('\nโœจ Done!');
4925
+ }
4926
+ catch (error) {
4927
+ if (error.message === 'Not a git repository') {
4928
+ console.error('โŒ Error: Not a git repository');
4929
+ console.error(' Review mode requires git to detect changes.');
4930
+ process.exit(1);
4931
+ }
4932
+ throw error;
4933
+ }
4934
+ }
4935
+ /**
4936
+ * Build AI prompt for a specific review step based on its ruleset
4937
+ */
4938
+ async function buildStepPrompt(step, filesToReview, stepOutputPath) {
4939
+ const filesContext = filesToReview.map(f => `- ${f.filePath}: ${f.functions.join(', ')}`).join('\n');
4940
+ const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
4941
+ // Read ruleset from markdown file
4942
+ const rulesetPath = path.join(process.cwd(), 'codeguard-ruleset', step.ruleset);
4943
+ let rulesetContent;
4944
+ try {
4945
+ rulesetContent = await fs.readFile(rulesetPath, 'utf-8');
4946
+ if (globalSpinner && globalSpinner.isSpinning) {
4947
+ globalSpinner.text = `๐Ÿ“– Loaded ruleset: ${step.ruleset}`;
4948
+ await new Promise(resolve => setTimeout(resolve, 100));
4949
+ }
4950
+ }
4951
+ catch (error) {
4952
+ if (globalSpinner && globalSpinner.isSpinning) {
4953
+ globalSpinner.stop();
4954
+ globalSpinner = null;
4955
+ }
4956
+ console.warn(`โš ๏ธ Could not read ruleset file: ${rulesetPath}`);
4957
+ console.warn(` Using default ruleset message. Error: ${error.message}`);
4958
+ rulesetContent = `Please review the code for ${step.name} following industry best practices.`;
4959
+ }
4960
+ return `You are a senior software engineer conducting a focused code review for: **${step.name}**
4961
+
4962
+ ## CONTEXT
4963
+ Review Focus: ${step.name}
4964
+ Category: ${step.category}
4965
+ Total files changed: ${filesToReview.length}
4966
+ Total functions changed: ${totalFunctions}
4967
+
4968
+ Changed files and functions:
4969
+ ${filesContext}
4970
+
4971
+ ## GIT DIFFS
4972
+
4973
+ ${filesToReview.map(f => `### ${f.filePath}
4974
+ \`\`\`diff
4975
+ ${f.diff}
4976
+ \`\`\`
4977
+ `).join('\n')}
4978
+
4979
+ ## YOUR TASK
4980
+ Conduct a focused code review ONLY for ${step.name}. Use the available tools to analyze the code thoroughly.
4981
+ Must complete and write the review within 1 minutes. Your review should be brief and to the point.
4982
+
4983
+ ## ANALYSIS STEPS
4984
+
4985
+ **Phase 1: Code Analysis (MANDATORY)**
4986
+ For each changed file:
4987
+ 1. analyze_file_ast(filePath) โ†’ Get file structure and all functions
4988
+ - OR use analyze_file_ast(filePath, functionName) for specific function reviews (token-efficient)
4989
+ 2. For each changed function:
4990
+ - get_function_ast(filePath, functionName) โ†’ Get implementation details
4991
+ 3. get_imports_ast(filePath) โ†’ Understand dependencies
4992
+ 4. get_type_definitions(filePath) โ†’ Review type safety
4993
+ 5. Use search_codebase() if needed to understand usage patterns
4994
+ 6. Use tools to analyze the code thoroughly. -> Where the functions is used and what all dependencies are there that call this function? What can be potential issues with the code?
4995
+
4996
+ **Phase 2: Review Analysis for ${step.name}**
4997
+
4998
+ Review the code against the following ruleset:
4999
+
5000
+ ${rulesetContent}
5001
+
5002
+ Also give attention to any industry specific points that are not covered by the ruleset for ${step.name}.
5003
+
5004
+ **Phase 3: Write Review**
5005
+
5006
+ Use the write_review tool to save findings to: ${stepOutputPath}
5007
+
5008
+ ## OUTPUT FORMAT
5009
+
5010
+ Your review MUST be in markdown format with the following structure:
5011
+
5012
+ \`\`\`markdown
5013
+ # ${step.name}
5014
+
5015
+
5016
+ ## Findings
5017
+
5018
+ ### ๐Ÿ”ด Critical Issues
5019
+
5020
+ #### [Issue Title]
5021
+ **File**: \`filePath\`
5022
+ **Function**: \`functionName\`
5023
+ **Severity**: Critical
5024
+
5025
+ **Issue**:
5026
+ [Description]
5027
+
5028
+ **Current Code**:
5029
+ \`\`\`typescript
5030
+ // problematic code
5031
+ \`\`\`
5032
+
5033
+ **Recommended Code**:
5034
+ \`\`\`typescript
5035
+ // improved code
5036
+ \`\`\`
5037
+
5038
+ **Rationale**:
5039
+ [Why this is important]
5040
+
5041
+ ---
5042
+
5043
+ ### ๐ŸŸ  High Priority Issues
5044
+ [Same format as above]
5045
+
5046
+ ---
5047
+
5048
+ ### ๐ŸŸก Medium Priority Issues
5049
+ [Same format as above]
5050
+
5051
+ ---
5052
+
5053
+ ## Positive Aspects
5054
+ [What was done well in this area]
5055
+
5056
+ ## Recommendations
5057
+ [Specific recommendations for ${step.name}]
5058
+ \`\`\`
5059
+
5060
+ ## CRITICAL REMINDERS
5061
+
5062
+ - ALWAYS use tools to analyze code before reviewing
5063
+ - Focus ONLY on ${step.name} - do not review other aspects
5064
+ - Be specific and actionable
5065
+ - Include code examples
5066
+ - Use write_review tool to save to: ${stepOutputPath}
5067
+ - Must complete within 30 seconds
5068
+ - Use avaiable tools extensively to analyze the code.
5069
+ - The review should be based on the ruleset, it should be to the point without too much text.
5070
+ - Before writing the review, make sure to analyze the code thoroughly using the tools available, how similar code is used in the codebase.
5071
+ - Keep the review short and to the point, do not write too much text.
5072
+
5073
+ **START**: Begin by calling analyze_file_ast on each changed file.`;
5074
+ }
5075
+ /**
5076
+ * Execute a single review step with AI
5077
+ */
5078
+ async function executeReviewStep(step, filesToReview, stepOutputPath) {
5079
+ // console.log(`\n๐Ÿ” Running ${step.name} review step...`);
5080
+ try {
5081
+ const prompt = await buildStepPrompt(step, filesToReview, stepOutputPath);
5082
+ const messages = [
5083
+ {
5084
+ role: 'user',
5085
+ content: prompt
5086
+ }
5087
+ ];
5088
+ let iterations = 0;
5089
+ const maxIterations = 30;
5090
+ let reviewWritten = false;
5091
+ while (iterations < maxIterations) {
5092
+ iterations++;
5093
+ const response = await callAI(messages, TOOLS_FOR_CODE_REVIEW, CONFIG.aiProvider);
5094
+ if (response.content) {
5095
+ const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
5096
+ messages.push({ role: 'assistant', content });
5097
+ // Check if review is complete
5098
+ if (typeof content === 'string' && (content.toLowerCase().includes('review complete') ||
5099
+ content.toLowerCase().includes('review has been written'))) {
5100
+ if (reviewWritten) {
5101
+ break;
5102
+ }
5103
+ }
5104
+ }
5105
+ if (!response.toolCalls || response.toolCalls.length === 0) {
5106
+ if (reviewWritten) {
5107
+ break;
5108
+ }
5109
+ // Prompt AI to continue
5110
+ messages.push({
5111
+ role: 'user',
5112
+ content: `Please use the write_review tool NOW to save your ${step.name} review to ${stepOutputPath}. Include all findings with severity levels and code examples.`
5113
+ });
5114
+ continue;
5115
+ }
5116
+ // Execute tool calls
5117
+ const toolResults = [];
5118
+ for (const toolCall of response.toolCalls) {
5119
+ const toolResult = await executeTool(toolCall.name, toolCall.input);
5120
+ toolResults.push({ id: toolCall.id, name: toolCall.name, result: toolResult });
5121
+ // Track if review was written
5122
+ if (toolCall.name === 'write_review' && toolResult.success) {
5123
+ reviewWritten = true;
5124
+ }
5125
+ }
5126
+ // Add tool calls and results to messages (format differs by provider)
5127
+ if (CONFIG.aiProvider === 'openai') {
5128
+ messages.push({
5129
+ role: 'assistant',
5130
+ tool_calls: response.toolCalls.map(tc => ({
5131
+ id: tc.id,
5132
+ type: 'function',
5133
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
5134
+ }))
5135
+ });
5136
+ for (const tr of toolResults) {
5137
+ messages.push({
5138
+ role: 'tool',
5139
+ tool_call_id: tr.id,
5140
+ content: JSON.stringify(tr.result)
5141
+ });
5142
+ }
5143
+ }
5144
+ else if (CONFIG.aiProvider === 'gemini') {
5145
+ for (const toolCall of response.toolCalls) {
5146
+ messages.push({
5147
+ role: 'model',
5148
+ functionCall: { name: toolCall.name, args: toolCall.input }
5149
+ });
5150
+ const result = toolResults.find(tr => tr.name === toolCall.name);
5151
+ messages.push({
5152
+ role: 'user',
5153
+ functionResponse: { name: toolCall.name, response: result?.result }
5154
+ });
5155
+ }
5156
+ }
5157
+ else {
5158
+ // Claude
5159
+ messages.push({
5160
+ role: 'assistant',
5161
+ content: response.toolCalls.map(tc => ({
5162
+ type: 'tool_use',
5163
+ id: tc.id,
5164
+ name: tc.name,
5165
+ input: tc.input
5166
+ }))
5167
+ });
5168
+ messages.push({
5169
+ role: 'user',
5170
+ content: toolResults.map(tr => ({
5171
+ type: 'tool_result',
5172
+ tool_use_id: tr.id,
5173
+ content: JSON.stringify(tr.result)
5174
+ }))
5175
+ });
5176
+ }
5177
+ }
5178
+ if (!reviewWritten) {
5179
+ throw new Error(`Could not complete ${step.name} review within iteration limit`);
5180
+ }
5181
+ console.log(` โœ… ${step.name} review completed`);
5182
+ return { success: true, stepId: step.id, stepName: step.name };
5183
+ }
5184
+ catch (error) {
5185
+ console.error(` โŒ ${step.name} review failed: ${error.message}`);
5186
+ return { success: false, error: error.message, stepId: step.id, stepName: step.name };
5187
+ }
5188
+ }
5189
+ /**
5190
+ * Merge review results from multiple steps into a single unified review
5191
+ */
5192
+ async function mergeReviewResults(stepResults, filesToReview, finalOutputPath) {
5193
+ const timestamp = new Date().toISOString().split('T')[0];
5194
+ const totalFunctions = filesToReview.reduce((sum, f) => sum + f.functions.length, 0);
5195
+ // Start building the merged review
5196
+ let mergedReview = `# Code Review
5197
+
5198
+ **Date**: ${timestamp}
5199
+ **Reviewer**: AI Code Review System
5200
+ **Files Changed**: ${filesToReview.length}
5201
+ **Functions Changed**: ${totalFunctions}
5202
+
5203
+ ---
5204
+
5205
+ ## Summary
5206
+
5207
+ This review covers ${filesToReview.length} file(s) with ${totalFunctions} changed function(s). The review was conducted across multiple aspects: ${stepResults.map(r => r.stepName).join(', ')}.
5208
+
5209
+ ---
5210
+
5211
+ ## Files Changed
5212
+
5213
+ ${filesToReview.map(f => `- **${f.filePath}**
5214
+ - Functions: ${f.functions.join(', ')}`).join('\n')}
5215
+
5216
+ ---
5217
+
5218
+ `;
5219
+ // Add each step's findings
5220
+ for (const result of stepResults) {
5221
+ const stepFilePath = path.join('reviews', '.tmp', `step-${result.stepId}.md`);
5222
+ if (result.success) {
5223
+ try {
5224
+ // Read the step review file
5225
+ if (fsSync.existsSync(stepFilePath)) {
5226
+ const stepContent = await fs.readFile(stepFilePath, 'utf-8');
5227
+ // Extract content after the first heading (skip the step title)
5228
+ const lines = stepContent.split('\n');
5229
+ let contentStart = 0;
5230
+ for (let i = 0; i < lines.length; i++) {
5231
+ if (lines[i].startsWith('# ')) {
5232
+ contentStart = i + 1;
5233
+ break;
5234
+ }
5235
+ }
5236
+ const content = lines.slice(contentStart).join('\n').trim();
5237
+ // Add as a section in the merged review
5238
+ mergedReview += `## ${result.stepName}\n\n${content}\n\n---\n\n`;
5239
+ }
5240
+ else {
5241
+ mergedReview += `## ${result.stepName}\n\nโš ๏ธ Review file not found.\n\n---\n\n`;
5242
+ }
5243
+ }
5244
+ catch (error) {
5245
+ mergedReview += `## ${result.stepName}\n\nโš ๏ธ Error reading review: ${error.message}\n\n---\n\n`;
5246
+ }
5247
+ }
5248
+ else {
5249
+ mergedReview += `## ${result.stepName}\n\nโŒ **Review step failed**: ${result.error}\n\n---\n\n`;
5250
+ }
5251
+ }
5252
+ // Add conclusion
5253
+ mergedReview += `## Conclusion
5254
+
5255
+ This comprehensive review analyzed the code changes from multiple perspectives. Please address the findings based on their severity levels, starting with critical issues.
5256
+
5257
+ The review was conducted using AI-powered analysis with access to the full codebase context.
5258
+ `;
5259
+ // Ensure reviews directory exists
5260
+ const reviewsDir = path.dirname(finalOutputPath);
5261
+ if (!fsSync.existsSync(reviewsDir)) {
5262
+ await fs.mkdir(reviewsDir, { recursive: true });
5263
+ }
5264
+ // Write the merged review
5265
+ await fs.writeFile(finalOutputPath, mergedReview, 'utf-8');
5266
+ console.log(`\nโœ… Merged review written to: ${finalOutputPath}`);
5267
+ // Clean up temporary files
5268
+ try {
5269
+ const tmpDir = path.join('reviews', '.tmp');
5270
+ if (fsSync.existsSync(tmpDir)) {
5271
+ const tmpFiles = await fs.readdir(tmpDir);
5272
+ for (const file of tmpFiles) {
5273
+ await fs.unlink(path.join(tmpDir, file));
5274
+ }
5275
+ await fs.rmdir(tmpDir);
5276
+ }
5277
+ }
5278
+ catch (error) {
5279
+ // Ignore cleanup errors
5280
+ }
5281
+ }
5282
+ /**
5283
+ * Generate unified code review for all changed files
5284
+ */
5285
+ async function generateUnifiedCodeReview(filesToReview) {
5286
+ const reviewFilePath = path.join('reviews', 'code_review.md');
5287
+ // Get enabled review steps from config
5288
+ const enabledSteps = CONFIG.reviewSteps.filter(step => step.enabled);
5289
+ if (enabledSteps.length === 0) {
5290
+ console.log('\nโš ๏ธ No review steps enabled. Please enable at least one step in codeguard.json');
5291
+ return;
5292
+ }
5293
+ console.log(`\n๐Ÿ“‹ Running ${enabledSteps.length} review step(s): ${enabledSteps.map(s => s.name).join(', ')}`);
5294
+ // Ensure temporary directory exists
5295
+ const tmpDir = path.join('reviews', '.tmp');
5296
+ if (!fsSync.existsSync(tmpDir)) {
5297
+ await fs.mkdir(tmpDir, { recursive: true });
5298
+ }
5299
+ // Execute review steps
5300
+ let stepResults;
5301
+ if (CONFIG.reviewExecutionMode === 'parallel') {
5302
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5303
+ globalSpinner = ora('๐Ÿ”„ Executing review steps in parallel...').start();
5304
+ }
5305
+ else {
5306
+ globalSpinner.text = '๐Ÿ”„ Executing review steps in parallel...';
5307
+ }
5308
+ // Run all steps in parallel
5309
+ const stepPromises = enabledSteps.map(step => {
5310
+ const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
5311
+ return executeReviewStep(step, filesToReview, stepOutputPath);
5312
+ });
5313
+ stepResults = await Promise.all(stepPromises);
5314
+ }
5315
+ else {
5316
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5317
+ globalSpinner = ora('๐Ÿ”„ Executing review steps sequentially...').start();
5318
+ }
5319
+ else {
5320
+ globalSpinner.text = '๐Ÿ”„ Executing review steps sequentially...';
5321
+ }
5322
+ // Run steps sequentially
5323
+ stepResults = [];
5324
+ for (const step of enabledSteps) {
5325
+ if (globalSpinner) {
5326
+ globalSpinner.text = `๐Ÿ” Running ${step.name} review step...`;
5327
+ }
5328
+ const stepOutputPath = path.join('reviews', '.tmp', `step-${step.id}.md`);
5329
+ const result = await executeReviewStep(step, filesToReview, stepOutputPath);
5330
+ stepResults.push(result);
5331
+ if (globalSpinner && result.success) {
5332
+ globalSpinner.text = `${step.name} review completed`;
5333
+ await new Promise(resolve => setTimeout(resolve, 200));
5334
+ }
5335
+ }
5336
+ }
5337
+ // Merge results into final review
5338
+ await mergeReviewResults(stepResults, filesToReview, reviewFilePath);
5339
+ // Clear global spinner before final summary
5340
+ if (globalSpinner && globalSpinner.isSpinning) {
5341
+ globalSpinner.stop();
5342
+ globalSpinner = null;
5343
+ }
5344
+ // Summary
5345
+ const successCount = stepResults.filter(r => r.success).length;
5346
+ const failCount = stepResults.filter(r => !r.success).length;
5347
+ console.log(`\nโœ… Review complete: ${successCount} step(s) succeeded${failCount > 0 ? `, ${failCount} failed` : ''}`);
5348
+ }
4372
5349
  async function main() {
4373
5350
  console.log('๐Ÿงช AI-Powered Unit Test Generator with AST Analysis\n');
4374
- // Check for auto mode from CLI arguments EARLY (before any prompts)
5351
+ // Parse command from CLI arguments EARLY (before any prompts)
4375
5352
  const args = process.argv.slice(2);
4376
- const isAutoMode = args.includes('auto');
5353
+ const command = args[0]; // First argument is the command: 'auto', 'test', 'review', or undefined
5354
+ // Validate command if provided
5355
+ if (command && !['auto', 'test', 'review', 'doc'].includes(command)) {
5356
+ console.error('โŒ Invalid command. Usage:\n');
5357
+ console.error(' testgen auto - Review changes and generate tests');
5358
+ console.error(' testgen test - Generate tests only');
5359
+ console.error(' testgen review - Review changes only');
5360
+ console.error(' testgen doc - Generate API documentation');
5361
+ console.error(' testgen - Interactive mode\n');
5362
+ process.exit(1);
5363
+ }
4377
5364
  // Load configuration from codeguard.json
4378
5365
  try {
4379
5366
  CONFIG = (0, config_1.loadConfig)();
@@ -4396,20 +5383,52 @@ async function main() {
4396
5383
  console.error('npm install @babel/parser @babel/traverse ts-node\n');
4397
5384
  process.exit(1);
4398
5385
  }
4399
- // If auto mode, skip indexing setup and proceed directly
4400
- if (isAutoMode) {
4401
- console.log('๐Ÿค– Auto Mode: Detecting changes via git diff\n');
5386
+ // If command mode (auto, test, review, doc), prepare index (build/update) and proceed directly
5387
+ if (command === 'auto' || command === 'test' || command === 'review' || command === 'doc') {
5388
+ const modeLabel = command === 'auto' ? 'Auto Mode (Review + Test)' :
5389
+ command === 'test' ? 'Test Generation Mode' :
5390
+ command === 'review' ? 'Code Review Mode' :
5391
+ CONFIG.repoDoc ? 'Documentation Generation Mode (Full Repository)' : 'Documentation Generation Mode (Changes Only)';
5392
+ console.log(`๐Ÿค– ${modeLabel}: Detecting changes via git diff\n`);
4402
5393
  console.log(`โœ… Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
4403
- // Initialize indexer if it exists, but don't prompt
5394
+ // Initialize and prepare codebase indexer (build or update) without prompts
4404
5395
  globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
4405
5396
  const hasExistingIndex = globalIndexer.hasIndex();
4406
5397
  if (hasExistingIndex) {
4407
- await globalIndexer.loadIndex();
5398
+ const loaded = await globalIndexer.loadIndex();
5399
+ if (loaded) {
5400
+ const staleFiles = globalIndexer.getStaleFiles();
5401
+ if (staleFiles.length > 0) {
5402
+ console.log(`๐Ÿ”„ Updating ${staleFiles.length} modified file(s) in index...`);
5403
+ await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
5404
+ }
5405
+ }
4408
5406
  }
4409
5407
  else {
4410
- globalIndexer = null;
5408
+ // Build index once for the whole repo to speed up AST queries
5409
+ console.log('๐Ÿ“ฆ Building codebase index for faster analysis...');
5410
+ await globalIndexer.buildIndex('.', analyzeFileAST);
5411
+ }
5412
+ // Execute based on command
5413
+ if (command === 'auto') {
5414
+ // Run review first, then tests
5415
+ await reviewChangedFiles();
5416
+ console.log('\n' + '='.repeat(60) + '\n');
5417
+ await autoGenerateTests();
5418
+ }
5419
+ else if (command === 'test') {
5420
+ // Only generate tests
5421
+ await autoGenerateTests();
5422
+ }
5423
+ else if (command === 'review') {
5424
+ // Only review changes
5425
+ await reviewChangedFiles();
5426
+ }
5427
+ else if (command === 'doc') {
5428
+ // Only generate documentation
5429
+ console.log('Currently in development. Please try again later.');
5430
+ // await generateDocumentationMode();
4411
5431
  }
4412
- await autoGenerateTests();
4413
5432
  return;
4414
5433
  }
4415
5434
  // Optional: Codebase Indexing
@@ -4425,16 +5444,18 @@ async function main() {
4425
5444
  // Check for stale files (modified since last index)
4426
5445
  const staleFiles = globalIndexer.getStaleFiles();
4427
5446
  if (staleFiles.length > 0) {
4428
- console.log(`๐Ÿ”„ Updating ${staleFiles.length} modified file(s)...`);
4429
- // Show which files are being updated (if not too many)
4430
- if (staleFiles.length <= 5) {
4431
- staleFiles.forEach(f => console.log(` ๐Ÿ“ ${path.basename(f)}`));
5447
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5448
+ globalSpinner = ora(`๐Ÿ”„ Updating ${staleFiles.length} modified file(s)...`).start();
4432
5449
  }
4433
5450
  else {
4434
- console.log(` ๐Ÿ“ Updating ${staleFiles.length} files...`);
5451
+ globalSpinner.text = `๐Ÿ”„ Updating ${staleFiles.length} modified file(s)...`;
4435
5452
  }
4436
5453
  await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
4437
- console.log(`โœ… Index updated!\n`);
5454
+ globalSpinner.text = `Index updated - ${staleFiles.length} file(s) refreshed`;
5455
+ await new Promise(resolve => setTimeout(resolve, 200));
5456
+ globalSpinner.stop();
5457
+ globalSpinner = null;
5458
+ console.log(); // Empty line for spacing
4438
5459
  }
4439
5460
  else {
4440
5461
  console.log('โœ… All files up to date!\n');
@@ -4497,13 +5518,23 @@ async function main() {
4497
5518
  case '1':
4498
5519
  default:
4499
5520
  // File-wise mode (original functionality)
4500
- console.log('\n๐Ÿ“‚ Scanning repository...\n');
5521
+ if (!globalSpinner || !globalSpinner.isSpinning) {
5522
+ globalSpinner = ora('๐Ÿ“‚ Scanning repository...').start();
5523
+ }
5524
+ else {
5525
+ globalSpinner.text = '๐Ÿ“‚ Scanning repository...';
5526
+ }
4501
5527
  const files = await listFilesRecursive('.');
4502
5528
  if (files.length === 0) {
4503
- console.log('No source files found!');
5529
+ globalSpinner.fail('No source files found!');
5530
+ globalSpinner = null;
4504
5531
  return;
4505
5532
  }
4506
- console.log('Select a file to generate tests:\n');
5533
+ globalSpinner.text = `Found ${files.length} source file(s)`;
5534
+ await new Promise(resolve => setTimeout(resolve, 200));
5535
+ globalSpinner.stop();
5536
+ globalSpinner = null;
5537
+ console.log('\nSelect a file to generate tests:\n');
4507
5538
  files.forEach((file, index) => {
4508
5539
  console.log(`${index + 1}. ${file}`);
4509
5540
  });
@@ -4514,6 +5545,11 @@ async function main() {
4514
5545
  return;
4515
5546
  }
4516
5547
  await generateTests(selectedFile);
5548
+ // Clear spinner before final message
5549
+ if (globalSpinner && globalSpinner.isSpinning) {
5550
+ globalSpinner.stop();
5551
+ globalSpinner = null;
5552
+ }
4517
5553
  console.log('\nโœจ Done!');
4518
5554
  break;
4519
5555
  }