codeguard-testgen 1.0.0

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 ADDED
@@ -0,0 +1,2720 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /** //This version has lefitimate failure detection and reporting.
4
+ * AI-Powered Unit Test Generator with AST Analysis
5
+ * Supports OpenAI, Gemini, and Claude with function calling
6
+ *
7
+ * Usage: npx ts-node src/testgen.ts
8
+ *
9
+ * Required packages: npm install @babel/parser @babel/traverse ts-node
10
+ *
11
+ * Required environment variables:
12
+ * - OPENAI_API_KEY (for OpenAI)
13
+ * - GEMINI_API_KEY (for Gemini)
14
+ * - ANTHROPIC_API_KEY (for Claude)
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.TOOLS = exports.CodebaseIndexer = void 0;
18
+ exports.main = main;
19
+ exports.generateTests = generateTests;
20
+ exports.generateTestsForFolder = generateTestsForFolder;
21
+ exports.generateTestsForFunction = generateTestsForFunction;
22
+ exports.generateTestsForFunctions = generateTestsForFunctions;
23
+ exports.executeTool = executeTool;
24
+ exports.analyzeFileAST = analyzeFileAST;
25
+ exports.getFunctionAST = getFunctionAST;
26
+ exports.getImportsAST = getImportsAST;
27
+ exports.getTypeDefinitions = getTypeDefinitions;
28
+ exports.getClassMethods = getClassMethods;
29
+ exports.replaceFunctionTests = replaceFunctionTests;
30
+ exports.deleteLines = deleteLines;
31
+ exports.insertLines = insertLines;
32
+ exports.replaceLines = replaceLines;
33
+ const fs = require("fs/promises");
34
+ const fsSync = require("fs");
35
+ const path = require("path");
36
+ const child_process_1 = require("child_process");
37
+ const readline = require("readline");
38
+ // AST parsers
39
+ const babelParser = require("@babel/parser");
40
+ const traverse = require('@babel/traverse').default;
41
+ // Codebase indexer (optional)
42
+ const codebaseIndexer_1 = require("./codebaseIndexer");
43
+ Object.defineProperty(exports, "CodebaseIndexer", { enumerable: true, get: function () { return codebaseIndexer_1.CodebaseIndexer; } });
44
+ // Configuration loader
45
+ const config_1 = require("./config");
46
+ // Configuration - will be loaded from codeguard.json
47
+ let CONFIG;
48
+ // Global indexer instance (optional - only initialized if user chooses to index)
49
+ let globalIndexer = null;
50
+ // AI Provider configurations - models will be set from CONFIG
51
+ function getAIProviders() {
52
+ return {
53
+ openai: {
54
+ url: 'https://api.openai.com/v1/chat/completions',
55
+ model: CONFIG.models.openai,
56
+ headers: (apiKey) => ({
57
+ 'Content-Type': 'application/json',
58
+ 'Authorization': `Bearer ${apiKey}`
59
+ })
60
+ },
61
+ gemini: {
62
+ url: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.models.gemini}:generateContent?key=${apiKey}`,
63
+ model: CONFIG.models.gemini,
64
+ headers: () => ({
65
+ 'Content-Type': 'application/json'
66
+ })
67
+ },
68
+ claude: {
69
+ url: 'https://api.anthropic.com/v1/messages',
70
+ model: CONFIG.models.claude,
71
+ headers: (apiKey) => ({
72
+ 'Content-Type': 'application/json',
73
+ 'x-api-key': apiKey || '',
74
+ 'anthropic-version': '2023-06-01'
75
+ })
76
+ }
77
+ };
78
+ }
79
+ // Tools available to AI
80
+ const TOOLS = [
81
+ {
82
+ name: 'read_file',
83
+ description: 'Read the contents of a file from the repository',
84
+ input_schema: {
85
+ type: 'object',
86
+ properties: {
87
+ file_path: {
88
+ type: 'string',
89
+ description: 'The relative path to the file from the repository root'
90
+ }
91
+ },
92
+ required: ['file_path']
93
+ }
94
+ },
95
+ {
96
+ name: 'analyze_file_ast',
97
+ description: 'Parse file using AST and extract detailed information about all functions, classes, types, and exports',
98
+ input_schema: {
99
+ type: 'object',
100
+ properties: {
101
+ file_path: {
102
+ type: 'string',
103
+ description: 'The relative path to the source file'
104
+ }
105
+ },
106
+ required: ['file_path']
107
+ }
108
+ },
109
+ {
110
+ name: 'get_function_ast',
111
+ description: 'Get detailed AST analysis of a specific function including parameters, return type, body, and dependencies',
112
+ input_schema: {
113
+ type: 'object',
114
+ properties: {
115
+ file_path: {
116
+ type: 'string',
117
+ description: 'The relative path to the file'
118
+ },
119
+ function_name: {
120
+ type: 'string',
121
+ description: 'The name of the function to analyze'
122
+ }
123
+ },
124
+ required: ['file_path', 'function_name']
125
+ }
126
+ },
127
+ {
128
+ name: 'get_imports_ast',
129
+ description: 'Extract all import statements with detailed information using AST parsing',
130
+ input_schema: {
131
+ type: 'object',
132
+ properties: {
133
+ file_path: {
134
+ type: 'string',
135
+ description: 'The relative path to the file'
136
+ }
137
+ },
138
+ required: ['file_path']
139
+ }
140
+ },
141
+ {
142
+ name: 'get_type_definitions',
143
+ description: 'Extract TypeScript type definitions, interfaces, and type aliases from a file',
144
+ input_schema: {
145
+ type: 'object',
146
+ properties: {
147
+ file_path: {
148
+ type: 'string',
149
+ description: 'The relative path to the file'
150
+ }
151
+ },
152
+ required: ['file_path']
153
+ }
154
+ },
155
+ {
156
+ name: 'resolve_import_path',
157
+ description: 'Resolve a relative import path to an absolute path',
158
+ input_schema: {
159
+ type: 'object',
160
+ properties: {
161
+ from_file: {
162
+ type: 'string',
163
+ description: 'The file containing the import'
164
+ },
165
+ import_path: {
166
+ type: 'string',
167
+ description: 'The import path to resolve'
168
+ }
169
+ },
170
+ required: ['from_file', 'import_path']
171
+ }
172
+ },
173
+ {
174
+ name: 'get_class_methods',
175
+ description: 'Extract all methods from a class using AST',
176
+ input_schema: {
177
+ type: 'object',
178
+ properties: {
179
+ file_path: {
180
+ type: 'string',
181
+ description: 'The relative path to the file'
182
+ },
183
+ class_name: {
184
+ type: 'string',
185
+ description: 'The name of the class'
186
+ }
187
+ },
188
+ required: ['file_path', 'class_name']
189
+ }
190
+ },
191
+ {
192
+ name: 'write_test_file',
193
+ description: 'Write the complete test file content. This will OVERWRITE the entire file! Only use this for NEW test files or when regenerating ALL tests. If test file exists and you only need to update specific functions, use replace_function_tests instead!',
194
+ input_schema: {
195
+ type: 'object',
196
+ properties: {
197
+ file_path: {
198
+ type: 'string',
199
+ description: 'The path where the test file should be written'
200
+ },
201
+ content: {
202
+ type: 'string',
203
+ description: 'The complete content of the test file'
204
+ },
205
+ source_file: {
206
+ type: 'string',
207
+ description: 'The path to the source file being tested (used to validate all functions are tested)'
208
+ }
209
+ },
210
+ required: ['file_path', 'content']
211
+ }
212
+ },
213
+ {
214
+ name: 'edit_test_file',
215
+ description: 'Edit an existing test file by replacing specific sections. If this fails, use write_test_file to overwrite the entire file instead.',
216
+ input_schema: {
217
+ type: 'object',
218
+ properties: {
219
+ file_path: {
220
+ type: 'string',
221
+ description: 'The path to the test file'
222
+ },
223
+ old_content: {
224
+ type: 'string',
225
+ description: 'The content to be replaced (whitespace-normalized matching). Can be empty to overwrite entire file.'
226
+ },
227
+ new_content: {
228
+ type: 'string',
229
+ description: 'The new content to insert'
230
+ }
231
+ },
232
+ required: ['file_path', 'old_content', 'new_content']
233
+ }
234
+ },
235
+ {
236
+ name: 'replace_function_tests',
237
+ description: 'Replace or add tests for a specific function in an existing test file. This tool PRESERVES all other existing tests! Use this when test file exists and you want to update only specific function tests without affecting other tests.',
238
+ input_schema: {
239
+ type: 'object',
240
+ properties: {
241
+ test_file_path: {
242
+ type: 'string',
243
+ description: 'The path to the test file'
244
+ },
245
+ function_name: {
246
+ type: 'string',
247
+ description: 'The name of the function whose tests should be replaced'
248
+ },
249
+ new_test_content: {
250
+ type: 'string',
251
+ description: 'The complete describe block for the function tests (e.g., describe("functionName", () => { test(...) }))'
252
+ }
253
+ },
254
+ required: ['test_file_path', 'function_name', 'new_test_content']
255
+ }
256
+ },
257
+ {
258
+ name: 'run_tests',
259
+ description: 'Run Jest tests for a specific test file',
260
+ input_schema: {
261
+ type: 'object',
262
+ properties: {
263
+ test_file_path: {
264
+ type: 'string',
265
+ description: 'The path to the test file to run'
266
+ }
267
+ },
268
+ required: ['test_file_path']
269
+ }
270
+ },
271
+ {
272
+ name: 'list_directory',
273
+ description: 'List all files and directories in a given path to discover available modules',
274
+ input_schema: {
275
+ type: 'object',
276
+ properties: {
277
+ directory_path: {
278
+ type: 'string',
279
+ description: 'The directory path to list (e.g., "src/models", "src/helpers")'
280
+ }
281
+ },
282
+ required: ['directory_path']
283
+ }
284
+ },
285
+ {
286
+ name: 'find_file',
287
+ description: 'Search for a file by name in the repository to find its exact path',
288
+ input_schema: {
289
+ type: 'object',
290
+ properties: {
291
+ filename: {
292
+ type: 'string',
293
+ description: 'The filename to search for (e.g., "agent.helper.ts", "response-wrapper.ts")'
294
+ }
295
+ },
296
+ required: ['filename']
297
+ }
298
+ },
299
+ {
300
+ name: 'calculate_relative_path',
301
+ description: 'Calculate the correct relative import path from one file to another',
302
+ input_schema: {
303
+ type: 'object',
304
+ properties: {
305
+ from_file: {
306
+ type: 'string',
307
+ description: 'The file that will contain the import (e.g., "src/tests/index.test.ts")'
308
+ },
309
+ to_file: {
310
+ type: 'string',
311
+ description: 'The file being imported (e.g., "src/models/serviceDesk.models.ts")'
312
+ }
313
+ },
314
+ required: ['from_file', 'to_file']
315
+ }
316
+ },
317
+ {
318
+ name: 'report_legitimate_failure',
319
+ 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.',
320
+ input_schema: {
321
+ type: 'object',
322
+ properties: {
323
+ test_file_path: {
324
+ type: 'string',
325
+ description: 'The path to the test file'
326
+ },
327
+ failing_tests: {
328
+ type: 'array',
329
+ items: { type: 'string' },
330
+ description: 'List of test names that are legitimately failing'
331
+ },
332
+ reason: {
333
+ type: 'string',
334
+ description: 'Explanation of why the failures are legitimate (e.g., "Function returns undefined instead of expected object", "Missing null check causes TypeError")'
335
+ },
336
+ source_code_issue: {
337
+ type: 'string',
338
+ description: 'Description of the bug in the source code that causes the failure'
339
+ }
340
+ },
341
+ required: ['test_file_path', 'failing_tests', 'reason', 'source_code_issue']
342
+ }
343
+ },
344
+ {
345
+ name: 'delete_lines',
346
+ description: 'Delete specific lines from a file by line number range. Useful for removing incorrect imports, mocks, or test code. Use this after reading the file to know exact line numbers.',
347
+ input_schema: {
348
+ type: 'object',
349
+ properties: {
350
+ file_path: {
351
+ type: 'string',
352
+ description: 'The path to the file to edit'
353
+ },
354
+ start_line: {
355
+ type: 'number',
356
+ description: 'The starting line number to delete (1-indexed, inclusive)'
357
+ },
358
+ end_line: {
359
+ type: 'number',
360
+ description: 'The ending line number to delete (1-indexed, inclusive)'
361
+ }
362
+ },
363
+ required: ['file_path', 'start_line', 'end_line']
364
+ }
365
+ },
366
+ {
367
+ name: 'insert_lines',
368
+ description: 'Insert new lines at a specific position in a file. Useful for adding missing imports, mocks, or test cases. Use this after reading the file to know exact line numbers.',
369
+ input_schema: {
370
+ type: 'object',
371
+ properties: {
372
+ file_path: {
373
+ type: 'string',
374
+ description: 'The path to the file to edit'
375
+ },
376
+ line_number: {
377
+ type: 'number',
378
+ description: 'The line number where content should be inserted (1-indexed). New content will be inserted BEFORE this line. Use 1 to insert at the beginning, or file length + 1 to append.'
379
+ },
380
+ content: {
381
+ type: 'string',
382
+ description: 'The content to insert (can be multiple lines separated by \\n)'
383
+ }
384
+ },
385
+ required: ['file_path', 'line_number', 'content']
386
+ }
387
+ },
388
+ {
389
+ name: 'replace_lines',
390
+ description: 'Replace a range of lines with new content. Combines delete and insert for efficient line-based edits. Use this after reading the file to know exact line numbers.',
391
+ input_schema: {
392
+ type: 'object',
393
+ properties: {
394
+ file_path: {
395
+ type: 'string',
396
+ description: 'The path to the file to edit'
397
+ },
398
+ start_line: {
399
+ type: 'number',
400
+ description: 'The starting line number to replace (1-indexed, inclusive)'
401
+ },
402
+ end_line: {
403
+ type: 'number',
404
+ description: 'The ending line number to replace (1-indexed, inclusive)'
405
+ },
406
+ new_content: {
407
+ type: 'string',
408
+ description: 'The new content to insert in place of the deleted lines'
409
+ }
410
+ },
411
+ required: ['file_path', 'start_line', 'end_line', 'new_content']
412
+ }
413
+ }
414
+ ];
415
+ exports.TOOLS = TOOLS;
416
+ // AST Parsing utilities
417
+ function parseFileToAST(filePath, content) {
418
+ const ext = path.extname(filePath);
419
+ try {
420
+ // Use Babel parser for all files (it handles TypeScript via plugin)
421
+ return babelParser.parse(content, {
422
+ sourceType: 'module',
423
+ plugins: [
424
+ 'typescript',
425
+ ext === '.tsx' || ext === '.jsx' ? 'jsx' : null,
426
+ 'decorators-legacy',
427
+ 'classProperties',
428
+ 'objectRestSpread',
429
+ 'asyncGenerators',
430
+ 'dynamicImport',
431
+ 'optionalChaining',
432
+ 'nullishCoalescingOperator'
433
+ ].filter(Boolean)
434
+ });
435
+ }
436
+ catch (error) {
437
+ throw new Error(`Failed to parse ${filePath}: ${error.message}`);
438
+ }
439
+ }
440
+ function analyzeFileAST(filePath) {
441
+ try {
442
+ const content = fsSync.readFileSync(filePath, 'utf-8');
443
+ const ast = parseFileToAST(filePath, content);
444
+ if (!ast || !ast.program) {
445
+ throw new Error('Failed to parse AST: no program node found');
446
+ }
447
+ const analysis = {
448
+ functions: [],
449
+ classes: [],
450
+ exports: [],
451
+ imports: [],
452
+ types: [],
453
+ constants: []
454
+ };
455
+ traverse(ast, {
456
+ // Function declarations
457
+ FunctionDeclaration(path) {
458
+ const node = path.node;
459
+ analysis.functions.push({
460
+ name: node.id?.name,
461
+ type: 'function',
462
+ async: node.async,
463
+ generator: node.generator,
464
+ params: node.params.map(p => extractParamInfo(p)),
465
+ returnType: node.returnType ? getTypeAnnotation(node.returnType) : null,
466
+ line: node.loc?.start.line,
467
+ exported: path.parent.type === 'ExportNamedDeclaration' ||
468
+ path.parent.type === 'ExportDefaultDeclaration'
469
+ });
470
+ },
471
+ // Arrow functions and function expressions
472
+ VariableDeclarator(path) {
473
+ const node = path.node;
474
+ if (node.init && (node.init.type === 'ArrowFunctionExpression' ||
475
+ node.init.type === 'FunctionExpression')) {
476
+ analysis.functions.push({
477
+ name: node.id.name,
478
+ type: node.init.type === 'ArrowFunctionExpression' ? 'arrow' : 'function',
479
+ async: node.init.async,
480
+ params: node.init.params.map(p => extractParamInfo(p)),
481
+ returnType: node.init.returnType ? getTypeAnnotation(node.init.returnType) : null,
482
+ line: node.loc?.start.line,
483
+ exported: path.parentPath.parent.type === 'ExportNamedDeclaration'
484
+ });
485
+ }
486
+ else if (node.init) {
487
+ // Constants
488
+ analysis.constants.push({
489
+ name: node.id.name,
490
+ type: node.id.typeAnnotation ? getTypeAnnotation(node.id.typeAnnotation) : null,
491
+ line: node.loc?.start.line
492
+ });
493
+ }
494
+ },
495
+ // Classes
496
+ ClassDeclaration(path) {
497
+ const node = path.node;
498
+ const methods = [];
499
+ if (node.body && node.body.body) {
500
+ node.body.body.forEach(member => {
501
+ if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
502
+ methods.push({
503
+ name: member.key.name,
504
+ kind: member.kind, // constructor, method, get, set
505
+ static: member.static,
506
+ async: member.async,
507
+ params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
508
+ returnType: member.returnType ? getTypeAnnotation(member.returnType) : null
509
+ });
510
+ }
511
+ });
512
+ }
513
+ analysis.classes.push({
514
+ name: node.id?.name,
515
+ methods,
516
+ superClass: node.superClass?.name,
517
+ line: node.loc?.start.line,
518
+ exported: path.parent.type === 'ExportNamedDeclaration' ||
519
+ path.parent.type === 'ExportDefaultDeclaration'
520
+ });
521
+ },
522
+ // Type definitions
523
+ TSTypeAliasDeclaration(path) {
524
+ analysis.types.push({
525
+ name: path.node.id.name,
526
+ kind: 'type',
527
+ line: path.node.loc?.start.line
528
+ });
529
+ },
530
+ TSInterfaceDeclaration(path) {
531
+ analysis.types.push({
532
+ name: path.node.id.name,
533
+ kind: 'interface',
534
+ line: path.node.loc?.start.line
535
+ });
536
+ },
537
+ // Exports
538
+ ExportNamedDeclaration(path) {
539
+ if (path.node.specifiers) {
540
+ path.node.specifiers.forEach(spec => {
541
+ analysis.exports.push({
542
+ name: spec.exported.name,
543
+ local: spec.local.name,
544
+ type: 'named'
545
+ });
546
+ });
547
+ }
548
+ },
549
+ ExportDefaultDeclaration(path) {
550
+ const declaration = path.node.declaration;
551
+ let name = 'default';
552
+ if (declaration.id) {
553
+ name = declaration.id.name;
554
+ }
555
+ else if (declaration.name) {
556
+ name = declaration.name;
557
+ }
558
+ analysis.exports.push({
559
+ name,
560
+ type: 'default'
561
+ });
562
+ }
563
+ });
564
+ return {
565
+ success: true,
566
+ analysis,
567
+ summary: {
568
+ functionCount: analysis.functions.length,
569
+ classCount: analysis.classes.length,
570
+ exportCount: analysis.exports.length,
571
+ typeCount: analysis.types.length
572
+ }
573
+ };
574
+ }
575
+ catch (error) {
576
+ return { success: false, error: error.message };
577
+ }
578
+ }
579
+ function extractParamInfo(param) {
580
+ if (param.type === 'Identifier') {
581
+ return {
582
+ name: param.name,
583
+ type: param.typeAnnotation ? getTypeAnnotation(param.typeAnnotation) : null,
584
+ optional: param.optional
585
+ };
586
+ }
587
+ else if (param.type === 'RestElement') {
588
+ return {
589
+ name: param.argument.name,
590
+ rest: true,
591
+ type: param.typeAnnotation ? getTypeAnnotation(param.typeAnnotation) : null
592
+ };
593
+ }
594
+ else if (param.type === 'AssignmentPattern') {
595
+ return {
596
+ name: param.left.name,
597
+ defaultValue: true,
598
+ type: param.left.typeAnnotation ? getTypeAnnotation(param.left.typeAnnotation) : null
599
+ };
600
+ }
601
+ return { name: 'unknown' };
602
+ }
603
+ function getTypeAnnotation(typeNode) {
604
+ if (!typeNode)
605
+ return null;
606
+ // Handle different type annotation formats
607
+ if (typeNode.typeAnnotation) {
608
+ typeNode = typeNode.typeAnnotation;
609
+ }
610
+ if (typeNode.type === 'TSStringKeyword')
611
+ return 'string';
612
+ if (typeNode.type === 'TSNumberKeyword')
613
+ return 'number';
614
+ if (typeNode.type === 'TSBooleanKeyword')
615
+ return 'boolean';
616
+ if (typeNode.type === 'TSAnyKeyword')
617
+ return 'any';
618
+ if (typeNode.type === 'TSVoidKeyword')
619
+ return 'void';
620
+ if (typeNode.type === 'TSUndefinedKeyword')
621
+ return 'undefined';
622
+ if (typeNode.type === 'TSNullKeyword')
623
+ return 'null';
624
+ if (typeNode.type === 'TSTypeReference' && typeNode.typeName) {
625
+ return typeNode.typeName.name || 'unknown';
626
+ }
627
+ if (typeNode.type === 'TSArrayType') {
628
+ const elementType = getTypeAnnotation(typeNode.elementType);
629
+ return `${elementType}[]`;
630
+ }
631
+ return 'unknown';
632
+ }
633
+ function getFunctionAST(filePath, functionName) {
634
+ try {
635
+ const content = fsSync.readFileSync(filePath, 'utf-8');
636
+ const ast = parseFileToAST(filePath, content);
637
+ const lines = content.split('\n');
638
+ let functionInfo = null;
639
+ traverse(ast, {
640
+ FunctionDeclaration(path) {
641
+ if (path.node.id?.name === functionName) {
642
+ functionInfo = extractFunctionDetails(path, lines);
643
+ path.stop();
644
+ }
645
+ },
646
+ VariableDeclarator(path) {
647
+ if (path.node.id.name === functionName &&
648
+ (path.node.init?.type === 'ArrowFunctionExpression' ||
649
+ path.node.init?.type === 'FunctionExpression')) {
650
+ functionInfo = extractFunctionDetails(path, lines, true);
651
+ path.stop();
652
+ }
653
+ },
654
+ ClassMethod(path) {
655
+ if (path.node.key.name === functionName) {
656
+ functionInfo = extractFunctionDetails(path, lines);
657
+ path.stop();
658
+ }
659
+ }
660
+ });
661
+ if (!functionInfo) {
662
+ return { success: false, error: `Function ${functionName} not found` };
663
+ }
664
+ return Object.assign({ success: true }, functionInfo);
665
+ }
666
+ catch (error) {
667
+ return { success: false, error: error.message };
668
+ }
669
+ }
670
+ function extractFunctionDetails(path, lines, isVariable = false) {
671
+ const node = isVariable ? path.node.init : path.node;
672
+ const startLine = node.loc.start.line - 1;
673
+ const endLine = node.loc.end.line - 1;
674
+ const code = lines.slice(startLine, endLine + 1).join('\n');
675
+ // Extract basic info without deep traversal (to avoid scope issues)
676
+ return {
677
+ name: isVariable ? path.node.id.name : node.id?.name,
678
+ code,
679
+ startLine: startLine + 1,
680
+ endLine: endLine + 1,
681
+ params: node.params.map((p) => extractParamInfo(p)),
682
+ returnType: node.returnType ? getTypeAnnotation(node.returnType) : null,
683
+ async: node.async,
684
+ complexity: 1 // Simplified complexity
685
+ };
686
+ }
687
+ // Removed estimateComplexity to avoid scope/parentPath traversal issues
688
+ // Complexity is now hardcoded to 1 in extractFunctionDetails
689
+ function getImportsAST(filePath) {
690
+ try {
691
+ const content = fsSync.readFileSync(filePath, 'utf-8');
692
+ const ast = parseFileToAST(filePath, content);
693
+ const imports = [];
694
+ traverse(ast, {
695
+ ImportDeclaration(path) {
696
+ const node = path.node;
697
+ const imported = [];
698
+ node.specifiers.forEach(spec => {
699
+ if (spec.type === 'ImportDefaultSpecifier') {
700
+ imported.push({ name: spec.local.name, type: 'default' });
701
+ }
702
+ else if (spec.type === 'ImportSpecifier') {
703
+ imported.push({
704
+ name: spec.local.name,
705
+ imported: spec.imported.name,
706
+ type: 'named'
707
+ });
708
+ }
709
+ else if (spec.type === 'ImportNamespaceSpecifier') {
710
+ imported.push({ name: spec.local.name, type: 'namespace' });
711
+ }
712
+ });
713
+ imports.push({
714
+ source: node.source.value,
715
+ imported,
716
+ line: node.loc.start.line
717
+ });
718
+ },
719
+ // Handle require statements
720
+ CallExpression(path) {
721
+ if (path.node.callee.name === 'require' &&
722
+ path.node.arguments[0]?.type === 'StringLiteral') {
723
+ const parent = path.parent;
724
+ let variableName = null;
725
+ if (parent.type === 'VariableDeclarator') {
726
+ variableName = parent.id.name;
727
+ }
728
+ imports.push({
729
+ source: path.node.arguments[0].value,
730
+ imported: variableName ? [{ name: variableName, type: 'require' }] : [],
731
+ type: 'require',
732
+ line: path.node.loc.start.line
733
+ });
734
+ }
735
+ }
736
+ });
737
+ return { success: true, imports };
738
+ }
739
+ catch (error) {
740
+ return { success: false, error: error.message };
741
+ }
742
+ }
743
+ function getTypeDefinitions(filePath) {
744
+ try {
745
+ const content = fsSync.readFileSync(filePath, 'utf-8');
746
+ const ast = parseFileToAST(filePath, content);
747
+ const lines = content.split('\n');
748
+ const types = [];
749
+ traverse(ast, {
750
+ TSTypeAliasDeclaration(path) {
751
+ const node = path.node;
752
+ const startLine = node.loc.start.line - 1;
753
+ const endLine = node.loc.end.line - 1;
754
+ types.push({
755
+ name: node.id.name,
756
+ kind: 'type',
757
+ code: lines.slice(startLine, endLine + 1).join('\n'),
758
+ line: startLine + 1,
759
+ exported: path.parent.type === 'ExportNamedDeclaration'
760
+ });
761
+ },
762
+ TSInterfaceDeclaration(path) {
763
+ const node = path.node;
764
+ const startLine = node.loc.start.line - 1;
765
+ const endLine = node.loc.end.line - 1;
766
+ const properties = [];
767
+ if (node.body && node.body.body) {
768
+ node.body.body.forEach(member => {
769
+ if (member.type === 'TSPropertySignature') {
770
+ properties.push({
771
+ name: member.key.name,
772
+ type: member.typeAnnotation ?
773
+ getTypeAnnotation(member.typeAnnotation) : null,
774
+ optional: member.optional
775
+ });
776
+ }
777
+ });
778
+ }
779
+ types.push({
780
+ name: node.id.name,
781
+ kind: 'interface',
782
+ code: lines.slice(startLine, endLine + 1).join('\n'),
783
+ properties,
784
+ line: startLine + 1,
785
+ exported: path.parent.type === 'ExportNamedDeclaration'
786
+ });
787
+ },
788
+ TSEnumDeclaration(path) {
789
+ const node = path.node;
790
+ const startLine = node.loc.start.line - 1;
791
+ const endLine = node.loc.end.line - 1;
792
+ types.push({
793
+ name: node.id.name,
794
+ kind: 'enum',
795
+ code: lines.slice(startLine, endLine + 1).join('\n'),
796
+ line: startLine + 1,
797
+ exported: path.parent.type === 'ExportNamedDeclaration'
798
+ });
799
+ }
800
+ });
801
+ return { success: true, types };
802
+ }
803
+ catch (error) {
804
+ return { success: false, error: error.message };
805
+ }
806
+ }
807
+ function getClassMethods(filePath, className) {
808
+ try {
809
+ const content = fsSync.readFileSync(filePath, 'utf-8');
810
+ const ast = parseFileToAST(filePath, content);
811
+ const lines = content.split('\n');
812
+ let classInfo = null;
813
+ traverse(ast, {
814
+ ClassDeclaration(path) {
815
+ if (path.node.id?.name === className) {
816
+ const methods = [];
817
+ if (path.node.body && path.node.body.body) {
818
+ path.node.body.body.forEach(member => {
819
+ if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
820
+ const startLine = member.loc.start.line - 1;
821
+ const endLine = member.loc.end.line - 1;
822
+ methods.push({
823
+ name: member.key.name,
824
+ kind: member.kind,
825
+ static: member.static,
826
+ async: member.async,
827
+ params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
828
+ returnType: member.returnType ? getTypeAnnotation(member.returnType) : null,
829
+ code: lines.slice(startLine, endLine + 1).join('\n'),
830
+ line: startLine + 1
831
+ });
832
+ }
833
+ });
834
+ }
835
+ classInfo = {
836
+ name: className,
837
+ methods,
838
+ superClass: path.node.superClass?.name
839
+ };
840
+ path.stop();
841
+ }
842
+ }
843
+ });
844
+ if (!classInfo) {
845
+ return { success: false, error: `Class ${className} not found` };
846
+ }
847
+ return { success: true, ...classInfo };
848
+ }
849
+ catch (error) {
850
+ return { success: false, error: error.message };
851
+ }
852
+ }
853
+ // Other tool implementations
854
+ async function readFile(filePath) {
855
+ try {
856
+ const content = await fs.readFile(filePath, 'utf-8');
857
+ return { success: true, content };
858
+ }
859
+ catch (error) {
860
+ return { success: false, error: error.message };
861
+ }
862
+ }
863
+ function resolveImportPath(fromFile, importPath) {
864
+ try {
865
+ if (importPath.startsWith('.')) {
866
+ const dir = path.dirname(fromFile);
867
+ const resolved = path.resolve(dir, importPath);
868
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
869
+ for (const ext of extensions) {
870
+ const withExt = resolved + ext;
871
+ if (fsSync.existsSync(withExt)) {
872
+ return { success: true, resolvedPath: withExt };
873
+ }
874
+ }
875
+ if (fsSync.existsSync(resolved)) {
876
+ return { success: true, resolvedPath: resolved };
877
+ }
878
+ }
879
+ return {
880
+ success: true,
881
+ resolvedPath: importPath,
882
+ isExternal: true
883
+ };
884
+ }
885
+ catch (error) {
886
+ return { success: false, error: error.message };
887
+ }
888
+ }
889
+ async function writeTestFile(filePath, content, sourceFilePath) {
890
+ try {
891
+ // VALIDATE: Check for incomplete/placeholder tests BEFORE writing
892
+ const invalidPatterns = [
893
+ /\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
894
+ /\/\/\s*(Add more|write more|Similarly|write tests for)/i,
895
+ /\/\/\s*TODO/i,
896
+ /\/\/\s*\.\.\./,
897
+ /\/\/.*etc\./i,
898
+ /expect\(\).*\/\//, // expect() followed by comment
899
+ ];
900
+ const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
901
+ if (hasPlaceholders) {
902
+ // Extract the actual placeholder comment for the error message
903
+ const foundPlaceholder = content.match(invalidPatterns.find(p => p.test(content)) || /\/\/.*/);
904
+ return {
905
+ success: false,
906
+ error: `REJECTED: Test file contains placeholder comment: "${foundPlaceholder?.[0]}"\n\nYou must write COMPLETE tests with actual code for ALL functions, not comments like "// Add more tests", "// Similarly", "// Further tests". Write the FULL implementation for EVERY function!`
907
+ };
908
+ }
909
+ // VALIDATE: Check if file has actual expect() statements
910
+ const expectCount = (content.match(/expect\(/g) || []).length;
911
+ const testCount = (content.match(/test\(|it\(/g) || []).length;
912
+ const describeCount = (content.match(/describe\(/g) || []).length;
913
+ if (testCount > 0 && expectCount === 0) {
914
+ return {
915
+ success: false,
916
+ error: 'REJECTED: Test file has test cases but NO expect() assertions! Every test MUST have at least one expect() statement. Write actual assertions!'
917
+ };
918
+ }
919
+ // VALIDATE: Compare against source file if provided
920
+ let expectedFunctionCount = 3; // Default minimum
921
+ if (sourceFilePath && fsSync.existsSync(sourceFilePath)) {
922
+ const analysis = analyzeFileAST(sourceFilePath);
923
+ if (analysis.success) {
924
+ const exportedFunctions = analysis.analysis.functions.filter((f) => f.exported);
925
+ expectedFunctionCount = exportedFunctions.length;
926
+ if (describeCount < expectedFunctionCount) {
927
+ return {
928
+ success: false,
929
+ error: `REJECTED: Source file has ${expectedFunctionCount} exported functions but test file only has ${describeCount} describe blocks!\n\nMissing tests for: ${exportedFunctions.slice(describeCount).map((f) => f.name).join(', ')}\n\nWrite a describe block with tests for EVERY function!`
930
+ };
931
+ }
932
+ }
933
+ }
934
+ else if (describeCount < 3) {
935
+ return {
936
+ success: false,
937
+ error: `REJECTED: Test file has only ${describeCount} describe blocks! The source file has multiple exported functions. You must write a describe block for EACH function, not just some of them. Write tests for ALL functions!`
938
+ };
939
+ }
940
+ if (testCount < Math.max(4, expectedFunctionCount * 2)) {
941
+ return {
942
+ success: false,
943
+ error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
944
+ };
945
+ }
946
+ const dir = path.dirname(filePath);
947
+ await fs.mkdir(dir, { recursive: true });
948
+ await fs.writeFile(filePath, content, 'utf-8');
949
+ return {
950
+ success: true,
951
+ path: filePath,
952
+ stats: {
953
+ tests: testCount,
954
+ expectations: expectCount
955
+ }
956
+ };
957
+ }
958
+ catch (error) {
959
+ return { success: false, error: error.message };
960
+ }
961
+ }
962
+ async function editTestFile(filePath, oldContent, newContent) {
963
+ try {
964
+ // Ensure directory exists
965
+ const dir = path.dirname(filePath);
966
+ await fs.mkdir(dir, { recursive: true });
967
+ let content = await fs.readFile(filePath, 'utf-8');
968
+ // Normalize whitespace for comparison (more forgiving)
969
+ const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();
970
+ const normalizedContent = normalizeWhitespace(content);
971
+ const normalizedOld = normalizeWhitespace(oldContent);
972
+ // First try: Exact match
973
+ if (content.includes(oldContent)) {
974
+ content = content.replace(oldContent, newContent);
975
+ await fs.writeFile(filePath, content, 'utf-8');
976
+ return { success: true, message: 'File edited successfully (exact match)' };
977
+ }
978
+ // Second try: Normalized whitespace match
979
+ if (normalizedContent.includes(normalizedOld)) {
980
+ // Find the original position and replace
981
+ const lines = content.split('\n');
982
+ const oldLines = oldContent.split('\n');
983
+ // Try to find the section
984
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
985
+ const section = lines.slice(i, i + oldLines.length).join('\n');
986
+ if (normalizeWhitespace(section) === normalizedOld) {
987
+ lines.splice(i, oldLines.length, newContent);
988
+ content = lines.join('\n');
989
+ await fs.writeFile(filePath, content, 'utf-8');
990
+ return { success: true, message: 'File edited successfully (normalized match)' };
991
+ }
992
+ }
993
+ }
994
+ // Third try: If oldContent is empty or very short, just overwrite the file
995
+ if (!oldContent || oldContent.trim().length < 10) {
996
+ await fs.writeFile(filePath, newContent, 'utf-8');
997
+ return { success: true, message: 'File overwritten (old content was empty/short)' };
998
+ }
999
+ // If all fails, show helpful error with file preview
1000
+ const preview = content.substring(0, 500);
1001
+ return {
1002
+ success: false,
1003
+ error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use write_test_file to overwrite the entire file instead.`,
1004
+ currentContent: preview
1005
+ };
1006
+ }
1007
+ catch (error) {
1008
+ return { success: false, error: error.message };
1009
+ }
1010
+ }
1011
+ async function replaceFunctionTests(testFilePath, functionName, newTestContent) {
1012
+ try {
1013
+ // Ensure directory exists
1014
+ const dir = path.dirname(testFilePath);
1015
+ await fs.mkdir(dir, { recursive: true });
1016
+ if (!fsSync.existsSync(testFilePath)) {
1017
+ // File doesn't exist, create it
1018
+ return await writeTestFile(testFilePath, newTestContent);
1019
+ }
1020
+ const content = await fs.readFile(testFilePath, 'utf-8');
1021
+ // Try to find the describe block for this function
1022
+ // Patterns to match: describe('functionName', ...) or describe("functionName", ...)
1023
+ const describePattern = new RegExp(`describe\\s*\\(['"]\s*${functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*['"]\\s*,.*?\\n\\}\\);?`, 'gs');
1024
+ const match = content.match(describePattern);
1025
+ if (match && match.length > 0) {
1026
+ // Replace the existing describe block
1027
+ const updatedContent = content.replace(describePattern, newTestContent);
1028
+ await fs.writeFile(testFilePath, updatedContent, 'utf-8');
1029
+ return {
1030
+ success: true,
1031
+ message: `Replaced tests for function '${functionName}'`,
1032
+ replaced: true
1033
+ };
1034
+ }
1035
+ else {
1036
+ // Function tests don't exist, append them
1037
+ // Find the last closing bracket or append at the end
1038
+ const lines = content.split('\n');
1039
+ let insertIndex = lines.length;
1040
+ // Try to insert before the last line if it's just whitespace or closing bracket
1041
+ for (let i = lines.length - 1; i >= 0; i--) {
1042
+ const line = lines[i].trim();
1043
+ if (line && line !== '}' && line !== '});') {
1044
+ insertIndex = i + 1;
1045
+ break;
1046
+ }
1047
+ }
1048
+ lines.splice(insertIndex, 0, '', newTestContent);
1049
+ await fs.writeFile(testFilePath, lines.join('\n'), 'utf-8');
1050
+ return {
1051
+ success: true,
1052
+ message: `Added tests for function '${functionName}'`,
1053
+ replaced: false,
1054
+ appended: true
1055
+ };
1056
+ }
1057
+ }
1058
+ catch (error) {
1059
+ return { success: false, error: error.message };
1060
+ }
1061
+ }
1062
+ function runTests(testFilePath) {
1063
+ try {
1064
+ const output = (0, child_process_1.execSync)(`npx jest ${testFilePath} --no-coverage`, {
1065
+ encoding: 'utf-8',
1066
+ stdio: 'pipe'
1067
+ });
1068
+ return {
1069
+ success: true,
1070
+ output,
1071
+ passed: true
1072
+ };
1073
+ }
1074
+ catch (error) {
1075
+ return {
1076
+ success: false,
1077
+ output: error.stdout + error.stderr,
1078
+ passed: false,
1079
+ error: error.message
1080
+ };
1081
+ }
1082
+ }
1083
+ function listDirectory(directoryPath) {
1084
+ try {
1085
+ if (!fsSync.existsSync(directoryPath)) {
1086
+ return { success: false, error: `Directory not found: ${directoryPath}` };
1087
+ }
1088
+ const items = fsSync.readdirSync(directoryPath);
1089
+ const details = items.map(item => {
1090
+ const fullPath = path.join(directoryPath, item);
1091
+ const stats = fsSync.statSync(fullPath);
1092
+ return {
1093
+ name: item,
1094
+ path: fullPath,
1095
+ type: stats.isDirectory() ? 'directory' : 'file',
1096
+ isDirectory: stats.isDirectory()
1097
+ };
1098
+ });
1099
+ return {
1100
+ success: true,
1101
+ path: directoryPath,
1102
+ items: details,
1103
+ files: details.filter(d => !d.isDirectory).map(d => d.name),
1104
+ directories: details.filter(d => d.isDirectory).map(d => d.name)
1105
+ };
1106
+ }
1107
+ catch (error) {
1108
+ return { success: false, error: error.message };
1109
+ }
1110
+ }
1111
+ function findFile(filename) {
1112
+ try {
1113
+ const searchDirs = ['src', 'tests', '.'];
1114
+ const found = [];
1115
+ function searchRecursive(dir) {
1116
+ if (!fsSync.existsSync(dir))
1117
+ return;
1118
+ const items = fsSync.readdirSync(dir);
1119
+ for (const item of items) {
1120
+ const fullPath = path.join(dir, item);
1121
+ const stats = fsSync.statSync(fullPath);
1122
+ if (stats.isDirectory()) {
1123
+ if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(item)) {
1124
+ searchRecursive(fullPath);
1125
+ }
1126
+ }
1127
+ else {
1128
+ // Check both full path and filename for matches
1129
+ const normalizedFullPath = fullPath.replace(/\\/g, '/');
1130
+ const normalizedSearch = filename.replace(/\\/g, '/');
1131
+ if (normalizedFullPath === normalizedSearch || // Exact full path match
1132
+ normalizedFullPath.includes(normalizedSearch) || // Partial path match
1133
+ normalizedFullPath.endsWith(normalizedSearch) || // Ends with search term
1134
+ item === filename || // Exact filename match
1135
+ item.includes(filename) // Partial filename match
1136
+ ) {
1137
+ found.push(fullPath);
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+ for (const dir of searchDirs) {
1143
+ searchRecursive(dir);
1144
+ }
1145
+ if (found.length === 0) {
1146
+ return {
1147
+ success: false,
1148
+ error: `File "${filename}" not found in repository`
1149
+ };
1150
+ }
1151
+ return {
1152
+ success: true,
1153
+ filename,
1154
+ found: found,
1155
+ count: found.length,
1156
+ primaryMatch: found[0]
1157
+ };
1158
+ }
1159
+ catch (error) {
1160
+ return { success: false, error: error.message };
1161
+ }
1162
+ }
1163
+ function calculateRelativePath(fromFile, toFile) {
1164
+ try {
1165
+ const fromDir = path.dirname(fromFile);
1166
+ let relativePath = path.relative(fromDir, toFile);
1167
+ // Remove .ts, .tsx, .js, .jsx extensions for imports
1168
+ relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, '');
1169
+ // Ensure it starts with ./ or ../
1170
+ if (!relativePath.startsWith('.')) {
1171
+ relativePath = './' + relativePath;
1172
+ }
1173
+ // Convert backslashes to forward slashes (Windows compatibility)
1174
+ relativePath = relativePath.replace(/\\/g, '/');
1175
+ return {
1176
+ success: true,
1177
+ from: fromFile,
1178
+ to: toFile,
1179
+ relativePath,
1180
+ importStatement: `import { ... } from '${relativePath}';`
1181
+ };
1182
+ }
1183
+ catch (error) {
1184
+ return { success: false, error: error.message };
1185
+ }
1186
+ }
1187
+ function reportLegitimateFailure(testFilePath, failingTests, reason, sourceCodeIssue) {
1188
+ console.log('\nโš ๏ธ LEGITIMATE TEST FAILURE REPORTED');
1189
+ console.log(` Test file: ${testFilePath}`);
1190
+ console.log(` Failing tests: ${failingTests.join(', ')}`);
1191
+ console.log(` Reason: ${reason}`);
1192
+ console.log(` Source code issue: ${sourceCodeIssue}`);
1193
+ return {
1194
+ success: true,
1195
+ acknowledged: true,
1196
+ message: 'Legitimate failure reported. Tests have been written correctly but source code has bugs.',
1197
+ testFilePath,
1198
+ failingTests,
1199
+ reason,
1200
+ sourceCodeIssue,
1201
+ recommendation: 'Fix the source code to resolve these test failures.'
1202
+ };
1203
+ }
1204
+ async function deleteLines(filePath, startLine, endLine) {
1205
+ try {
1206
+ const content = await fs.readFile(filePath, 'utf-8');
1207
+ const lines = content.split('\n');
1208
+ // Validate line numbers (1-indexed)
1209
+ if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
1210
+ return {
1211
+ success: false,
1212
+ error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
1213
+ };
1214
+ }
1215
+ if (startLine > endLine) {
1216
+ return {
1217
+ success: false,
1218
+ error: `Start line (${startLine}) must be <= end line (${endLine})`
1219
+ };
1220
+ }
1221
+ // Delete lines (convert to 0-indexed)
1222
+ const deletedLines = lines.splice(startLine - 1, endLine - startLine + 1);
1223
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
1224
+ return {
1225
+ success: true,
1226
+ message: `Deleted lines ${startLine}-${endLine}`,
1227
+ deletedCount: deletedLines.length,
1228
+ deletedContent: deletedLines.join('\n')
1229
+ };
1230
+ }
1231
+ catch (error) {
1232
+ return { success: false, error: error.message };
1233
+ }
1234
+ }
1235
+ async function insertLines(filePath, lineNumber, content) {
1236
+ try {
1237
+ const fileContent = await fs.readFile(filePath, 'utf-8');
1238
+ const lines = fileContent.split('\n');
1239
+ // Validate line number (1-indexed)
1240
+ if (lineNumber < 1 || lineNumber > lines.length + 1) {
1241
+ return {
1242
+ success: false,
1243
+ error: `Invalid line number: ${lineNumber}. File has ${lines.length} lines (use 1-${lines.length + 1}).`
1244
+ };
1245
+ }
1246
+ // Split content into lines and insert (convert to 0-indexed)
1247
+ const newLines = content.split('\n');
1248
+ lines.splice(lineNumber - 1, 0, ...newLines);
1249
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
1250
+ return {
1251
+ success: true,
1252
+ message: `Inserted ${newLines.length} line(s) at line ${lineNumber}`,
1253
+ insertedCount: newLines.length
1254
+ };
1255
+ }
1256
+ catch (error) {
1257
+ return { success: false, error: error.message };
1258
+ }
1259
+ }
1260
+ async function replaceLines(filePath, startLine, endLine, newContent) {
1261
+ try {
1262
+ const content = await fs.readFile(filePath, 'utf-8');
1263
+ const lines = content.split('\n');
1264
+ // Validate line numbers (1-indexed)
1265
+ if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
1266
+ return {
1267
+ success: false,
1268
+ error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
1269
+ };
1270
+ }
1271
+ if (startLine > endLine) {
1272
+ return {
1273
+ success: false,
1274
+ error: `Start line (${startLine}) must be <= end line (${endLine})`
1275
+ };
1276
+ }
1277
+ // Replace lines (convert to 0-indexed)
1278
+ const oldLines = lines.splice(startLine - 1, endLine - startLine + 1, ...newContent.split('\n'));
1279
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
1280
+ return {
1281
+ success: true,
1282
+ message: `Replaced lines ${startLine}-${endLine}`,
1283
+ oldContent: oldLines.join('\n'),
1284
+ newLineCount: newContent.split('\n').length
1285
+ };
1286
+ }
1287
+ catch (error) {
1288
+ return { success: false, error: error.message };
1289
+ }
1290
+ }
1291
+ // User-friendly messages for each tool
1292
+ const TOOL_MESSAGES = {
1293
+ 'read_file': '๐Ÿ“– Reading source file',
1294
+ 'analyze_file_ast': '๐Ÿ” Analyzing codebase structure',
1295
+ 'get_function_ast': '๐Ÿ”Ž Examining function details',
1296
+ 'get_imports_ast': '๐Ÿ“ฆ Analyzing dependencies',
1297
+ 'get_type_definitions': '๐Ÿ“‹ Extracting type definitions',
1298
+ 'get_class_methods': '๐Ÿ—๏ธ Analyzing class structure',
1299
+ 'resolve_import_path': '๐Ÿ”— Resolving import paths',
1300
+ 'write_test_file': 'โœ๏ธ Writing test cases to file',
1301
+ 'edit_test_file': 'โœ๏ธ Updating test file',
1302
+ 'replace_function_tests': '๐Ÿ”„ Replacing test cases for specific functions',
1303
+ 'run_tests': '๐Ÿงช Running tests',
1304
+ 'list_directory': '๐Ÿ“‚ Exploring directory structure',
1305
+ 'find_file': '๐Ÿ” Locating file in repository',
1306
+ 'calculate_relative_path': '๐Ÿงญ Calculating import path',
1307
+ 'report_legitimate_failure': 'โš ๏ธ Reporting legitimate test failures',
1308
+ 'delete_lines': '๐Ÿ—‘๏ธ Deleting lines from file',
1309
+ 'insert_lines': 'โž• Inserting lines into file',
1310
+ 'replace_lines': '๐Ÿ”„ Replacing lines in file'
1311
+ };
1312
+ // Tool execution router
1313
+ async function executeTool(toolName, args) {
1314
+ // Show user-friendly message with dynamic context
1315
+ let friendlyMessage = TOOL_MESSAGES[toolName] || `๐Ÿ”ง ${toolName}`;
1316
+ // Add specific details for certain tools
1317
+ if (toolName === 'replace_function_tests' && args.function_name) {
1318
+ friendlyMessage = `๐Ÿ”„ Replacing test cases for function: ${args.function_name}`;
1319
+ }
1320
+ else if (toolName === 'delete_lines' && args.start_line && args.end_line) {
1321
+ friendlyMessage = `๐Ÿ—‘๏ธ Deleting lines ${args.start_line}-${args.end_line}`;
1322
+ }
1323
+ else if (toolName === 'insert_lines' && args.line_number) {
1324
+ friendlyMessage = `โž• Inserting lines at line ${args.line_number}`;
1325
+ }
1326
+ else if (toolName === 'replace_lines' && args.start_line && args.end_line) {
1327
+ friendlyMessage = `๐Ÿ”„ Replacing lines ${args.start_line}-${args.end_line}`;
1328
+ }
1329
+ console.log(`\n${friendlyMessage}...`);
1330
+ let result;
1331
+ try {
1332
+ switch (toolName) {
1333
+ case 'read_file':
1334
+ result = await readFile(args.file_path);
1335
+ break;
1336
+ case 'analyze_file_ast':
1337
+ // Try cache first if indexer is available
1338
+ if (globalIndexer) {
1339
+ // Check if file has been modified since caching
1340
+ if (globalIndexer.isFileStale(args.file_path)) {
1341
+ console.log(' ๐Ÿ”„ File modified, re-analyzing...');
1342
+ result = analyzeFileAST(args.file_path);
1343
+ // Update cache with new analysis
1344
+ if (result.success) {
1345
+ await globalIndexer.updateIndex([args.file_path], analyzeFileAST);
1346
+ }
1347
+ break;
1348
+ }
1349
+ const cached = globalIndexer.getFileAnalysis(args.file_path);
1350
+ if (cached) {
1351
+ console.log(' ๐Ÿ“ฆ Using cached analysis');
1352
+ result = {
1353
+ success: true,
1354
+ analysis: cached,
1355
+ summary: {
1356
+ functionCount: cached.functions.length,
1357
+ classCount: cached.classes.length,
1358
+ exportCount: cached.exports.length,
1359
+ typeCount: cached.types.length
1360
+ }
1361
+ };
1362
+ break;
1363
+ }
1364
+ }
1365
+ // Fall back to actual analysis
1366
+ result = analyzeFileAST(args.file_path);
1367
+ break;
1368
+ case 'get_function_ast':
1369
+ result = getFunctionAST(args.file_path, args.function_name);
1370
+ break;
1371
+ case 'get_imports_ast':
1372
+ result = getImportsAST(args.file_path);
1373
+ break;
1374
+ case 'get_type_definitions':
1375
+ result = getTypeDefinitions(args.file_path);
1376
+ break;
1377
+ case 'get_class_methods':
1378
+ result = getClassMethods(args.file_path, args.class_name);
1379
+ break;
1380
+ case 'resolve_import_path':
1381
+ result = resolveImportPath(args.from_file, args.import_path);
1382
+ break;
1383
+ case 'write_test_file':
1384
+ result = await writeTestFile(args.file_path, args.content, args.source_file);
1385
+ break;
1386
+ case 'edit_test_file':
1387
+ result = await editTestFile(args.file_path, args.old_content, args.new_content);
1388
+ break;
1389
+ case 'replace_function_tests':
1390
+ result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
1391
+ break;
1392
+ case 'run_tests':
1393
+ result = runTests(args.test_file_path);
1394
+ break;
1395
+ case 'list_directory':
1396
+ result = listDirectory(args.directory_path);
1397
+ break;
1398
+ case 'find_file':
1399
+ result = findFile(args.filename);
1400
+ break;
1401
+ case 'calculate_relative_path':
1402
+ result = calculateRelativePath(args.from_file, args.to_file);
1403
+ break;
1404
+ case 'report_legitimate_failure':
1405
+ result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
1406
+ break;
1407
+ case 'delete_lines':
1408
+ result = await deleteLines(args.file_path, args.start_line, args.end_line);
1409
+ break;
1410
+ case 'insert_lines':
1411
+ result = await insertLines(args.file_path, args.line_number, args.content);
1412
+ break;
1413
+ case 'replace_lines':
1414
+ result = await replaceLines(args.file_path, args.start_line, args.end_line, args.new_content);
1415
+ break;
1416
+ default:
1417
+ result = { success: false, error: `Unknown tool: ${toolName}` };
1418
+ }
1419
+ }
1420
+ catch (error) {
1421
+ result = { success: false, error: error.message, stack: error.stack };
1422
+ }
1423
+ // Show result with friendly message
1424
+ if (result.success) {
1425
+ console.log(` โœ… Done`);
1426
+ }
1427
+ else if (result.error) {
1428
+ console.log(` โŒ ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`);
1429
+ }
1430
+ return result;
1431
+ }
1432
+ // File system utilities
1433
+ async function listFilesRecursive(dir, fileList = []) {
1434
+ const files = await fs.readdir(dir);
1435
+ for (const file of files) {
1436
+ const filePath = path.join(dir, file);
1437
+ const stat = await fs.stat(filePath);
1438
+ if (stat.isDirectory()) {
1439
+ // Skip excluded directories and cache directories
1440
+ const shouldExclude = CONFIG.excludeDirs.includes(file) ||
1441
+ file.startsWith('.') || // Hidden directories
1442
+ file === 'tests' || // Test directories
1443
+ file === '__tests__' ||
1444
+ file === 'test';
1445
+ if (!shouldExclude) {
1446
+ await listFilesRecursive(filePath, fileList);
1447
+ }
1448
+ }
1449
+ else {
1450
+ const ext = path.extname(file);
1451
+ // Filter files: must have valid extension and NOT be a test file
1452
+ const isTestFile = file.endsWith('.test.ts') ||
1453
+ file.endsWith('.test.tsx') ||
1454
+ file.endsWith('.test.js') ||
1455
+ file.endsWith('.test.jsx') ||
1456
+ file.endsWith('.spec.ts') ||
1457
+ file.endsWith('.spec.tsx') ||
1458
+ file.endsWith('.spec.js') ||
1459
+ file.endsWith('.spec.jsx');
1460
+ if (CONFIG.extensions.includes(ext) && !isTestFile && !file.startsWith('.')) {
1461
+ fileList.push(filePath);
1462
+ }
1463
+ }
1464
+ }
1465
+ return fileList;
1466
+ }
1467
+ // Generate test file path preserving directory structure
1468
+ function getTestFilePath(sourceFile) {
1469
+ const testFileName = path.basename(sourceFile).replace(/\.(ts|js)x?$/, '.test.ts');
1470
+ // Normalize path separators
1471
+ const normalizedSource = sourceFile.replace(/\\/g, '/');
1472
+ // Check if source file is in src directory
1473
+ if (normalizedSource.includes('src/')) {
1474
+ // Extract the relative path after 'src/'
1475
+ const srcIndex = normalizedSource.indexOf('src/');
1476
+ const relativePath = normalizedSource.substring(srcIndex + 4); // Skip 'src/'
1477
+ const subDir = path.dirname(relativePath);
1478
+ // Preserve subdirectory structure in test directory
1479
+ if (subDir && subDir !== '.') {
1480
+ const testPath = path.join(CONFIG.testDir, subDir, testFileName);
1481
+ console.log(` ๐Ÿ“ Test file path: ${testPath}`);
1482
+ return testPath;
1483
+ }
1484
+ }
1485
+ // Fallback: just use testDir + filename
1486
+ const testPath = path.join(CONFIG.testDir, testFileName);
1487
+ console.log(` ๐Ÿ“ Test file path: ${testPath}`);
1488
+ return testPath;
1489
+ }
1490
+ // AI Provider implementations
1491
+ async function callOpenAI(messages, tools) {
1492
+ const apiKey = CONFIG.apiKeys.openai;
1493
+ if (!apiKey)
1494
+ throw new Error('OpenAI API key not configured in codeguard.json');
1495
+ const AI_PROVIDERS = getAIProviders();
1496
+ const response = await fetch(AI_PROVIDERS.openai.url, {
1497
+ method: 'POST',
1498
+ headers: AI_PROVIDERS.openai.headers(apiKey),
1499
+ body: JSON.stringify({
1500
+ model: AI_PROVIDERS.openai.model,
1501
+ messages,
1502
+ tools: tools.map(t => ({
1503
+ type: 'function',
1504
+ function: {
1505
+ name: t.name,
1506
+ description: t.description,
1507
+ parameters: t.input_schema
1508
+ }
1509
+ })),
1510
+ tool_choice: 'auto'
1511
+ })
1512
+ });
1513
+ const data = await response.json();
1514
+ // Check for API errors
1515
+ if (data.error) {
1516
+ throw new Error(`OpenAI API error: ${data.error.message || JSON.stringify(data.error)}`);
1517
+ }
1518
+ if (!data.choices || data.choices.length === 0) {
1519
+ throw new Error(`OpenAI API returned no choices. Response: ${JSON.stringify(data)}`);
1520
+ }
1521
+ const message = data.choices[0].message;
1522
+ return {
1523
+ content: message.content,
1524
+ toolCalls: message.tool_calls?.map((tc) => ({
1525
+ id: tc.id,
1526
+ name: tc.function.name,
1527
+ input: JSON.parse(tc.function.arguments)
1528
+ }))
1529
+ };
1530
+ }
1531
+ async function callGemini(messages, tools) {
1532
+ const apiKey = CONFIG.apiKeys.gemini;
1533
+ if (!apiKey)
1534
+ throw new Error('Gemini API key not configured in codeguard.json');
1535
+ // Convert messages to Gemini format with proper function call handling
1536
+ const contents = messages.map(m => {
1537
+ const role = m.role === 'assistant' || m.role === 'model' ? 'model' : 'user';
1538
+ // Handle function calls from model
1539
+ if (m.functionCall) {
1540
+ return {
1541
+ role: 'model',
1542
+ parts: [{
1543
+ functionCall: {
1544
+ name: m.functionCall.name,
1545
+ args: m.functionCall.args
1546
+ }
1547
+ }]
1548
+ };
1549
+ }
1550
+ // Handle function responses from user
1551
+ if (m.functionResponse) {
1552
+ return {
1553
+ role: 'user',
1554
+ parts: [{
1555
+ functionResponse: {
1556
+ name: m.functionResponse.name,
1557
+ response: m.functionResponse.response
1558
+ }
1559
+ }]
1560
+ };
1561
+ }
1562
+ // Regular text message
1563
+ return {
1564
+ role,
1565
+ parts: [{ text: m.content || '' }]
1566
+ };
1567
+ });
1568
+ const AI_PROVIDERS = getAIProviders();
1569
+ const url = typeof AI_PROVIDERS.gemini.url === 'function' ? AI_PROVIDERS.gemini.url(apiKey) : AI_PROVIDERS.gemini.url;
1570
+ const response = await fetch(url, {
1571
+ method: 'POST',
1572
+ headers: AI_PROVIDERS.gemini.headers(),
1573
+ body: JSON.stringify({
1574
+ contents,
1575
+ tools: [{
1576
+ functionDeclarations: tools.map(t => ({
1577
+ name: t.name,
1578
+ description: t.description,
1579
+ parameters: t.input_schema
1580
+ }))
1581
+ }]
1582
+ })
1583
+ });
1584
+ const data = await response.json();
1585
+ // Check for API errors
1586
+ if (data.error) {
1587
+ const errorMsg = data.error.message || JSON.stringify(data.error);
1588
+ // Check for quota/rate limit errors
1589
+ if (errorMsg.includes('quota') || errorMsg.includes('rate limit')) {
1590
+ throw new Error(`Gemini API quota exceeded. Please wait or use a different provider (Claude or OpenAI). Error: ${errorMsg}`);
1591
+ }
1592
+ throw new Error(`Gemini API error: ${errorMsg}`);
1593
+ }
1594
+ if (!data.candidates || data.candidates.length === 0) {
1595
+ throw new Error(`Gemini API returned no candidates. Response: ${JSON.stringify(data)}`);
1596
+ }
1597
+ const candidate = data.candidates[0];
1598
+ const parts = candidate.content.parts;
1599
+ // Check for function calls
1600
+ const functionCalls = parts.filter((p) => p.functionCall);
1601
+ if (functionCalls.length > 0) {
1602
+ return {
1603
+ toolCalls: functionCalls.map((fc) => ({
1604
+ id: `gemini-${fc.functionCall.name}-${Date.now()}`,
1605
+ name: fc.functionCall.name,
1606
+ input: fc.functionCall.args
1607
+ }))
1608
+ };
1609
+ }
1610
+ // Return text content
1611
+ const textPart = parts.find((p) => p.text);
1612
+ return { content: textPart?.text || '' };
1613
+ }
1614
+ async function callClaude(messages, tools) {
1615
+ const apiKey = CONFIG.apiKeys.claude;
1616
+ if (!apiKey)
1617
+ throw new Error('Claude API key not configured in codeguard.json');
1618
+ const AI_PROVIDERS = getAIProviders();
1619
+ const response = await fetch(AI_PROVIDERS.claude.url, {
1620
+ method: 'POST',
1621
+ headers: AI_PROVIDERS.claude.headers(apiKey),
1622
+ body: JSON.stringify({
1623
+ model: AI_PROVIDERS.claude.model,
1624
+ max_tokens: 4096,
1625
+ messages,
1626
+ tools
1627
+ })
1628
+ });
1629
+ const data = await response.json();
1630
+ // Check for API errors
1631
+ if (data.error) {
1632
+ throw new Error(`Claude API error: ${data.error.message || JSON.stringify(data.error)}`);
1633
+ }
1634
+ if (!data.content || !Array.isArray(data.content)) {
1635
+ throw new Error(`Claude API returned invalid content. Response: ${JSON.stringify(data)}`);
1636
+ }
1637
+ const textContent = data.content.find((c) => c.type === 'text');
1638
+ const toolUse = data.content.filter((c) => c.type === 'tool_use');
1639
+ return {
1640
+ content: textContent?.text,
1641
+ toolCalls: toolUse.map((tu) => ({
1642
+ id: tu.id,
1643
+ name: tu.name,
1644
+ input: tu.input
1645
+ })),
1646
+ stopReason: data.stop_reason
1647
+ };
1648
+ }
1649
+ async function callAI(messages, tools, provider = CONFIG.aiProvider) {
1650
+ switch (provider) {
1651
+ case 'openai':
1652
+ return await callOpenAI(messages, tools);
1653
+ case 'gemini':
1654
+ return await callGemini(messages, tools);
1655
+ case 'claude':
1656
+ return await callClaude(messages, tools);
1657
+ default:
1658
+ throw new Error(`Unknown AI provider: ${provider}`);
1659
+ }
1660
+ }
1661
+ // Main conversation loop
1662
+ async function generateTests(sourceFile) {
1663
+ console.log(`\n๐Ÿ“ Generating tests for: ${sourceFile}\n`);
1664
+ const testFilePath = getTestFilePath(sourceFile);
1665
+ const messages = [
1666
+ {
1667
+ role: 'user',
1668
+ content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests for a TypeScript file.
1669
+
1670
+ Source file: ${sourceFile}
1671
+ Test file path: ${testFilePath}
1672
+
1673
+ IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
1674
+
1675
+ Your task (you MUST complete ALL steps):
1676
+ 1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
1677
+ 2. Use get_imports_ast tool to understand all dependencies
1678
+ 3. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths for the test file
1679
+ 4. For complex functions, use get_function_ast tool to get detailed information including parameters, return types, and cyclomatic complexity
1680
+ 5. For classes, use get_class_methods tool to extract all methods
1681
+ 6. Use get_type_definitions tool to understand TypeScript types and interfaces
1682
+ 7. Generate comprehensive Jest unit tests with:
1683
+ - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
1684
+ - Mock database modules like '../database' or '../database/index'
1685
+ - Mock models, services, and any modules that access config/database
1686
+ - Use jest.mock() calls at the TOP of the file before any imports
1687
+ - Test suites for each function/class
1688
+ - Multiple test cases covering:
1689
+ * Happy path scenarios
1690
+ * Edge cases (null, undefined, empty arrays, etc.)
1691
+ * Error conditions
1692
+ * Async behavior (if applicable)
1693
+ - Proper TypeScript types
1694
+ - Clear, descriptive test names
1695
+ - Complete test implementations (NO placeholder comments!)
1696
+ 8. REQUIRED: Write the COMPLETE test file using write_test_file tool with REAL test code (NOT placeholders!)
1697
+ - CRITICAL: Include source_file parameter with path to source file (e.g., source_file: "${sourceFile}")
1698
+ - DO NOT use ANY placeholder comments like:
1699
+ * "// Mock setup", "// Assertions", "// Call function"
1700
+ * "// Further tests...", "// Additional tests..."
1701
+ * "// Similarly, write tests for..."
1702
+ * "// Add more tests...", "// TODO", "// ..."
1703
+ - Write ACTUAL working test code with real mocks, real assertions, real function calls
1704
+ - Every test MUST have:
1705
+ * Real setup code (mock functions, create test data)
1706
+ * Real execution (call the function being tested)
1707
+ * Real expect() assertions (at least one per test)
1708
+ - Write tests for EVERY exported function (minimum 2-3 tests per function)
1709
+ - If source has 4 functions, test file MUST have 4 describe blocks with actual tests
1710
+ - Example of COMPLETE test structure:
1711
+ * Setup: Create mocks and test data
1712
+ * Execute: Call the function being tested
1713
+ * Assert: Use expect() to verify results
1714
+ 9. REQUIRED: Run the tests using run_tests tool
1715
+ 10. REQUIRED: If tests fail with import errors:
1716
+ - Use find_file tool to locate the missing module
1717
+ - Use calculate_relative_path tool to get correct import path
1718
+ - PRIMARY METHOD (once test file exists): Use line-based editing:
1719
+ * read_file to get current test file with line numbers
1720
+ * insert_lines to add missing imports at correct position (e.g., line 3)
1721
+ * delete_lines to remove incorrect imports
1722
+ * replace_lines to fix import paths
1723
+ - FALLBACK: Only use edit_test_file or write_test_file if line-based editing isn't suitable
1724
+ 11. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
1725
+
1726
+ FIXABLE ERRORS (you should fix these):
1727
+ - Wrong import paths
1728
+ - Missing mocks
1729
+ - Incorrect mock implementations
1730
+ - Wrong assertions or test logic
1731
+ - TypeScript compilation errors
1732
+ - Missing test setup/teardown
1733
+ - Cannot read properties of undefined
1734
+
1735
+ LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
1736
+ - Function returns wrong type (e.g., undefined instead of object)
1737
+ - Missing null/undefined checks in source code
1738
+ - Logic errors in source code
1739
+ - Unhandled promise rejections in source code
1740
+ - Source code throws unexpected errors
1741
+
1742
+ 12. If errors are FIXABLE (AFTER test file is written):
1743
+ - โœ… PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
1744
+ * read_file to get current test file with line numbers
1745
+ * delete_lines to remove incorrect lines
1746
+ * insert_lines to add missing code (e.g., mocks, imports)
1747
+ * replace_lines to fix specific line ranges
1748
+ * This is FASTER and MORE RELIABLE than rewriting entire file!
1749
+ - โš ๏ธ FALLBACK: Only use edit_test_file or write_test_file if:
1750
+ * Line-based editing is too complex (needs major restructuring)
1751
+ * Multiple scattered changes across the file
1752
+ - Then retry running tests
1753
+ 13. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
1754
+ - Provide failing test names, reason, and source code issue description
1755
+ - The test file will be kept as-is with legitimate failing tests
1756
+ 14. REQUIRED: Repeat steps 9-13 until tests pass OR legitimate failures are reported
1757
+ 15. REQUIRED: Ensure all functions are tested in the test file.
1758
+
1759
+ CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
1760
+
1761
+ START NOW by calling the analyze_file_ast tool with the source file path.`
1762
+ }
1763
+ ];
1764
+ let iterations = 0;
1765
+ const maxIterations = 100;
1766
+ let testFileWritten = false;
1767
+ let allToolResults = [];
1768
+ let legitimateFailureReported = false;
1769
+ let lastTestError = '';
1770
+ let sameErrorCount = 0;
1771
+ while (iterations < maxIterations) {
1772
+ iterations++;
1773
+ if (iterations === 1) {
1774
+ console.log(`\n๐Ÿค– AI is analyzing your code...`);
1775
+ }
1776
+ else if (iterations % 5 === 0) {
1777
+ console.log(`\n๐Ÿค– AI is still working (step ${iterations})...`);
1778
+ }
1779
+ const response = await callAI(messages, TOOLS);
1780
+ if (response.content) {
1781
+ const content = response.content; // Store for TypeScript
1782
+ // Only show AI message if it's making excuses (for debugging), otherwise skip
1783
+ // Detect if AI is making excuses instead of using tools
1784
+ const excusePatterns = [
1785
+ /unable to proceed/i,
1786
+ /cannot directly/i,
1787
+ /constrained by/i,
1788
+ /simulated environment/i,
1789
+ /limited to providing/i,
1790
+ /beyond my capabilities/i,
1791
+ /can't execute/i
1792
+ ];
1793
+ const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
1794
+ if (isMakingExcuses) {
1795
+ console.log('\nโš ๏ธ AI is making excuses! Forcing it to use tools...');
1796
+ // Don't add the excuse to conversation, override with command
1797
+ messages.push({
1798
+ role: 'user',
1799
+ content: 'STOP making excuses! You CAN use the tools. The edit_test_file tool works. Use it NOW to fix the test file. Add proper mocks to prevent database initialization errors.'
1800
+ });
1801
+ continue;
1802
+ }
1803
+ messages.push({ role: 'assistant', content });
1804
+ }
1805
+ if (!response.toolCalls || response.toolCalls.length === 0) {
1806
+ // Don't stop unless tests actually passed or legitimate failure reported
1807
+ const lastTestRun = allToolResults[allToolResults.length - 1];
1808
+ const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
1809
+ if (legitimateFailureReported) {
1810
+ console.log('\nโœ… Test generation complete (with legitimate failures reported)');
1811
+ break;
1812
+ }
1813
+ if (testFileWritten && testsActuallyPassed) {
1814
+ console.log('\nโœ… Test generation complete!');
1815
+ break;
1816
+ }
1817
+ // If no tools called, prompt to continue with specific action
1818
+ console.log('\nโš ๏ธ No tool calls. Prompting AI to continue...');
1819
+ if (!testFileWritten) {
1820
+ messages.push({
1821
+ role: 'user',
1822
+ content: 'You have not written the test file yet. Use write_test_file tool NOW with complete test code (not placeholders).'
1823
+ });
1824
+ }
1825
+ else {
1826
+ messages.push({
1827
+ role: 'user',
1828
+ content: `STOP talking and USE TOOLS!
1829
+
1830
+ If tests are failing:
1831
+ - FIXABLE errors (imports, mocks, assertions):
1832
+ โœ… PRIMARY: Use line-based editing (read_file + insert_lines/delete_lines/replace_lines)
1833
+ โš ๏ธ FALLBACK: Use edit_test_file or write_test_file
1834
+ - LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
1835
+
1836
+ Start with read_file to see line numbers, then fix specific lines!`
1837
+ });
1838
+ }
1839
+ continue;
1840
+ }
1841
+ // Execute all tool calls
1842
+ const toolResults = [];
1843
+ for (const toolCall of response.toolCalls) {
1844
+ const result = await executeTool(toolCall.name, toolCall.input);
1845
+ const toolResult = {
1846
+ id: toolCall.id,
1847
+ name: toolCall.name,
1848
+ result
1849
+ };
1850
+ toolResults.push(toolResult);
1851
+ allToolResults.push(toolResult);
1852
+ // Track if legitimate failure was reported
1853
+ if (toolCall.name === 'report_legitimate_failure' && result.success) {
1854
+ legitimateFailureReported = true;
1855
+ console.log('\nโœ… Legitimate failure acknowledged. Stopping test fixes.');
1856
+ console.log(` Recommendation: ${result.recommendation}`);
1857
+ }
1858
+ // Track if test file was written
1859
+ if (toolCall.name === 'write_test_file') {
1860
+ if (result.success) {
1861
+ testFileWritten = true;
1862
+ console.log(`\n๐Ÿ“ Test file written: ${result.path}`);
1863
+ if (result.stats) {
1864
+ console.log(` Tests: ${result.stats.tests}, Expectations: ${result.stats.expectations}`);
1865
+ }
1866
+ }
1867
+ else {
1868
+ // Test file was REJECTED due to validation
1869
+ console.log(`\nโŒ Test file REJECTED: ${result.error}`);
1870
+ testFileWritten = false; // Make sure we track it wasn't written
1871
+ // Give very specific instructions based on rejection reason
1872
+ if (result.error.includes('placeholder')) {
1873
+ messages.push({
1874
+ role: 'user',
1875
+ content: `Your test file was REJECTED because it contains placeholder comments.
1876
+
1877
+ You MUST rewrite it with COMPLETE code:
1878
+ - Remove ALL comments like "// Further tests", "// Add test", "// Mock setup"
1879
+ - Write the ACTUAL test implementation for EVERY function
1880
+ - Each test needs: real setup, real function call, real expect() assertions
1881
+
1882
+ Try again with write_test_file and provide COMPLETE test implementations!`
1883
+ });
1884
+ }
1885
+ else if (result.error.includes('NO expect()')) {
1886
+ messages.push({
1887
+ role: 'user',
1888
+ content: `Your test file was REJECTED because tests have no assertions!
1889
+
1890
+ Every test MUST have expect() statements. Example:
1891
+ expect(functionName).toHaveBeenCalled();
1892
+ expect(result).toEqual(expectedValue);
1893
+
1894
+ Rewrite with write_test_file and add actual expect() assertions to ALL tests!`
1895
+ });
1896
+ }
1897
+ else if (result.error.includes('too few tests')) {
1898
+ messages.push({
1899
+ role: 'user',
1900
+ content: `Your test file was REJECTED because it has too few tests!
1901
+
1902
+ You analyzed ${toolResults.length > 0 ? 'multiple' : 'several'} functions in the source file. Write tests for ALL of them!
1903
+ - Minimum 2-3 test cases per function
1904
+ - Cover: happy path, edge cases, error cases
1905
+
1906
+ Rewrite with write_test_file and include tests for EVERY function!`
1907
+ });
1908
+ }
1909
+ }
1910
+ }
1911
+ // Detect if edit_test_file failed
1912
+ if (toolCall.name === 'edit_test_file' && !result.success) {
1913
+ console.log('\nโš ๏ธ edit_test_file failed. Redirecting to line-based tools...');
1914
+ messages.push({
1915
+ role: 'user',
1916
+ content: `โŒ edit_test_file failed due to content mismatch.
1917
+
1918
+ โœ… SWITCH TO LINE-BASED EDITING (Primary Method):
1919
+
1920
+ Step 1: Call read_file tool to see the test file with line numbers
1921
+ Step 2: Identify which lines need changes
1922
+ Step 3: Use the appropriate tool:
1923
+ - insert_lines: Add missing imports/mocks (e.g., line 5)
1924
+ - delete_lines: Remove incorrect code (e.g., lines 10-12)
1925
+ - replace_lines: Fix specific sections (e.g., lines 20-25)
1926
+
1927
+ Examples:
1928
+ insert_lines({ file_path: "${testFilePath}", line_number: 5, content: "jest.mock('../database');" })
1929
+ replace_lines({ file_path: "${testFilePath}", start_line: 10, end_line: 15, new_content: "const mockData = { id: 1 };" })
1930
+
1931
+ โš ๏ธ Only use write_test_file as LAST RESORT (full file rewrite).
1932
+
1933
+ Start with read_file NOW to see line numbers!`
1934
+ });
1935
+ }
1936
+ // Detect repeated errors (suggests legitimate failure)
1937
+ if (toolCall.name === 'run_tests' && !result.success) {
1938
+ const errorOutput = result.output || result.error || '';
1939
+ const currentError = errorOutput.substring(0, 300); // First 300 chars as signature
1940
+ if (currentError === lastTestError) {
1941
+ sameErrorCount++;
1942
+ console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
1943
+ if (sameErrorCount >= 3) {
1944
+ console.log('\n๐Ÿšจ Same error repeated 3+ times! Likely a legitimate source code issue.');
1945
+ messages.push({
1946
+ role: 'user',
1947
+ content: `The same test error has occurred ${sameErrorCount} times in a row!
1948
+
1949
+ This suggests the failure is LEGITIMATE (source code bug), not a test issue.
1950
+
1951
+ Analyze the error and determine:
1952
+ 1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
1953
+ 2. Or is this a LEGITIMATE source code bug?
1954
+
1955
+ If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
1956
+ If FIXABLE: Make one more attempt to fix it.`
1957
+ });
1958
+ }
1959
+ }
1960
+ else {
1961
+ lastTestError = currentError;
1962
+ sameErrorCount = 1;
1963
+ }
1964
+ }
1965
+ // Detect import path errors
1966
+ if (toolCall.name === 'run_tests' && !result.success) {
1967
+ const errorOutput = result.output || result.error || '';
1968
+ // Check for module not found errors
1969
+ const moduleNotFoundMatch = errorOutput.match(/Cannot find module ['"]([^'"]+)['"]/);
1970
+ const tsModuleErrorMatch = errorOutput.match(/TS2307.*Cannot find module ['"]([^'"]+)['"]/);
1971
+ if (moduleNotFoundMatch || tsModuleErrorMatch) {
1972
+ const missingModule = moduleNotFoundMatch?.[1] || tsModuleErrorMatch?.[1];
1973
+ console.log(`\n๐Ÿ” Import error detected: Cannot find module "${missingModule}"`);
1974
+ // Extract filename from the path
1975
+ const filename = missingModule?.split('/').pop();
1976
+ messages.push({
1977
+ role: 'user',
1978
+ content: `Import path error detected! Module not found: "${missingModule}"
1979
+
1980
+ โœ… FIX WITH LINE-BASED EDITING:
1981
+
1982
+ Step 1: find_file tool to search for "${filename}" in the repository
1983
+ Step 2: calculate_relative_path tool to get correct import path
1984
+ Step 3: Fix using line-based tools:
1985
+ a) read_file to see the test file with line numbers
1986
+ b) Find the incorrect import line (search for "${missingModule}")
1987
+ c) replace_lines to fix just that import line with correct path
1988
+
1989
+ Example workflow:
1990
+ 1. find_file({ filename: "${filename}.ts" })
1991
+ 2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
1992
+ 3. read_file({ file_path: "${testFilePath}" })
1993
+ 4. replace_lines({ file_path: "${testFilePath}", start_line: X, end_line: X, new_content: "import ... from 'correct-path';" })
1994
+
1995
+ Start NOW with find_file!`
1996
+ });
1997
+ }
1998
+ // Check for database initialization errors
1999
+ const isDatabaseError = /Cannot read properties of undefined.*reading|database|config|SSL|CA|HOST/i.test(errorOutput);
2000
+ if (isDatabaseError) {
2001
+ console.log('\n๐Ÿ” Database initialization error detected! Need to add mocks...');
2002
+ messages.push({
2003
+ role: 'user',
2004
+ content: `The test is failing because the source file imports modules that initialize database connections.
2005
+
2006
+ โœ… FIX WITH LINE-BASED EDITING:
2007
+
2008
+ Step 1: read_file to see current test file structure
2009
+ Step 2: insert_lines to add mocks at the TOP of the file (before any imports)
2010
+
2011
+ Required mocks to add:
2012
+ jest.mock('../database', () => ({ default: {} }));
2013
+ jest.mock('../database/index', () => ({ default: {} }));
2014
+ jest.mock('../models/serviceDesk.models');
2015
+
2016
+ Example:
2017
+ 1. read_file({ file_path: "${testFilePath}" })
2018
+ 2. Find where imports start (usually line 1-3)
2019
+ 3. insert_lines({
2020
+ file_path: "${testFilePath}",
2021
+ line_number: 1,
2022
+ content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n"
2023
+ })
2024
+
2025
+ โš ๏ธ Mocks MUST be at the TOP before any imports!
2026
+
2027
+ Start NOW with read_file to see current structure!`
2028
+ });
2029
+ }
2030
+ }
2031
+ }
2032
+ // Add tool results to conversation based on provider
2033
+ if (CONFIG.aiProvider === 'claude') {
2034
+ messages.push({
2035
+ role: 'assistant',
2036
+ content: response.toolCalls.map(tc => ({
2037
+ type: 'tool_use',
2038
+ id: tc.id,
2039
+ name: tc.name,
2040
+ input: tc.input
2041
+ }))
2042
+ });
2043
+ messages.push({
2044
+ role: 'user',
2045
+ content: toolResults.map(tr => ({
2046
+ type: 'tool_result',
2047
+ tool_use_id: tr.id,
2048
+ content: JSON.stringify(tr.result)
2049
+ }))
2050
+ });
2051
+ }
2052
+ else if (CONFIG.aiProvider === 'openai') {
2053
+ messages.push({
2054
+ role: 'assistant',
2055
+ tool_calls: response.toolCalls.map(tc => ({
2056
+ id: tc.id,
2057
+ type: 'function',
2058
+ function: {
2059
+ name: tc.name,
2060
+ arguments: JSON.stringify(tc.input)
2061
+ }
2062
+ }))
2063
+ });
2064
+ for (const tr of toolResults) {
2065
+ messages.push({
2066
+ role: 'tool',
2067
+ tool_call_id: tr.id,
2068
+ content: JSON.stringify(tr.result)
2069
+ });
2070
+ }
2071
+ }
2072
+ else {
2073
+ // Gemini - use proper function call format
2074
+ for (const toolCall of response.toolCalls) {
2075
+ // Add model's function call
2076
+ messages.push({
2077
+ role: 'model',
2078
+ functionCall: {
2079
+ name: toolCall.name,
2080
+ args: toolCall.input
2081
+ }
2082
+ });
2083
+ // Add user's function response
2084
+ const result = toolResults.find(tr => tr.name === toolCall.name);
2085
+ messages.push({
2086
+ role: 'user',
2087
+ functionResponse: {
2088
+ name: toolCall.name,
2089
+ response: result?.result
2090
+ }
2091
+ });
2092
+ }
2093
+ }
2094
+ // Check if legitimate failure was reported
2095
+ if (legitimateFailureReported) {
2096
+ console.log('\nโœ… Stopping iteration: Legitimate failure reported.');
2097
+ break;
2098
+ }
2099
+ // Check if tests were run and passed
2100
+ const testRun = toolResults.find(tr => tr.name === 'run_tests');
2101
+ if (testRun?.result.passed) {
2102
+ console.log('\n๐ŸŽ‰ All tests passed!');
2103
+ break;
2104
+ }
2105
+ }
2106
+ if (iterations >= maxIterations) {
2107
+ console.log('\nโš ๏ธ Reached maximum iterations. Tests may not be complete.');
2108
+ }
2109
+ if (!testFileWritten) {
2110
+ console.log('\nโŒ WARNING: Test file was never written! The AI may not have used the tools correctly.');
2111
+ console.log(' Try running again or check your API key and connectivity.');
2112
+ }
2113
+ else if (legitimateFailureReported) {
2114
+ console.log('\n๐Ÿ“‹ Test file created with legitimate failures documented.');
2115
+ console.log(' These failures indicate bugs in the source code that need to be fixed.');
2116
+ }
2117
+ return testFilePath;
2118
+ }
2119
+ // Interactive CLI
2120
+ async function promptUser(question) {
2121
+ const rl = readline.createInterface({
2122
+ input: process.stdin,
2123
+ output: process.stdout
2124
+ });
2125
+ return new Promise(resolve => {
2126
+ rl.question(question, answer => {
2127
+ rl.close();
2128
+ resolve(answer);
2129
+ });
2130
+ });
2131
+ }
2132
+ // Get all directories recursively
2133
+ async function listDirectories(dir, dirList = []) {
2134
+ const items = await fs.readdir(dir);
2135
+ for (const item of items) {
2136
+ const itemPath = path.join(dir, item);
2137
+ const stat = await fs.stat(itemPath);
2138
+ if (stat.isDirectory() && !CONFIG.excludeDirs.includes(item)) {
2139
+ dirList.push(itemPath);
2140
+ await listDirectories(itemPath, dirList);
2141
+ }
2142
+ }
2143
+ return dirList;
2144
+ }
2145
+ // Folder-wise test generation
2146
+ async function generateTestsForFolder() {
2147
+ console.log('\n๐Ÿ“‚ Folder-wise Test Generation\n');
2148
+ // Get all directories
2149
+ const directories = await listDirectories('.');
2150
+ if (directories.length === 0) {
2151
+ console.log('No directories found!');
2152
+ return;
2153
+ }
2154
+ console.log('Select a folder to generate tests for all files:\n');
2155
+ directories.forEach((dir, index) => {
2156
+ console.log(`${index + 1}. ${dir}`);
2157
+ });
2158
+ const choice = await promptUser('\nEnter folder number: ');
2159
+ const selectedDir = directories[parseInt(choice) - 1];
2160
+ if (!selectedDir) {
2161
+ console.log('Invalid selection!');
2162
+ return;
2163
+ }
2164
+ // Get all files in the selected directory (recursive)
2165
+ const files = await listFilesRecursive(selectedDir);
2166
+ if (files.length === 0) {
2167
+ console.log(`No source files found in ${selectedDir}!`);
2168
+ return;
2169
+ }
2170
+ console.log(`\n๐Ÿ“ Found ${files.length} files to process in ${selectedDir}\n`);
2171
+ // Process each file
2172
+ for (let i = 0; i < files.length; i++) {
2173
+ const file = files[i];
2174
+ const testFilePath = getTestFilePath(file);
2175
+ console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);
2176
+ // Check if test file already exists
2177
+ if (fsSync.existsSync(testFilePath)) {
2178
+ const answer = await promptUser(` Test file already exists: ${testFilePath}\n Regenerate? (y/n): `);
2179
+ if (answer.toLowerCase() !== 'y') {
2180
+ console.log(' Skipped.');
2181
+ continue;
2182
+ }
2183
+ }
2184
+ try {
2185
+ await generateTests(file);
2186
+ console.log(` โœ… Completed: ${testFilePath}`);
2187
+ }
2188
+ catch (error) {
2189
+ console.error(` โŒ Failed: ${error.message}`);
2190
+ }
2191
+ }
2192
+ console.log(`\nโœจ Folder processing complete! Processed ${files.length} files.`);
2193
+ }
2194
+ // Function-wise test generation
2195
+ async function generateTestsForFunctions(sourceFile, functionNames) {
2196
+ console.log(`\n๐Ÿ“ Generating tests for selected functions in: ${sourceFile}\n`);
2197
+ const testFilePath = getTestFilePath(sourceFile);
2198
+ const testFileExists = fsSync.existsSync(testFilePath);
2199
+ const messages = [
2200
+ {
2201
+ role: 'user',
2202
+ content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests for specific functions in a TypeScript file.
2203
+
2204
+ Source file: ${sourceFile}
2205
+ Test file path: ${testFilePath}
2206
+ Test file exists: ${testFileExists}
2207
+ Selected functions to test: ${functionNames.join(', ')}
2208
+
2209
+ IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
2210
+
2211
+ ${testFileExists ? `
2212
+ ๐Ÿšจ CRITICAL WARNING: Test file ALREADY EXISTS at ${testFilePath}! ๐Ÿšจ
2213
+
2214
+ You MUST use the replace_function_tests tool to update ONLY the selected function tests!
2215
+ DO NOT use write_test_file as it will OVERWRITE THE ENTIRE FILE and DELETE all other tests!
2216
+
2217
+ Other tests in this file for different functions MUST be preserved!
2218
+ ` : ''}
2219
+
2220
+ Your task (you MUST complete ALL steps):
2221
+ 1. FIRST: Use analyze_file_ast tool to get information about the selected functions: ${functionNames.join(', ')}
2222
+ 2. Use get_function_ast tool for each selected function to get detailed information
2223
+ 3. Use get_imports_ast tool to understand dependencies
2224
+ 4. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths
2225
+ 5. Generate comprehensive Jest unit tests ONLY for these functions: ${functionNames.join(', ')}
2226
+ - CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
2227
+ - Mock database modules, models, services, and config modules
2228
+ - Use jest.mock() calls at the TOP of the file before any imports
2229
+ - Test suites for each selected function
2230
+ - Multiple test cases covering:
2231
+ * Happy path scenarios
2232
+ * Edge cases (null, undefined, empty arrays, etc.)
2233
+ * Error conditions
2234
+ * Async behavior (if applicable)
2235
+ - Proper TypeScript types
2236
+ - Clear, descriptive test names
2237
+ - Complete test implementations (NO placeholder comments!)
2238
+ ${testFileExists ? `
2239
+ 6. ๐Ÿšจ CRITICAL: Test file EXISTS! Call replace_function_tests tool for EACH function: ${functionNames.join(', ')}
2240
+ - Call replace_function_tests ONCE for each function
2241
+ - Pass the complete describe block as new_test_content parameter
2242
+ - Example: replace_function_tests(test_file_path: "${testFilePath}", function_name: "${functionNames[0]}", new_test_content: "describe('${functionNames[0]}', () => { ... })")
2243
+ - This preserves ALL other existing tests in the file
2244
+ - DO NOT use write_test_file! It will DELETE all other tests!
2245
+ - DO NOT use edit_test_file! Use replace_function_tests instead!` : `
2246
+ 6. REQUIRED: Test file does NOT exist. Use write_test_file tool with tests for: ${functionNames.join(', ')}
2247
+ - Create a new test file with complete test implementation`}
2248
+ 7. REQUIRED: Run the tests using run_tests tool
2249
+ 8. REQUIRED: If tests fail, analyze if errors are FIXABLE or LEGITIMATE:
2250
+
2251
+ FIXABLE ERRORS (fix these):
2252
+ - Wrong import paths โ†’ use find_file + calculate_relative_path + edit tools
2253
+ - Missing mocks โ†’ add proper jest.mock() calls
2254
+ - Incorrect mock implementations โ†’ update mock return values
2255
+ - Wrong test assertions โ†’ fix expect() statements
2256
+ - TypeScript errors โ†’ fix types and imports
2257
+
2258
+ LEGITIMATE FAILURES (report these):
2259
+ - Function returns wrong type (source code bug)
2260
+ - Missing null checks in source code
2261
+ - Logic errors in source code
2262
+ - Source code throws unexpected errors
2263
+
2264
+ 9. If FIXABLE (AFTER test file is written/updated):
2265
+ ${testFileExists ? `- โœ… PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
2266
+ * read_file to see current test file with line numbers
2267
+ * delete_lines to remove incorrect lines
2268
+ * insert_lines to add missing mocks, imports, or test cases
2269
+ * replace_lines to fix specific sections
2270
+ * This preserves ALL other tests and is more reliable!
2271
+ - โš ๏ธ SECONDARY: replace_function_tests for specific function updates
2272
+ - โŒ AVOID: write_test_file (will DELETE all other tests!)` : `- Use write_test_file to create the test file
2273
+ - Once written, use line-based tools for fixes (read_file + insert/delete/replace_lines)`}
2274
+ - Then retry tests
2275
+ 10. If LEGITIMATE: Call report_legitimate_failure with details and STOP
2276
+ 11. REQUIRED: Repeat steps 7-10 until tests pass OR legitimate failures reported
2277
+
2278
+ ${testFileExists ? `
2279
+ ๐Ÿšจ REMINDER: The test file EXISTS! Use replace_function_tests, NOT write_test_file! ๐Ÿšจ
2280
+ ` : ''}
2281
+
2282
+ CRITICAL: Fix test bugs but REPORT source code bugs (don't try to make broken code pass)!
2283
+
2284
+ START NOW by calling the analyze_file_ast tool with the source file path.`
2285
+ }
2286
+ ];
2287
+ let iterations = 0;
2288
+ const maxIterations = 100;
2289
+ let testFileWritten = false;
2290
+ let allToolResults = [];
2291
+ let legitimateFailureReported = false;
2292
+ let lastTestError = '';
2293
+ let sameErrorCount = 0;
2294
+ while (iterations < maxIterations) {
2295
+ iterations++;
2296
+ if (iterations === 1) {
2297
+ console.log(`\n๐Ÿค– AI is analyzing selected functions...`);
2298
+ }
2299
+ else if (iterations % 5 === 0) {
2300
+ console.log(`\n๐Ÿค– AI is still working (step ${iterations})...`);
2301
+ }
2302
+ const response = await callAI(messages, TOOLS);
2303
+ if (response.content) {
2304
+ const content = response.content;
2305
+ // Only show AI message if it's making excuses (for debugging), otherwise skip
2306
+ // Detect if AI is making excuses
2307
+ const excusePatterns = [
2308
+ /unable to proceed/i,
2309
+ /cannot directly/i,
2310
+ /constrained by/i,
2311
+ /simulated environment/i,
2312
+ /limited to providing/i,
2313
+ /beyond my capabilities/i,
2314
+ /can't execute/i
2315
+ ];
2316
+ const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
2317
+ if (isMakingExcuses) {
2318
+ console.log('\nโš ๏ธ AI is making excuses! Forcing it to use tools...');
2319
+ messages.push({
2320
+ role: 'user',
2321
+ content: 'STOP making excuses! You CAN use the tools. Use replace_function_tests or write_test_file NOW to fix the test file.'
2322
+ });
2323
+ continue;
2324
+ }
2325
+ messages.push({ role: 'assistant', content });
2326
+ }
2327
+ if (!response.toolCalls || response.toolCalls.length === 0) {
2328
+ const lastTestRun = allToolResults[allToolResults.length - 1];
2329
+ const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
2330
+ if (legitimateFailureReported) {
2331
+ console.log('\nโœ… Test generation complete (with legitimate failures reported)');
2332
+ break;
2333
+ }
2334
+ if (testFileWritten && testsActuallyPassed) {
2335
+ console.log('\nโœ… Test generation complete!');
2336
+ break;
2337
+ }
2338
+ console.log('\nโš ๏ธ No tool calls. Prompting AI to continue...');
2339
+ if (!testFileWritten) {
2340
+ messages.push({
2341
+ role: 'user',
2342
+ content: testFileExists
2343
+ ? `๐Ÿšจ STOP TALKING! The test file EXISTS at ${testFilePath}!
2344
+
2345
+ Call replace_function_tests tool NOW for EACH function: ${functionNames.join(', ')}
2346
+
2347
+ Example:
2348
+ replace_function_tests({
2349
+ test_file_path: "${testFilePath}",
2350
+ function_name: "${functionNames[0]}",
2351
+ new_test_content: "describe('${functionNames[0]}', () => { test('should...', () => { ... }) })"
2352
+ })
2353
+
2354
+ DO NOT use write_test_file! It will DELETE all other tests!`
2355
+ : `Use write_test_file tool NOW with complete test code for: ${functionNames.join(', ')}`
2356
+ });
2357
+ }
2358
+ else {
2359
+ messages.push({
2360
+ role: 'user',
2361
+ content: testFileExists
2362
+ ? `STOP talking and USE TOOLS!
2363
+
2364
+ โœ… PRIMARY METHOD: Fix using line-based editing:
2365
+ 1. read_file to see test file with line numbers
2366
+ 2. insert_lines/delete_lines/replace_lines to fix specific issues
2367
+
2368
+ โš ๏ธ SECONDARY: Use replace_function_tests for function-level updates
2369
+ โŒ NEVER: Use write_test_file (will delete all other tests!)
2370
+
2371
+ Start NOW with read_file!`
2372
+ : `STOP talking and USE TOOLS!
2373
+
2374
+ - If test file doesn't exist: write_test_file
2375
+ - If test file exists: read_file + line-based editing tools
2376
+
2377
+ Act NOW!`
2378
+ });
2379
+ }
2380
+ continue;
2381
+ }
2382
+ // Execute all tool calls
2383
+ const toolResults = [];
2384
+ for (const toolCall of response.toolCalls) {
2385
+ const result = await executeTool(toolCall.name, toolCall.input);
2386
+ const toolResult = {
2387
+ id: toolCall.id,
2388
+ name: toolCall.name,
2389
+ result
2390
+ };
2391
+ toolResults.push(toolResult);
2392
+ allToolResults.push(toolResult);
2393
+ // Track if legitimate failure was reported
2394
+ if (toolCall.name === 'report_legitimate_failure' && result.success) {
2395
+ legitimateFailureReported = true;
2396
+ console.log('\nโœ… Legitimate failure acknowledged. Stopping test fixes.');
2397
+ console.log(` Recommendation: ${result.recommendation}`);
2398
+ }
2399
+ // Detect repeated errors (suggests legitimate failure)
2400
+ if (toolCall.name === 'run_tests' && !result.success) {
2401
+ const errorOutput = result.output || result.error || '';
2402
+ const currentError = errorOutput.substring(0, 300);
2403
+ if (currentError === lastTestError) {
2404
+ sameErrorCount++;
2405
+ console.log(`\nโš ๏ธ Same error repeated ${sameErrorCount} times`);
2406
+ if (sameErrorCount >= 3) {
2407
+ console.log('\n๐Ÿšจ Same error repeated 3+ times! Likely a legitimate source code issue.');
2408
+ messages.push({
2409
+ role: 'user',
2410
+ content: `The same test error has occurred ${sameErrorCount} times in a row!
2411
+
2412
+ This suggests the failure is LEGITIMATE (source code bug), not a test issue.
2413
+
2414
+ If this is a source code bug: Call report_legitimate_failure tool NOW.
2415
+ If this is still fixable: Make ONE final attempt to fix it.`
2416
+ });
2417
+ }
2418
+ }
2419
+ else {
2420
+ lastTestError = currentError;
2421
+ sameErrorCount = 1;
2422
+ }
2423
+ }
2424
+ // Track if test file was written
2425
+ if (toolCall.name === 'write_test_file' || toolCall.name === 'replace_function_tests') {
2426
+ if (result.success) {
2427
+ testFileWritten = true;
2428
+ console.log(`\n๐Ÿ“ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
2429
+ }
2430
+ }
2431
+ // Detect if AI incorrectly used write_test_file when file exists
2432
+ if (toolCall.name === 'write_test_file' && testFileExists) {
2433
+ console.log('\nโš ๏ธ WARNING: AI used write_test_file on existing file! This overwrites all other tests!');
2434
+ messages.push({
2435
+ role: 'user',
2436
+ content: `โŒ CRITICAL ERROR: You used write_test_file but the test file ALREADY EXISTS!
2437
+
2438
+ This OVERWROTE the entire file and DELETED all other tests! This is WRONG!
2439
+
2440
+ You MUST use replace_function_tests instead. For future fixes, call it for EACH function:
2441
+
2442
+ ${functionNames.map(fname => `replace_function_tests({
2443
+ test_file_path: "${testFilePath}",
2444
+ function_name: "${fname}",
2445
+ new_test_content: "describe('${fname}', () => { /* your tests */ })"
2446
+ })`).join('\n\n')}
2447
+
2448
+ DO NOT use write_test_file when the file exists! Always use replace_function_tests!`
2449
+ });
2450
+ }
2451
+ }
2452
+ // Add tool results to conversation based on provider
2453
+ if (CONFIG.aiProvider === 'claude') {
2454
+ messages.push({
2455
+ role: 'assistant',
2456
+ content: response.toolCalls.map(tc => ({
2457
+ type: 'tool_use',
2458
+ id: tc.id,
2459
+ name: tc.name,
2460
+ input: tc.input
2461
+ }))
2462
+ });
2463
+ messages.push({
2464
+ role: 'user',
2465
+ content: toolResults.map(tr => ({
2466
+ type: 'tool_result',
2467
+ tool_use_id: tr.id,
2468
+ content: JSON.stringify(tr.result)
2469
+ }))
2470
+ });
2471
+ }
2472
+ else if (CONFIG.aiProvider === 'openai') {
2473
+ messages.push({
2474
+ role: 'assistant',
2475
+ tool_calls: response.toolCalls.map(tc => ({
2476
+ id: tc.id,
2477
+ type: 'function',
2478
+ function: {
2479
+ name: tc.name,
2480
+ arguments: JSON.stringify(tc.input)
2481
+ }
2482
+ }))
2483
+ });
2484
+ for (const tr of toolResults) {
2485
+ messages.push({
2486
+ role: 'tool',
2487
+ tool_call_id: tr.id,
2488
+ content: JSON.stringify(tr.result)
2489
+ });
2490
+ }
2491
+ }
2492
+ else {
2493
+ for (const toolCall of response.toolCalls) {
2494
+ messages.push({
2495
+ role: 'model',
2496
+ functionCall: {
2497
+ name: toolCall.name,
2498
+ args: toolCall.input
2499
+ }
2500
+ });
2501
+ const result = toolResults.find(tr => tr.name === toolCall.name);
2502
+ messages.push({
2503
+ role: 'user',
2504
+ functionResponse: {
2505
+ name: toolCall.name,
2506
+ response: result?.result
2507
+ }
2508
+ });
2509
+ }
2510
+ }
2511
+ // Check if legitimate failure was reported
2512
+ if (legitimateFailureReported) {
2513
+ console.log('\nโœ… Stopping iteration: Legitimate failure reported.');
2514
+ break;
2515
+ }
2516
+ // Check if tests passed
2517
+ const testRun = toolResults.find(tr => tr.name === 'run_tests');
2518
+ if (testRun?.result.passed) {
2519
+ console.log('\n๐ŸŽ‰ All tests passed!');
2520
+ break;
2521
+ }
2522
+ }
2523
+ if (iterations >= maxIterations) {
2524
+ console.log('\nโš ๏ธ Reached maximum iterations. Tests may not be complete.');
2525
+ }
2526
+ if (legitimateFailureReported) {
2527
+ console.log('\n๐Ÿ“‹ Test file updated with legitimate failures documented.');
2528
+ console.log(' These failures indicate bugs in the source code that need to be fixed.');
2529
+ }
2530
+ return testFilePath;
2531
+ }
2532
+ async function generateTestsForFunction() {
2533
+ console.log('\n๐ŸŽฏ Function-wise Test Generation\n');
2534
+ // List all files
2535
+ console.log('๐Ÿ“‚ Scanning repository...\n');
2536
+ const files = await listFilesRecursive('.');
2537
+ if (files.length === 0) {
2538
+ console.log('No source files found!');
2539
+ return;
2540
+ }
2541
+ console.log('Select a file:\n');
2542
+ files.forEach((file, index) => {
2543
+ console.log(`${index + 1}. ${file}`);
2544
+ });
2545
+ const fileChoice = await promptUser('\nEnter file number: ');
2546
+ const selectedFile = files[parseInt(fileChoice) - 1];
2547
+ if (!selectedFile) {
2548
+ console.log('Invalid selection!');
2549
+ return;
2550
+ }
2551
+ // Analyze file to get functions
2552
+ const analysis = analyzeFileAST(selectedFile);
2553
+ if (!analysis.success) {
2554
+ console.error(`Failed to analyze file: ${analysis.error}`);
2555
+ return;
2556
+ }
2557
+ const functions = analysis.analysis.functions.filter((f) => f.exported);
2558
+ if (functions.length === 0) {
2559
+ console.log('No exported functions found in the file!');
2560
+ return;
2561
+ }
2562
+ console.log(`\nFound ${functions.length} exported functions:\n`);
2563
+ functions.forEach((func, index) => {
2564
+ console.log(`${index + 1}. ${func.name} (${func.type}, ${func.async ? 'async' : 'sync'})`);
2565
+ });
2566
+ const functionsChoice = await promptUser('\nEnter function numbers (comma-separated, e.g., 1,3,4): ');
2567
+ const selectedIndices = functionsChoice.split(',').map(s => parseInt(s.trim()) - 1);
2568
+ const selectedFunctions = selectedIndices
2569
+ .filter(i => i >= 0 && i < functions.length)
2570
+ .map(i => functions[i].name);
2571
+ if (selectedFunctions.length === 0) {
2572
+ console.log('No valid functions selected!');
2573
+ return;
2574
+ }
2575
+ console.log(`\nโœ… Selected functions: ${selectedFunctions.join(', ')}\n`);
2576
+ await generateTestsForFunctions(selectedFile, selectedFunctions);
2577
+ console.log('\nโœจ Done!');
2578
+ }
2579
+ async function main() {
2580
+ console.log('๐Ÿงช AI-Powered Unit Test Generator with AST Analysis\n');
2581
+ // Load configuration from codeguard.json
2582
+ try {
2583
+ CONFIG = (0, config_1.loadConfig)();
2584
+ (0, config_1.validateConfig)(CONFIG);
2585
+ }
2586
+ catch (error) {
2587
+ console.error('โŒ Configuration Error:', error.message);
2588
+ console.error('\nPlease create a codeguard.json file in your project root.');
2589
+ console.error('Example:\n');
2590
+ console.error('{\n "aiProvider": "claude",\n "apiKeys": {\n "claude": "sk-ant-..."\n }\n}');
2591
+ process.exit(1);
2592
+ }
2593
+ // Check for required packages
2594
+ try {
2595
+ require('@babel/parser');
2596
+ require('@babel/traverse');
2597
+ }
2598
+ catch (error) {
2599
+ console.error('โŒ Missing required packages. Please install:');
2600
+ console.error('npm install @babel/parser @babel/traverse ts-node\n');
2601
+ process.exit(1);
2602
+ }
2603
+ // Optional: Codebase Indexing
2604
+ globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
2605
+ const hasExistingIndex = globalIndexer.hasIndex();
2606
+ if (hasExistingIndex) {
2607
+ // Index exists - automatically use it
2608
+ console.log('๐Ÿ“ฆ Loading codebase index...');
2609
+ const loaded = await globalIndexer.loadIndex();
2610
+ if (loaded) {
2611
+ const stats = globalIndexer.getStats();
2612
+ console.log(`โœ… Loaded cached index (${stats?.fileCount} files indexed)`);
2613
+ // Check for stale files (modified since last index)
2614
+ const staleFiles = globalIndexer.getStaleFiles();
2615
+ if (staleFiles.length > 0) {
2616
+ console.log(`๐Ÿ”„ Updating ${staleFiles.length} modified file(s)...`);
2617
+ // Show which files are being updated (if not too many)
2618
+ if (staleFiles.length <= 5) {
2619
+ staleFiles.forEach(f => console.log(` ๐Ÿ“ ${path.basename(f)}`));
2620
+ }
2621
+ else {
2622
+ console.log(` ๐Ÿ“ Updating ${staleFiles.length} files...`);
2623
+ }
2624
+ await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
2625
+ console.log(`โœ… Index updated!\n`);
2626
+ }
2627
+ else {
2628
+ console.log('โœ… All files up to date!\n');
2629
+ }
2630
+ }
2631
+ else {
2632
+ // Index file exists but failed to load - clear it
2633
+ console.log('โš ๏ธ Cache corrupted, will rebuild on next run.\n');
2634
+ globalIndexer = null;
2635
+ }
2636
+ }
2637
+ else {
2638
+ // No index exists - offer to create one (first time only)
2639
+ console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
2640
+ console.log('๐Ÿ“ฆ Codebase Indexing (First Time Setup)\n');
2641
+ console.log('Indexing analyzes your codebase once and caches the results.');
2642
+ console.log('This makes test generation 100x+ faster on subsequent runs!\n');
2643
+ console.log('Benefits:');
2644
+ console.log(' โœ“ Much faster analysis (cached AST parsing)');
2645
+ console.log(' โœ“ Instant dependency lookups');
2646
+ console.log(' โœ“ Better AI context\n');
2647
+ console.log('Note: Takes ~15 seconds now, saves time on every future run.');
2648
+ console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n');
2649
+ const useIndexing = await promptUser('Enable codebase indexing? (y/n, default=n): ');
2650
+ if (useIndexing.toLowerCase() === 'y' || useIndexing.toLowerCase() === 'yes') {
2651
+ console.log('\nโœ… Building codebase index...');
2652
+ console.log('This is a one-time process. Future runs will be instant!\n');
2653
+ // Determine which directory to scan
2654
+ // Priority: src/ if exists, otherwise current directory
2655
+ let scanDir = '.';
2656
+ if (fsSync.existsSync('src') && fsSync.statSync('src').isDirectory()) {
2657
+ scanDir = 'src';
2658
+ console.log('๐Ÿ“‚ Scanning src/ directory...\n');
2659
+ }
2660
+ else {
2661
+ console.log('๐Ÿ“‚ Scanning current directory...\n');
2662
+ }
2663
+ await globalIndexer.buildIndex(scanDir, analyzeFileAST, (current, total, file) => {
2664
+ const percent = Math.round((current / total) * 100);
2665
+ const barLength = 30;
2666
+ const filled = Math.round((current / total) * barLength);
2667
+ const bar = 'โ–ˆ'.repeat(filled) + 'โ–‘'.repeat(barLength - filled);
2668
+ process.stdout.write(`\r[${bar}] ${percent}% (${current}/${total}) ${path.basename(file)}${' '.repeat(20)}`);
2669
+ });
2670
+ console.log(); // New line after progress
2671
+ }
2672
+ else {
2673
+ console.log('\nโญ๏ธ Skipping indexing. You can enable it later by running with an empty cache.\n');
2674
+ globalIndexer = null;
2675
+ }
2676
+ }
2677
+ // Display selected AI provider from config
2678
+ console.log(`\nโœ… Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
2679
+ // Select test generation mode
2680
+ console.log('Select Test Generation Mode:');
2681
+ console.log('1. File-wise - Generate tests for a single file');
2682
+ console.log('2. Folder-wise - Generate tests for all files in a folder');
2683
+ console.log('3. Function-wise - Generate tests for specific functions in a file');
2684
+ const modeChoice = await promptUser('\nEnter mode choice (1-3): ');
2685
+ switch (modeChoice) {
2686
+ case '2':
2687
+ await generateTestsForFolder();
2688
+ break;
2689
+ case '3':
2690
+ await generateTestsForFunction();
2691
+ break;
2692
+ case '1':
2693
+ default:
2694
+ // File-wise mode (original functionality)
2695
+ console.log('\n๐Ÿ“‚ Scanning repository...\n');
2696
+ const files = await listFilesRecursive('.');
2697
+ if (files.length === 0) {
2698
+ console.log('No source files found!');
2699
+ return;
2700
+ }
2701
+ console.log('Select a file to generate tests:\n');
2702
+ files.forEach((file, index) => {
2703
+ console.log(`${index + 1}. ${file}`);
2704
+ });
2705
+ const choice = await promptUser('\nEnter file number: ');
2706
+ const selectedFile = files[parseInt(choice) - 1];
2707
+ if (!selectedFile) {
2708
+ console.log('Invalid selection!');
2709
+ return;
2710
+ }
2711
+ await generateTests(selectedFile);
2712
+ console.log('\nโœจ Done!');
2713
+ break;
2714
+ }
2715
+ }
2716
+ // Run if executed directly
2717
+ if (require.main === module) {
2718
+ main().catch(console.error);
2719
+ }
2720
+ //# sourceMappingURL=index.js.map