@theihtisham/ai-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.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/dist/analyzers/analyzer.d.ts +10 -0
  4. package/dist/analyzers/analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/analyzer.js +131 -0
  6. package/dist/analyzers/analyzer.js.map +1 -0
  7. package/dist/analyzers/go-analyzer.d.ts +3 -0
  8. package/dist/analyzers/go-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/go-analyzer.js +244 -0
  10. package/dist/analyzers/go-analyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +15 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/analyzers/js-ts-analyzer.d.ts +3 -0
  16. package/dist/analyzers/js-ts-analyzer.d.ts.map +1 -0
  17. package/dist/analyzers/js-ts-analyzer.js +299 -0
  18. package/dist/analyzers/js-ts-analyzer.js.map +1 -0
  19. package/dist/analyzers/python-analyzer.d.ts +3 -0
  20. package/dist/analyzers/python-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/python-analyzer.js +306 -0
  22. package/dist/analyzers/python-analyzer.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +381 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.d.ts.map +1 -0
  29. package/dist/config/defaults.js +80 -0
  30. package/dist/config/defaults.js.map +1 -0
  31. package/dist/config/index.d.ts +3 -0
  32. package/dist/config/index.d.ts.map +1 -0
  33. package/dist/config/index.js +14 -0
  34. package/dist/config/index.js.map +1 -0
  35. package/dist/config/loader.d.ts +6 -0
  36. package/dist/config/loader.d.ts.map +1 -0
  37. package/dist/config/loader.js +126 -0
  38. package/dist/config/loader.js.map +1 -0
  39. package/dist/coverage.d.ts +4 -0
  40. package/dist/coverage.d.ts.map +1 -0
  41. package/dist/coverage.js +108 -0
  42. package/dist/coverage.js.map +1 -0
  43. package/dist/generators/ai-generator.d.ts +4 -0
  44. package/dist/generators/ai-generator.d.ts.map +1 -0
  45. package/dist/generators/ai-generator.js +175 -0
  46. package/dist/generators/ai-generator.js.map +1 -0
  47. package/dist/generators/generator.d.ts +4 -0
  48. package/dist/generators/generator.d.ts.map +1 -0
  49. package/dist/generators/generator.js +121 -0
  50. package/dist/generators/generator.js.map +1 -0
  51. package/dist/generators/go-generator.d.ts +3 -0
  52. package/dist/generators/go-generator.d.ts.map +1 -0
  53. package/dist/generators/go-generator.js +175 -0
  54. package/dist/generators/go-generator.js.map +1 -0
  55. package/dist/generators/index.d.ts +6 -0
  56. package/dist/generators/index.d.ts.map +1 -0
  57. package/dist/generators/index.js +16 -0
  58. package/dist/generators/index.js.map +1 -0
  59. package/dist/generators/js-ts-generator.d.ts +3 -0
  60. package/dist/generators/js-ts-generator.d.ts.map +1 -0
  61. package/dist/generators/js-ts-generator.js +331 -0
  62. package/dist/generators/js-ts-generator.js.map +1 -0
  63. package/dist/generators/python-generator.d.ts +3 -0
  64. package/dist/generators/python-generator.d.ts.map +1 -0
  65. package/dist/generators/python-generator.js +180 -0
  66. package/dist/generators/python-generator.js.map +1 -0
  67. package/dist/incremental.d.ts +16 -0
  68. package/dist/incremental.d.ts.map +1 -0
  69. package/dist/incremental.js +146 -0
  70. package/dist/incremental.js.map +1 -0
  71. package/dist/index.d.ts +9 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +44 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/mutation/index.d.ts +2 -0
  76. package/dist/mutation/index.d.ts.map +1 -0
  77. package/dist/mutation/index.js +9 -0
  78. package/dist/mutation/index.js.map +1 -0
  79. package/dist/mutation/mutator.d.ts +6 -0
  80. package/dist/mutation/mutator.d.ts.map +1 -0
  81. package/dist/mutation/mutator.js +237 -0
  82. package/dist/mutation/mutator.js.map +1 -0
  83. package/dist/types.d.ts +199 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +4 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/utils/file.d.ts +10 -0
  88. package/dist/utils/file.d.ts.map +1 -0
  89. package/dist/utils/file.js +108 -0
  90. package/dist/utils/file.js.map +1 -0
  91. package/dist/utils/index.d.ts +4 -0
  92. package/dist/utils/index.d.ts.map +1 -0
  93. package/dist/utils/index.js +24 -0
  94. package/dist/utils/index.js.map +1 -0
  95. package/dist/utils/language.d.ts +8 -0
  96. package/dist/utils/language.d.ts.map +1 -0
  97. package/dist/utils/language.js +137 -0
  98. package/dist/utils/language.js.map +1 -0
  99. package/dist/utils/logger.d.ts +13 -0
  100. package/dist/utils/logger.d.ts.map +1 -0
  101. package/dist/utils/logger.js +57 -0
  102. package/dist/utils/logger.js.map +1 -0
  103. package/dist/watcher/index.d.ts +2 -0
  104. package/dist/watcher/index.d.ts.map +1 -0
  105. package/dist/watcher/index.js +6 -0
  106. package/dist/watcher/index.js.map +1 -0
  107. package/dist/watcher/watcher.d.ts +19 -0
  108. package/dist/watcher/watcher.d.ts.map +1 -0
  109. package/dist/watcher/watcher.js +122 -0
  110. package/dist/watcher/watcher.js.map +1 -0
  111. package/package.json +63 -0
  112. package/src/analyzers/analyzer.ts +180 -0
  113. package/src/analyzers/go-analyzer.ts +235 -0
  114. package/src/analyzers/index.ts +4 -0
  115. package/src/analyzers/js-ts-analyzer.ts +324 -0
  116. package/src/analyzers/python-analyzer.ts +306 -0
  117. package/src/cli.ts +416 -0
  118. package/src/config/defaults.ts +81 -0
  119. package/src/config/index.ts +2 -0
  120. package/src/config/loader.ts +114 -0
  121. package/src/coverage.ts +128 -0
  122. package/src/generators/ai-generator.ts +170 -0
  123. package/src/generators/generator.ts +117 -0
  124. package/src/generators/go-generator.ts +183 -0
  125. package/src/generators/index.ts +5 -0
  126. package/src/generators/js-ts-generator.ts +379 -0
  127. package/src/generators/python-generator.ts +201 -0
  128. package/src/incremental.ts +131 -0
  129. package/src/index.ts +8 -0
  130. package/src/mutation/index.ts +1 -0
  131. package/src/mutation/mutator.ts +314 -0
  132. package/src/types.ts +240 -0
  133. package/src/utils/file.ts +73 -0
  134. package/src/utils/index.ts +3 -0
  135. package/src/utils/language.ts +114 -0
  136. package/src/utils/logger.ts +61 -0
  137. package/src/watcher/index.ts +1 -0
  138. package/src/watcher/watcher.ts +103 -0
  139. package/tests/analyzer.test.ts +429 -0
  140. package/tests/config.test.ts +171 -0
  141. package/tests/coverage.test.ts +197 -0
  142. package/tests/file-utils.test.ts +121 -0
  143. package/tests/generators.test.ts +383 -0
  144. package/tests/incremental.test.ts +108 -0
  145. package/tests/language.test.ts +90 -0
  146. package/tests/mutation.test.ts +286 -0
  147. package/tests/watcher.test.ts +35 -0
  148. package/tsconfig.json +26 -0
  149. package/vitest.config.ts +25 -0
@@ -0,0 +1,128 @@
1
+ import {
2
+ SourceAnalysis,
3
+ CoveragePrediction,
4
+ TestSuite,
5
+ } from './types.js';
6
+
7
+ export function predictCoverage(analysis: SourceAnalysis, suite: TestSuite): CoveragePrediction {
8
+ const uncoveredPaths: string[] = [];
9
+ const suggestions: string[] = [];
10
+
11
+ const testedNames = new Set(suite.testCases.map((tc) => tc.name.split(' ')[0] ?? ''));
12
+
13
+ // Check function coverage
14
+ let coveredFunctions = 0;
15
+ for (const fn of analysis.functions) {
16
+ if (fn.isExported) {
17
+ const isTested = testedNames.has(fn.name);
18
+ if (isTested) {
19
+ coveredFunctions++;
20
+ } else {
21
+ uncoveredPaths.push(`Function ${fn.name} is not tested`);
22
+ suggestions.push(`Add tests for exported function: ${fn.name}`);
23
+ }
24
+ }
25
+ }
26
+
27
+ // Check class method coverage
28
+ let coveredMethods = 0;
29
+ let totalMethods = 0;
30
+ for (const cls of analysis.classes) {
31
+ for (const method of cls.methods) {
32
+ totalMethods++;
33
+ const isTested = testedNames.has(method.name) || Array.from(testedNames).some((n) => n.includes(method.name));
34
+ if (isTested) {
35
+ coveredMethods++;
36
+ } else {
37
+ uncoveredPaths.push(`${cls.name}.${method.name} is not tested`);
38
+ suggestions.push(`Add tests for ${cls.name}.${method.name} method`);
39
+ }
40
+ }
41
+ }
42
+
43
+ // Check for uncovered error paths
44
+ for (const fn of analysis.functions) {
45
+ if (fn.throws.length > 0) {
46
+ const hasErrorTest = suite.testCases.some(
47
+ (tc) => tc.name.includes(fn.name) && (tc.tags.includes('error') || tc.tags.includes('edge-case')),
48
+ );
49
+ if (!hasErrorTest) {
50
+ uncoveredPaths.push(`${fn.name} error paths not tested`);
51
+ suggestions.push(`Add error path tests for ${fn.name} (throws: ${fn.throws.join(', ')})`);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Check for untested async error paths
57
+ for (const fn of analysis.functions) {
58
+ if (fn.isAsync) {
59
+ const hasAsyncErrorTest = suite.testCases.some(
60
+ (tc) => tc.name.includes(fn.name) && tc.tags.includes('async'),
61
+ );
62
+ if (!hasAsyncErrorTest) {
63
+ suggestions.push(`Add async rejection test for ${fn.name}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Check for untested optional params
69
+ for (const fn of analysis.functions) {
70
+ const optionalParams = fn.params.filter((p) => p.optional);
71
+ if (optionalParams.length > 0) {
72
+ const hasOptionalTest = suite.testCases.some(
73
+ (tc) => tc.name.includes(fn.name) && tc.name.includes('undefined'),
74
+ );
75
+ if (!hasOptionalTest) {
76
+ suggestions.push(
77
+ `Test ${fn.name} without optional params: ${optionalParams.map((p) => p.name).join(', ')}`,
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ // Calculate estimates
84
+ const totalExported = analysis.functions.filter((f) => f.isExported).length + totalMethods;
85
+ const covered = coveredFunctions + coveredMethods;
86
+ const functionCoverage = totalExported > 0 ? Math.round((covered / totalExported) * 100) : 100;
87
+
88
+ const lineCoverage = Math.min(functionCoverage + 10, suite.coverageEstimate);
89
+ const branchCoverage = Math.max(functionCoverage - 10, 0);
90
+
91
+ return {
92
+ filePath: analysis.filePath,
93
+ estimatedLineCoverage: lineCoverage,
94
+ estimatedBranchCoverage: branchCoverage,
95
+ estimatedFunctionCoverage: functionCoverage,
96
+ uncoveredPaths: uncoveredPaths.slice(0, 10),
97
+ suggestions: suggestions.slice(0, 10),
98
+ };
99
+ }
100
+
101
+ export function formatCoverageReport(prediction: CoveragePrediction): string {
102
+ const lines: string[] = [];
103
+ lines.push('');
104
+ lines.push(` Coverage Prediction: ${prediction.filePath}`);
105
+ lines.push(' ----------------------------------------');
106
+ lines.push(` Line coverage: ~${prediction.estimatedLineCoverage}%`);
107
+ lines.push(` Branch coverage: ~${prediction.estimatedBranchCoverage}%`);
108
+ lines.push(` Function coverage: ~${prediction.estimatedFunctionCoverage}%`);
109
+
110
+ if (prediction.uncoveredPaths.length > 0) {
111
+ lines.push('');
112
+ lines.push(' Uncovered paths:');
113
+ for (const path of prediction.uncoveredPaths) {
114
+ lines.push(` - ${path}`);
115
+ }
116
+ }
117
+
118
+ if (prediction.suggestions.length > 0) {
119
+ lines.push('');
120
+ lines.push(' Suggestions:');
121
+ for (const suggestion of prediction.suggestions) {
122
+ lines.push(` + ${suggestion}`);
123
+ }
124
+ }
125
+
126
+ lines.push('');
127
+ return lines.join('\n');
128
+ }
@@ -0,0 +1,170 @@
1
+ import * as fs from 'fs';
2
+ import OpenAI from 'openai';
3
+ import {
4
+ SourceAnalysis,
5
+ TestSuite,
6
+ TestFramework,
7
+ TestGenConfig,
8
+ } from '../types.js';
9
+ import { detectEdgeCases } from '../analyzers/analyzer.js';
10
+
11
+ export async function generateWithAI(
12
+ analysis: SourceAnalysis,
13
+ framework: TestFramework,
14
+ config: TestGenConfig,
15
+ ): Promise<string> {
16
+ if (!config.ai.enabled || config.ai.provider === 'none') {
17
+ throw new Error('AI generation is not enabled in configuration');
18
+ }
19
+
20
+ const apiKey = process.env[config.ai.apiKeyEnv];
21
+ if (!apiKey) {
22
+ throw new Error(
23
+ `AI API key not found. Set the ${config.ai.apiKeyEnv} environment variable.`,
24
+ );
25
+ }
26
+
27
+ const client = new OpenAI({ apiKey, baseURL: getBaseURL(config) });
28
+
29
+ const prompt = buildPrompt(analysis, framework, config);
30
+
31
+ const response = await client.chat.completions.create({
32
+ model: config.ai.model,
33
+ messages: [
34
+ {
35
+ role: 'system',
36
+ content: getSystemPrompt(framework),
37
+ },
38
+ {
39
+ role: 'user',
40
+ content: prompt,
41
+ },
42
+ ],
43
+ max_tokens: config.ai.maxTokens,
44
+ temperature: config.ai.temperature,
45
+ });
46
+
47
+ const content = response.choices[0]?.message?.content;
48
+ if (!content) {
49
+ throw new Error('AI returned empty response');
50
+ }
51
+
52
+ // Extract code from markdown code blocks if present
53
+ const codeBlockMatch = content.match(/```(?:typescript|javascript|python|go|tsx|jsx)?\n([\s\S]*?)```/);
54
+ return codeBlockMatch?.[1]?.trim() ?? content.trim();
55
+ }
56
+
57
+ function getBaseURL(config: TestGenConfig): string | undefined {
58
+ if (config.ai.provider === 'openai') return undefined; // uses default
59
+ if (config.ai.provider === 'anthropic') return 'https://api.anthropic.com/v1';
60
+ return undefined;
61
+ }
62
+
63
+ function getSystemPrompt(framework: TestFramework): string {
64
+ const frameworkName = framework === 'go-test' ? 'Go testing' : framework;
65
+ return `You are an expert test engineer. Generate comprehensive, well-structured test suites using ${frameworkName}.
66
+ Follow these rules:
67
+ - Use the appropriate testing patterns and best practices for ${frameworkName}
68
+ - Include edge cases: null/undefined/nil, empty inputs, boundary values
69
+ - Mock external dependencies properly
70
+ - Include descriptive test names and comments
71
+ - Handle both happy path and error cases
72
+ - Return ONLY the test code, no explanations outside code comments`;
73
+ }
74
+
75
+ function buildPrompt(
76
+ analysis: SourceAnalysis,
77
+ framework: TestFramework,
78
+ config: TestGenConfig,
79
+ ): string {
80
+ const parts: string[] = [];
81
+
82
+ parts.push(`Generate a complete test suite for the following ${analysis.language} source file.`);
83
+ parts.push(`Use the ${framework} testing framework.`);
84
+ parts.push('');
85
+
86
+ if (config.ai.privacyMode) {
87
+ // Send only analysis metadata, not full source
88
+ parts.push('## Source Code Analysis');
89
+ parts.push(`File: ${analysis.filePath}`);
90
+ parts.push(`Language: ${analysis.language}`);
91
+ parts.push(`Lines of code: ${analysis.linesOfCode}`);
92
+ parts.push('');
93
+
94
+ parts.push('## Exported Functions');
95
+ for (const fn of analysis.functions.filter((f) => f.isExported)) {
96
+ const params = fn.params.map((p) => `${p.name}${p.type ? ': ' + p.type : ''}`).join(', ');
97
+ parts.push(`- ${fn.name}(${params})${fn.returnType ? ': ' + fn.returnType : ''}${fn.isAsync ? ' [async]' : ''}`);
98
+ if (fn.throws.length > 0) {
99
+ parts.push(` Throws: ${fn.throws.join(', ')}`);
100
+ }
101
+ }
102
+ parts.push('');
103
+
104
+ parts.push('## Classes');
105
+ for (const cls of analysis.classes) {
106
+ parts.push(`- ${cls.name}${cls.extends ? ` extends ${cls.extends}` : ''}`);
107
+ for (const method of cls.methods) {
108
+ const params = method.params.map((p) => `${p.name}${p.type ? ': ' + p.type : ''}`).join(', ');
109
+ parts.push(` - ${method.name}(${params})${method.returnType ? ': ' + method.returnType : ''}`);
110
+ }
111
+ }
112
+ parts.push('');
113
+
114
+ parts.push('## Interfaces');
115
+ for (const iface of analysis.interfaces) {
116
+ parts.push(`- ${iface.name}`);
117
+ for (const prop of iface.properties) {
118
+ parts.push(` - ${prop.name}${prop.type ? ': ' + prop.type : ''}${prop.optional ? '?' : ''}`);
119
+ }
120
+ }
121
+ parts.push('');
122
+
123
+ parts.push('## Imports');
124
+ for (const imp of analysis.imports) {
125
+ parts.push(`- ${imp.modulePath}: ${imp.namedImports.join(', ') || 'default'}`);
126
+ }
127
+ parts.push('');
128
+
129
+ parts.push('## Edge Cases to Cover');
130
+ const edgeCases = analysis.functions.flatMap((fn) => detectEdgeCases(fn)).slice(0, 10);
131
+ for (const ec of edgeCases) {
132
+ parts.push(`- ${ec.name}: ${ec.expectedBehavior}`);
133
+ }
134
+ } else {
135
+ // Send full source code (not recommended for proprietary code)
136
+ parts.push('## Source Code');
137
+ parts.push('```' + analysis.language);
138
+ try {
139
+ const source = fs.readFileSync(analysis.filePath, 'utf-8');
140
+ parts.push(source);
141
+ } catch {
142
+ parts.push('(Could not read source file)');
143
+ }
144
+ parts.push('```');
145
+ }
146
+
147
+ parts.push('');
148
+ parts.push('Generate tests that achieve at least 90% code coverage.');
149
+
150
+ return parts.join('\n');
151
+ }
152
+
153
+ export function buildAISuite(
154
+ generatedCode: string,
155
+ analysis: SourceAnalysis,
156
+ framework: TestFramework,
157
+ ): TestSuite {
158
+ return {
159
+ filePath: '',
160
+ sourceFilePath: analysis.filePath,
161
+ language: analysis.language,
162
+ framework,
163
+ testCases: [],
164
+ mocks: [],
165
+ imports: [generatedCode],
166
+ setupCode: '',
167
+ teardownCode: '',
168
+ coverageEstimate: 90, // AI-generated tests target 90%+
169
+ };
170
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ SourceAnalysis,
3
+ TestSuite,
4
+ TestFramework,
5
+ TestGenConfig,
6
+ GenerationResult,
7
+ CLIOptions,
8
+ } from '../types.js';
9
+ import { generateJsTsTestSuite } from './js-ts-generator.js';
10
+ import { generatePythonTestSuite } from './python-generator.js';
11
+ import { generateGoTestSuite } from './go-generator.js';
12
+ import { generateWithAI, buildAISuite } from './ai-generator.js';
13
+ import { buildTestFilePath } from '../utils/language.js';
14
+ import { writeFile } from '../utils/file.js';
15
+ import * as path from 'path';
16
+
17
+ export async function generateTestSuite(
18
+ analysis: SourceAnalysis,
19
+ framework: TestFramework,
20
+ config: TestGenConfig,
21
+ options: CLIOptions,
22
+ ): Promise<GenerationResult> {
23
+ const startTime = Date.now();
24
+ let suite: TestSuite;
25
+ let usedAI = false;
26
+
27
+ // Try AI generation first if enabled
28
+ if (config.ai.enabled && config.ai.provider !== 'none' && !options.noAI) {
29
+ try {
30
+ const generatedCode = await generateWithAI(analysis, framework, config);
31
+ suite = buildAISuite(generatedCode, analysis, framework);
32
+ usedAI = true;
33
+ } catch (err) {
34
+ const errorMessage = err instanceof Error ? err.message : String(err);
35
+ console.warn(`AI generation failed, falling back to AST-based: ${errorMessage}`);
36
+ suite = generateLocalSuite(analysis, framework, config);
37
+ }
38
+ } else {
39
+ suite = generateLocalSuite(analysis, framework, config);
40
+ }
41
+
42
+ // Determine output path
43
+ const testFilePath = buildTestFilePath(
44
+ analysis.filePath,
45
+ options.output ?? config.outputDir,
46
+ analysis.language,
47
+ framework,
48
+ );
49
+ suite.filePath = testFilePath;
50
+
51
+ // Get the final code
52
+ const code = suite.imports[0] ?? '';
53
+
54
+ if (!options.dryRun) {
55
+ writeFile(testFilePath, code);
56
+ }
57
+
58
+ return {
59
+ sourceFile: analysis.filePath,
60
+ testFile: testFilePath,
61
+ testSuite: suite,
62
+ duration: Date.now() - startTime,
63
+ usedAI,
64
+ };
65
+ }
66
+
67
+ function generateLocalSuite(
68
+ analysis: SourceAnalysis,
69
+ framework: TestFramework,
70
+ config: TestGenConfig,
71
+ ): TestSuite {
72
+ const maxTests = config.generation.maxTestsPerFunction;
73
+
74
+ switch (analysis.language) {
75
+ case 'typescript':
76
+ case 'javascript':
77
+ return generateJsTsTestSuite(analysis, framework, maxTests);
78
+ case 'python':
79
+ return generatePythonTestSuite(analysis, framework, maxTests);
80
+ case 'go':
81
+ return generateGoTestSuite(analysis, framework, maxTests);
82
+ case 'rust':
83
+ throw new Error('Rust test generation requires AI mode. Enable AI in your configuration.');
84
+ default:
85
+ throw new Error(`Unsupported language: ${analysis.language}`);
86
+ }
87
+ }
88
+
89
+ export function formatResults(results: GenerationResult[]): string {
90
+ const lines: string[] = [];
91
+ let totalTests = 0;
92
+ let totalDuration = 0;
93
+ let totalCoverage = 0;
94
+
95
+ for (const result of results) {
96
+ totalTests += result.testSuite.testCases.length;
97
+ totalDuration += result.duration;
98
+ totalCoverage += result.testSuite.coverageEstimate;
99
+ }
100
+
101
+ const avgCoverage = results.length > 0 ? Math.round(totalCoverage / results.length) : 0;
102
+
103
+ lines.push('');
104
+ lines.push(` Files analyzed: ${results.length}`);
105
+ lines.push(` Tests generated: ${totalTests}`);
106
+ lines.push(` Avg coverage estimate: ${avgCoverage}%`);
107
+ lines.push(` Total time: ${totalDuration}ms`);
108
+ lines.push(` AI assisted: ${results.some((r) => r.usedAI) ? 'Yes' : 'No'}`);
109
+ lines.push('');
110
+
111
+ for (const result of results) {
112
+ lines.push(` ${path.basename(result.testFile)}`);
113
+ lines.push(` ${result.testSuite.testCases.length} tests | ~${result.testSuite.coverageEstimate}% coverage | ${result.duration}ms`);
114
+ }
115
+
116
+ return lines.join('\n');
117
+ }
@@ -0,0 +1,183 @@
1
+ import {
2
+ SourceAnalysis,
3
+ TestSuite,
4
+ TestCase,
5
+ TestFramework,
6
+ AnalyzedFunction,
7
+ } from '../types.js';
8
+ import { detectEdgeCases } from '../analyzers/analyzer.js';
9
+
10
+ export function generateGoTestSuite(
11
+ analysis: SourceAnalysis,
12
+ _framework: TestFramework,
13
+ maxTestsPerFunction: number,
14
+ ): TestSuite {
15
+ const testCases: TestCase[] = [];
16
+
17
+ for (const fn of analysis.functions) {
18
+ if (!fn.isExported) continue;
19
+ testCases.push(...generateGoFunctionTests(fn));
20
+ if (testCases.length > maxTestsPerFunction * analysis.functions.length) break;
21
+ }
22
+
23
+ for (const fn of analysis.functions) {
24
+ if (!fn.isExported) continue;
25
+ const edgeCases = detectEdgeCases(fn).slice(0, 2);
26
+ testCases.push(...edgeCases);
27
+ }
28
+
29
+ const code = renderGoTestSuite(analysis, testCases);
30
+
31
+ return {
32
+ filePath: '',
33
+ sourceFilePath: analysis.filePath,
34
+ language: 'go',
35
+ framework: 'go-test',
36
+ testCases,
37
+ mocks: [],
38
+ imports: [code],
39
+ setupCode: '',
40
+ teardownCode: '',
41
+ coverageEstimate: estimateGoCoverage(analysis, testCases),
42
+ };
43
+ }
44
+
45
+ function generateGoFunctionTests(fn: AnalyzedFunction): TestCase[] {
46
+ const cases: TestCase[] = [];
47
+
48
+ cases.push({
49
+ name: `Test${fn.name}`,
50
+ type: 'unit',
51
+ description: `Test ${fn.name} with valid input`,
52
+ code: renderGoTestFunction(fn, 'happy'),
53
+ expectedBehavior: 'Returns expected result',
54
+ inputDescription: 'Valid input',
55
+ tags: ['unit'],
56
+ });
57
+
58
+ if (fn.returnType && fn.returnType.includes('error')) {
59
+ cases.push({
60
+ name: `Test${fn.name}Error`,
61
+ type: 'unit',
62
+ description: `Test ${fn.name} returns error for invalid input`,
63
+ code: renderGoTestFunction(fn, 'error'),
64
+ expectedBehavior: 'Returns error',
65
+ inputDescription: 'Invalid input',
66
+ tags: ['unit', 'error'],
67
+ });
68
+ }
69
+
70
+ return cases;
71
+ }
72
+
73
+ function renderGoTestFunction(fn: AnalyzedFunction, testType: string): string {
74
+ const args = fn.params.map((p) => generateGoMockValue(p)).join(', ');
75
+ const lines: string[] = [];
76
+
77
+ lines.push(`func Test${fn.name}${testType === 'error' ? 'Error' : ''}(t *testing.T) {`);
78
+
79
+ if (testType === 'happy') {
80
+ if (fn.returnType && fn.returnType.includes('error')) {
81
+ lines.push(` result, err := ${fn.name}(${args})`);
82
+ lines.push(' if err != nil {');
83
+ lines.push(' t.Fatalf("unexpected error: %v", err)');
84
+ lines.push(' }');
85
+ lines.push(' if result == nil {');
86
+ lines.push(' t.Error("expected non-nil result")');
87
+ lines.push(' }');
88
+ } else if (fn.returnType) {
89
+ lines.push(` result := ${fn.name}(${args})`);
90
+ lines.push(' if result == nil {');
91
+ lines.push(' t.Error("expected non-nil result")');
92
+ lines.push(' }');
93
+ } else {
94
+ lines.push(` ${fn.name}(${args})`);
95
+ }
96
+ } else {
97
+ lines.push(` _, err := ${fn.name}(${generateGoInvalidArgs(fn)})`);
98
+ lines.push(' if err == nil {');
99
+ lines.push(' t.Error("expected error, got nil")');
100
+ lines.push(' }');
101
+ }
102
+
103
+ lines.push('}');
104
+ return lines.join('\n');
105
+ }
106
+
107
+ function generateGoMockValue(param: { type: string | null; name: string }): string {
108
+ if (!param.type) return 'nil';
109
+
110
+ const t = param.type;
111
+ if (t.includes('string')) return '"test"';
112
+ if (t.includes('int')) return '42';
113
+ if (t.includes('float')) return '3.14';
114
+ if (t.includes('bool')) return 'true';
115
+ if (t.includes('[]byte')) return '[]byte("test")';
116
+ if (t.includes('[]')) return 'nil';
117
+ if (t.includes('map')) return 'nil';
118
+ if (t.includes('context')) return 'context.Background()';
119
+ if (t.includes('error')) return 'nil';
120
+ if (t.includes('io.Reader')) return 'strings.NewReader("test")';
121
+ if (t.includes('time.Duration')) return 'time.Second';
122
+
123
+ return 'nil';
124
+ }
125
+
126
+ function generateGoInvalidArgs(fn: AnalyzedFunction): string {
127
+ return fn.params
128
+ .map((p) => {
129
+ if (p.type?.includes('string')) return '""';
130
+ if (p.type?.includes('int')) return '-1';
131
+ return 'nil';
132
+ })
133
+ .join(', ');
134
+ }
135
+
136
+ function renderGoTestSuite(analysis: SourceAnalysis, testCases: TestCase[]): string {
137
+ const lines: string[] = [];
138
+
139
+ lines.push(`package ${getGoPackageName(analysis.filePath)}`);
140
+ lines.push('');
141
+ lines.push('import (');
142
+ lines.push(' "testing"');
143
+
144
+ const needsContext = testCases.some((tc) => tc.code?.includes('context.'));
145
+ const needsStrings = testCases.some((tc) => tc.code?.includes('strings.'));
146
+ const needsTime = testCases.some((tc) => tc.code?.includes('time.'));
147
+
148
+ if (needsContext) lines.push(' "context"');
149
+ if (needsStrings) lines.push(' "strings"');
150
+ if (needsTime) lines.push(' "time"');
151
+
152
+ lines.push(')');
153
+ lines.push('');
154
+
155
+ for (const test of testCases) {
156
+ if (test.code) {
157
+ lines.push(test.code);
158
+ } else {
159
+ lines.push(`func ${test.name}(t *testing.T) {`);
160
+ lines.push(` // ${test.description}`);
161
+ lines.push(` // Expected: ${test.expectedBehavior}`);
162
+ lines.push(' t.Skip("placeholder test")');
163
+ lines.push('}');
164
+ }
165
+ lines.push('');
166
+ }
167
+
168
+ return lines.join('\n');
169
+ }
170
+
171
+ function getGoPackageName(filePath: string): string {
172
+ const parts = filePath.replace(/\\/g, '/').split('/');
173
+ const dir = parts.length > 1 ? parts[parts.length - 2] : 'main';
174
+ return dir ?? 'main';
175
+ }
176
+
177
+ function estimateGoCoverage(analysis: SourceAnalysis, testCases: TestCase[]): number {
178
+ const exportedFunctions = analysis.functions.filter((f) => f.isExported).length;
179
+ if (exportedFunctions === 0) return 0;
180
+ const testedFunctions = new Set(testCases.map((tc) => tc.name.replace(/^Test/, '').replace(/Error$/, '')));
181
+ const coveredRatio = Math.min(testedFunctions.size / exportedFunctions, 1);
182
+ return Math.min(Math.round(coveredRatio * 80 + 10), 100);
183
+ }
@@ -0,0 +1,5 @@
1
+ export { generateTestSuite, formatResults } from './generator.js';
2
+ export { generateJsTsTestSuite } from './js-ts-generator.js';
3
+ export { generatePythonTestSuite } from './python-generator.js';
4
+ export { generateGoTestSuite } from './go-generator.js';
5
+ export { generateWithAI, buildAISuite } from './ai-generator.js';