@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
package/src/cli.ts ADDED
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from 'fs';
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import * as path from 'path';
8
+
9
+ import { CLIOptions, TestType, SupportedLanguage, TestFramework, TestGenConfig, GenerationResult } from './types.js';
10
+ import { resolveConfig, createSampleConfig } from './config/index.js';
11
+ import { detectLanguage, detectFramework, logger, setLogLevel, findSourceFiles, shouldAnalyze } from './utils/index.js';
12
+ import { analyzeSource } from './analyzers/index.js';
13
+ import { generateTestSuite, formatResults } from './generators/index.js';
14
+ import { predictCoverage, formatCoverageReport } from './coverage.js';
15
+ import { generateMutants, calculateMutationScore, formatMutationReport } from './mutation/index.js';
16
+ import { FileWatcher } from './watcher/index.js';
17
+ import { IncrementalCache } from './incremental.js';
18
+
19
+ const VERSION = '1.0.0';
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name('ai-testgen')
25
+ .description('AI-powered test generator - Stop writing tests, start generating them')
26
+ .version(VERSION);
27
+
28
+ program
29
+ .command('generate')
30
+ .description('Generate test suites for source files')
31
+ .argument('<source>', 'Source file or directory to analyze')
32
+ .option('-o, --output <dir>', 'Output directory for test files')
33
+ .option('-c, --config <path>', 'Path to config file')
34
+ .option('-l, --language <lang>', 'Force language (typescript|javascript|python|go)')
35
+ .option('-f, --framework <fw>', 'Force test framework (jest|vitest|pytest|go-test)')
36
+ .option('--no-ai', 'Disable AI generation, use only AST analysis')
37
+ .option('--dry-run', 'Show what would be generated without writing files')
38
+ .option('--types <types>', 'Test types to generate (unit,integration,edge-case,mock-setup)', 'unit,edge-case')
39
+ .option('--coverage', 'Show coverage prediction')
40
+ .option('--mutation', 'Run mutation testing')
41
+ .option('-v, --verbose', 'Verbose output')
42
+ .action(async (source: string, options: Partial<CLIOptions> = {}) => {
43
+ await runGenerate(source, options);
44
+ });
45
+
46
+ program
47
+ .command('init')
48
+ .description('Create a sample .aitestgen.yml configuration file')
49
+ .action(() => {
50
+ const outputPath = path.join(process.cwd(), '.aitestgen.yml');
51
+ createSampleConfig(outputPath);
52
+ console.log(chalk.green(` Created configuration file: ${outputPath}`));
53
+ console.log(chalk.gray(' Edit the file to customize test generation settings.'));
54
+ });
55
+
56
+ program
57
+ .command('watch')
58
+ .description('Watch for file changes and auto-generate tests')
59
+ .argument('<source>', 'Directory to watch')
60
+ .option('-o, --output <dir>', 'Output directory for test files')
61
+ .option('-c, --config <path>', 'Path to config file')
62
+ .option('--no-ai', 'Disable AI generation')
63
+ .option('-v, --verbose', 'Verbose output')
64
+ .action(async (source: string, options: Partial<CLIOptions> = {}) => {
65
+ await runWatch(source, options);
66
+ });
67
+
68
+ program
69
+ .command('mutation')
70
+ .description('Run mutation testing on source files')
71
+ .argument('<source>', 'Source file to mutate')
72
+ .option('-c, --config <path>', 'Path to config file')
73
+ .option('-v, --verbose', 'Verbose output')
74
+ .action(async (source: string, options: Partial<CLIOptions> = {}) => {
75
+ await runMutation(source, options);
76
+ });
77
+
78
+ program
79
+ .command('analyze')
80
+ .description('Analyze source code without generating tests')
81
+ .argument('<source>', 'Source file to analyze')
82
+ .option('-c, --config <path>', 'Path to config file')
83
+ .option('-v, --verbose', 'Verbose output')
84
+ .action(async (source: string, options: Partial<CLIOptions> = {}) => {
85
+ await runAnalyze(source, options);
86
+ });
87
+
88
+ async function runGenerate(source: string, options: Partial<CLIOptions>): Promise<void> {
89
+ const startTime = Date.now();
90
+ const sourcePath = path.resolve(source);
91
+ const cliOptions = normalizeOptions(options);
92
+
93
+ if (cliOptions.verbose) setLogLevel('debug');
94
+
95
+ const config = resolveConfig(
96
+ { watch: { enabled: false }, incremental: { enabled: false } } as Partial<TestGenConfig>,
97
+ path.dirname(sourcePath),
98
+ );
99
+
100
+ console.log(chalk.bold.blue('\n AI-TestGen') + chalk.gray(` v${VERSION}`));
101
+ console.log(chalk.gray(' Stop writing tests. Start generating them.\n'));
102
+
103
+ const spinner = ora('Finding source files...').start();
104
+
105
+ try {
106
+ const isDir = fs.statSync(sourcePath).isDirectory();
107
+ let files: string[];
108
+
109
+ if (isDir) {
110
+ files = await findSourceFiles(
111
+ config.include.length > 0 ? config.include : ['**/*.{ts,tsx,js,jsx,py,go}'],
112
+ sourcePath,
113
+ config.exclude,
114
+ );
115
+ } else {
116
+ files = [sourcePath];
117
+ }
118
+
119
+ files = files.filter((f) => shouldAnalyze(f, config.exclude));
120
+
121
+ if (files.length === 0) {
122
+ spinner.warn('No source files found to analyze.');
123
+ return;
124
+ }
125
+
126
+ spinner.text = `Analyzing ${files.length} file(s)...`;
127
+
128
+ // Incremental mode
129
+ if (cliOptions.incremental || config.incremental.enabled) {
130
+ const cache = new IncrementalCache(config);
131
+ await cache.initialize();
132
+ const filesToProcess = cache.getChangedFiles(files);
133
+ spinner.text = `Incremental mode: ${filesToProcess.length}/${files.length} files changed`;
134
+
135
+ const incrResults: GenerationResult[] = [];
136
+ for (const file of filesToProcess) {
137
+ const result = await processFile(file, cliOptions, config, spinner);
138
+ if (result) {
139
+ incrResults.push(result);
140
+ cache.markGenerated(file, result.testFile);
141
+ }
142
+ }
143
+
144
+ if (incrResults.length === 0) {
145
+ spinner.succeed('All files up to date. No changes detected.');
146
+ return;
147
+ }
148
+
149
+ spinner.succeed(`Generated ${incrResults.length} test suite(s)`);
150
+ console.log(formatResults(incrResults));
151
+ printSummary(incrResults, startTime);
152
+ return;
153
+ }
154
+
155
+ // Process all files
156
+ const results: GenerationResult[] = [];
157
+ for (const file of files) {
158
+ const result = await processFile(file, cliOptions, config, spinner);
159
+ if (result) results.push(result);
160
+ }
161
+
162
+ if (results.length === 0) {
163
+ spinner.warn('No tests were generated.');
164
+ return;
165
+ }
166
+
167
+ spinner.succeed(`Generated ${results.length} test suite(s)`);
168
+ console.log(formatResults(results));
169
+
170
+ if (cliOptions.coverage) {
171
+ for (const result of results) {
172
+ try {
173
+ const analysis = analyzeSource(result.sourceFile, detectLanguage(result.sourceFile));
174
+ const prediction = predictCoverage(analysis, result.testSuite);
175
+ console.log(formatCoverageReport(prediction));
176
+ } catch {
177
+ // Skip coverage for individual files that fail
178
+ }
179
+ }
180
+ }
181
+
182
+ if (cliOptions.mutation) {
183
+ spinner.start('Running mutation testing...');
184
+ for (const result of results) {
185
+ try {
186
+ const analysis = analyzeSource(result.sourceFile, detectLanguage(result.sourceFile));
187
+ const mutants = generateMutants(result.sourceFile, analysis);
188
+ const mutationResult = calculateMutationScore(
189
+ mutants.map((m) => ({ ...m, status: 'survived' as const })),
190
+ );
191
+ console.log(formatMutationReport(mutationResult));
192
+ } catch {
193
+ // Skip mutation for files that fail
194
+ }
195
+ }
196
+ }
197
+
198
+ printSummary(results, startTime);
199
+ } catch (err) {
200
+ spinner.fail('Generation failed');
201
+ console.error(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
202
+ if (cliOptions.verbose && err instanceof Error && err.stack) {
203
+ console.error(chalk.gray(err.stack));
204
+ }
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ async function processFile(
210
+ file: string,
211
+ cliOptions: CLIOptions,
212
+ config: TestGenConfig,
213
+ spinner: ReturnType<typeof ora>,
214
+ ): Promise<GenerationResult | null> {
215
+ try {
216
+ spinner.text = `Analyzing: ${path.basename(file)}`;
217
+
218
+ const language = cliOptions.language ?? detectLanguage(file);
219
+ const framework = cliOptions.framework ?? detectFramework(language, path.dirname(file));
220
+
221
+ const analysis = analyzeSource(file, language);
222
+ const result = await generateTestSuite(analysis, framework, config, cliOptions);
223
+
224
+ if (cliOptions.dryRun) {
225
+ console.log(chalk.gray(`\n [DRY RUN] Would generate: ${result.testFile}`));
226
+ console.log(chalk.gray(` ${result.testSuite.testCases.length} test cases | ~${result.testSuite.coverageEstimate}% coverage`));
227
+ } else {
228
+ console.log(chalk.green(` Generated: ${path.basename(result.testFile)}`) +
229
+ chalk.gray(` (${result.testSuite.testCases.length} tests, ~${result.testSuite.coverageEstimate}% coverage, ${result.usedAI ? 'AI' : 'AST'})`));
230
+ }
231
+
232
+ return result;
233
+ } catch (err) {
234
+ logger.warn(`Skipped ${path.basename(file)}: ${err instanceof Error ? err.message : String(err)}`);
235
+ return null;
236
+ }
237
+ }
238
+
239
+ async function runWatch(source: string, options: Partial<CLIOptions>): Promise<void> {
240
+ const sourcePath = path.resolve(source);
241
+ const cliOptions = normalizeOptions(options);
242
+
243
+ if (cliOptions.verbose) setLogLevel('debug');
244
+
245
+ const config = resolveConfig(
246
+ { watch: { enabled: true } } as Partial<TestGenConfig>,
247
+ sourcePath,
248
+ );
249
+
250
+ console.log(chalk.bold.blue('\n AI-TestGen Watch Mode'));
251
+ console.log(chalk.gray(` Watching: ${sourcePath}\n`));
252
+
253
+ const watcher = new FileWatcher(config, async (events) => {
254
+ console.log(chalk.cyan(`\n ${events.length} file(s) changed`));
255
+
256
+ for (const event of events) {
257
+ if (event.eventType === 'unlink') continue;
258
+
259
+ try {
260
+ const language = detectLanguage(event.filePath);
261
+ const framework = detectFramework(language, path.dirname(event.filePath));
262
+ const analysis = analyzeSource(event.filePath, language);
263
+ const result = await generateTestSuite(analysis, framework, config, cliOptions);
264
+ console.log(chalk.green(` Updated: ${path.basename(result.testFile)}`) +
265
+ chalk.gray(` (${result.testSuite.testCases.length} tests)`));
266
+ } catch (err) {
267
+ console.error(chalk.red(` Error processing ${event.filePath}: ${err instanceof Error ? err.message : String(err)}`));
268
+ }
269
+ }
270
+ });
271
+
272
+ watcher.start([sourcePath]);
273
+
274
+ process.on('SIGINT', () => {
275
+ console.log(chalk.yellow('\n Stopping watch mode...'));
276
+ watcher.stop();
277
+ process.exit(0);
278
+ });
279
+ }
280
+
281
+ async function runMutation(source: string, options: Partial<CLIOptions>): Promise<void> {
282
+ const sourcePath = path.resolve(source);
283
+ const cliOptions = normalizeOptions(options);
284
+
285
+ if (cliOptions.verbose) setLogLevel('debug');
286
+
287
+ console.log(chalk.bold.blue('\n AI-TestGen Mutation Testing'));
288
+ console.log(chalk.gray(` Source: ${sourcePath}\n`));
289
+
290
+ const spinner = ora('Generating mutants...').start();
291
+
292
+ try {
293
+ const language = detectLanguage(sourcePath);
294
+ const analysis = analyzeSource(sourcePath, language);
295
+
296
+ const mutants = generateMutants(sourcePath, analysis);
297
+ spinner.text = `Generated ${mutants.length} mutants. Analyzing...`;
298
+
299
+ const result = calculateMutationScore(
300
+ mutants.map((m) => ({ ...m, status: 'survived' as const })),
301
+ );
302
+
303
+ spinner.succeed(`Generated ${mutants.length} mutants`);
304
+ console.log(formatMutationReport(result));
305
+ console.log(chalk.gray('\n Note: Run generated tests against each mutant for accurate kill/survive scores.'));
306
+ } catch (err) {
307
+ spinner.fail('Mutation testing failed');
308
+ console.error(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
309
+ process.exit(1);
310
+ }
311
+ }
312
+
313
+ async function runAnalyze(source: string, options: Partial<CLIOptions>): Promise<void> {
314
+ const sourcePath = path.resolve(source);
315
+ const cliOptions = normalizeOptions(options);
316
+
317
+ if (cliOptions.verbose) setLogLevel('debug');
318
+
319
+ console.log(chalk.bold.blue('\n AI-TestGen Analysis'));
320
+ console.log(chalk.gray(` Source: ${sourcePath}\n`));
321
+
322
+ try {
323
+ const language = detectLanguage(sourcePath);
324
+ const analysis = analyzeSource(sourcePath, language);
325
+
326
+ console.log(chalk.bold(' Language: ') + analysis.language);
327
+ console.log(chalk.bold(' Lines of code: ') + analysis.linesOfCode);
328
+ console.log(chalk.bold(' Cyclomatic complexity: ') + analysis.cyclomaticComplexity);
329
+ console.log('');
330
+
331
+ if (analysis.functions.length > 0) {
332
+ console.log(chalk.bold(' Functions:'));
333
+ for (const fn of analysis.functions) {
334
+ const exported = fn.isExported ? chalk.green(' [exported]') : '';
335
+ const isAsync = fn.isAsync ? chalk.yellow(' [async]') : '';
336
+ console.log(` - ${fn.name}(${fn.params.map((p) => p.name).join(', ')})${fn.returnType ? ': ' + fn.returnType : ''}${exported}${isAsync}`);
337
+ console.log(chalk.gray(` Complexity: ${fn.complexity} | Lines: ${fn.startLine}-${fn.endLine}`));
338
+ }
339
+ console.log('');
340
+ }
341
+
342
+ if (analysis.classes.length > 0) {
343
+ console.log(chalk.bold(' Classes:'));
344
+ for (const cls of analysis.classes) {
345
+ console.log(` - ${cls.name}${cls.extends ? ' extends ' + cls.extends : ''}`);
346
+ console.log(chalk.gray(` Methods: ${cls.methods.map((m) => m.name).join(', ') || 'none'}`));
347
+ console.log(chalk.gray(` Properties: ${cls.properties.map((p) => p.name).join(', ') || 'none'}`));
348
+ }
349
+ console.log('');
350
+ }
351
+
352
+ if (analysis.interfaces.length > 0) {
353
+ console.log(chalk.bold(' Interfaces:'));
354
+ for (const iface of analysis.interfaces) {
355
+ console.log(` - ${iface.name}`);
356
+ }
357
+ console.log('');
358
+ }
359
+
360
+ if (analysis.imports.length > 0) {
361
+ console.log(chalk.bold(' Dependencies:'));
362
+ for (const imp of analysis.imports) {
363
+ console.log(` - ${imp.modulePath}: ${imp.namedImports.join(', ') || (imp.defaultImport ?? 'default')}`);
364
+ }
365
+ console.log('');
366
+ }
367
+
368
+ console.log(chalk.bold(' Exports: ') + analysis.exports.length);
369
+ console.log(chalk.bold(' Functions: ') + analysis.functions.length);
370
+ console.log(chalk.bold(' Classes: ') + analysis.classes.length);
371
+ console.log(chalk.bold(' Interfaces: ') + analysis.interfaces.length);
372
+ } catch (err) {
373
+ console.error(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
374
+ process.exit(1);
375
+ }
376
+ }
377
+
378
+ function normalizeOptions(options: Partial<CLIOptions>): CLIOptions {
379
+ return {
380
+ source: options.source ?? '',
381
+ output: options.output,
382
+ config: options.config,
383
+ language: options.language as SupportedLanguage | undefined,
384
+ framework: options.framework as TestFramework | undefined,
385
+ watch: options.watch ?? false,
386
+ incremental: options.incremental ?? false,
387
+ verbose: options.verbose ?? false,
388
+ dryRun: options.dryRun ?? false,
389
+ noAI: options.noAI ?? false,
390
+ types: options.types ?? ['unit', 'edge-case'] as TestType[],
391
+ coverage: options.coverage ?? false,
392
+ mutation: options.mutation ?? false,
393
+ };
394
+ }
395
+
396
+ function printSummary(
397
+ results: GenerationResult[],
398
+ startTime: number,
399
+ ): void {
400
+ const totalDuration = Date.now() - startTime;
401
+ const totalTests = results.reduce((sum, r) => sum + r.testSuite.testCases.length, 0);
402
+ const avgCoverage = results.length > 0
403
+ ? Math.round(results.reduce((sum, r) => sum + r.testSuite.coverageEstimate, 0) / results.length)
404
+ : 0;
405
+
406
+ console.log('');
407
+ console.log(chalk.bold(' Summary'));
408
+ console.log(chalk.gray(' ----------------------------------------'));
409
+ console.log(` ${chalk.green('Files analyzed:')} ${results.length}`);
410
+ console.log(` ${chalk.green('Tests generated:')} ${totalTests}`);
411
+ console.log(` ${chalk.green('Avg coverage:')} ~${avgCoverage}%`);
412
+ console.log(` ${chalk.green('Total time:')} ${totalDuration}ms`);
413
+ console.log('');
414
+ }
415
+
416
+ program.parse();
@@ -0,0 +1,81 @@
1
+ import { TestGenConfig, SupportedLanguage, TestFramework } from '../types.js';
2
+
3
+ export const DEFAULT_CONFIG: TestGenConfig = {
4
+ version: '1.0.0',
5
+ language: 'auto',
6
+ framework: 'auto',
7
+ outputDir: '__tests__',
8
+ testFilePattern: '{name}.test.{ext}',
9
+ coverage: {
10
+ target: 90,
11
+ strict: false,
12
+ },
13
+ ai: {
14
+ enabled: false,
15
+ provider: 'none',
16
+ model: 'gpt-4o',
17
+ apiKeyEnv: 'OPENAI_API_KEY',
18
+ maxTokens: 4096,
19
+ temperature: 0.2,
20
+ privacyMode: true,
21
+ },
22
+ generation: {
23
+ unitTests: true,
24
+ integrationTests: true,
25
+ edgeCaseTests: true,
26
+ mockGeneration: true,
27
+ mutationTesting: false,
28
+ maxTestsPerFunction: 10,
29
+ includeComments: true,
30
+ },
31
+ incremental: {
32
+ enabled: false,
33
+ gitBased: true,
34
+ cacheDir: '.ai-testgen-cache',
35
+ },
36
+ watch: {
37
+ enabled: false,
38
+ ignorePatterns: ['node_modules', 'dist', '.git', 'coverage'],
39
+ debounceMs: 300,
40
+ },
41
+ exclude: [
42
+ 'node_modules/**',
43
+ 'dist/**',
44
+ 'coverage/**',
45
+ '**/*.d.ts',
46
+ '**/*.min.js',
47
+ '**/vendor/**',
48
+ ],
49
+ include: [
50
+ 'src/**/*.{ts,tsx,js,jsx}',
51
+ 'lib/**/*.{ts,tsx,js,jsx}',
52
+ ],
53
+ };
54
+
55
+ export const LANGUAGE_EXTENSIONS: Record<string, SupportedLanguage> = {
56
+ '.ts': 'typescript',
57
+ '.tsx': 'typescript',
58
+ '.js': 'javascript',
59
+ '.jsx': 'javascript',
60
+ '.mjs': 'javascript',
61
+ '.cjs': 'javascript',
62
+ '.py': 'python',
63
+ '.go': 'go',
64
+ '.rs': 'rust',
65
+ };
66
+
67
+ export const LANGUAGE_FRAMEWORKS: Record<SupportedLanguage, TestFramework[]> = {
68
+ typescript: ['jest', 'vitest'],
69
+ javascript: ['jest', 'vitest'],
70
+ python: ['pytest'],
71
+ go: ['go-test'],
72
+ rust: [],
73
+ };
74
+
75
+ export const TEST_FILE_EXTENSIONS: Record<SupportedLanguage, string> = {
76
+ typescript: 'ts',
77
+ javascript: 'js',
78
+ python: 'py',
79
+ go: 'go',
80
+ rust: 'rs',
81
+ };
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_CONFIG, LANGUAGE_EXTENSIONS, LANGUAGE_FRAMEWORKS, TEST_FILE_EXTENSIONS } from './defaults.js';
2
+ export { findConfig, loadConfig, resolveConfig, createSampleConfig } from './loader.js';
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as yaml from 'js-yaml';
4
+ import { TestGenConfig } from '../types.js';
5
+ import { DEFAULT_CONFIG } from './defaults.js';
6
+
7
+ const CONFIG_FILENAMES = ['.aitestgen.yml', '.aitestgen.yaml', '.aitestgen.json'];
8
+
9
+ export function findConfig(startDir: string): string | null {
10
+ let current = path.resolve(startDir);
11
+ const root = path.parse(current).root;
12
+
13
+ while (current !== root) {
14
+ for (const filename of CONFIG_FILENAMES) {
15
+ const configPath = path.join(current, filename);
16
+ if (fs.existsSync(configPath)) {
17
+ return configPath;
18
+ }
19
+ }
20
+ current = path.dirname(current);
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function loadConfig(configPath: string): TestGenConfig {
26
+ const ext = path.extname(configPath);
27
+ const raw = fs.readFileSync(configPath, 'utf-8');
28
+
29
+ let parsed: Record<string, unknown>;
30
+ if (ext === '.json') {
31
+ parsed = JSON.parse(raw) as Record<string, unknown>;
32
+ } else {
33
+ parsed = yaml.load(raw) as Record<string, unknown>;
34
+ }
35
+
36
+ return deepMerge(
37
+ DEFAULT_CONFIG as unknown as Record<string, unknown>,
38
+ parsed,
39
+ ) as unknown as TestGenConfig;
40
+ }
41
+
42
+ export function resolveConfig(
43
+ cliOverrides: Partial<TestGenConfig>,
44
+ projectDir: string,
45
+ ): TestGenConfig {
46
+ const configPath = cliOverrides['config' as keyof typeof cliOverrides] as string | undefined;
47
+ const foundPath = configPath ?? findConfig(projectDir);
48
+ const base = foundPath ? loadConfig(foundPath) : { ...DEFAULT_CONFIG };
49
+
50
+ return deepMerge(
51
+ base as unknown as Record<string, unknown>,
52
+ cliOverrides as unknown as Record<string, unknown>,
53
+ ) as unknown as TestGenConfig;
54
+ }
55
+
56
+ function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
57
+ const result = { ...target };
58
+
59
+ for (const key of Object.keys(source)) {
60
+ const sourceVal = source[key];
61
+ const targetVal = target[key];
62
+
63
+ if (
64
+ isPlainObject(sourceVal) &&
65
+ isPlainObject(targetVal)
66
+ ) {
67
+ result[key] = deepMerge(
68
+ targetVal as Record<string, unknown>,
69
+ sourceVal as Record<string, unknown>,
70
+ );
71
+ } else if (sourceVal !== undefined) {
72
+ result[key] = sourceVal;
73
+ }
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
80
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
81
+ }
82
+
83
+ export function createSampleConfig(outputPath: string): void {
84
+ const sample: Record<string, unknown> = {
85
+ version: '1.0.0',
86
+ language: 'auto',
87
+ framework: 'auto',
88
+ outputDir: '__tests__',
89
+ coverage: { target: 90, strict: false },
90
+ ai: {
91
+ enabled: true,
92
+ provider: 'openai',
93
+ model: 'gpt-4o',
94
+ apiKeyEnv: 'OPENAI_API_KEY',
95
+ maxTokens: 4096,
96
+ temperature: 0.2,
97
+ privacyMode: true,
98
+ },
99
+ generation: {
100
+ unitTests: true,
101
+ integrationTests: true,
102
+ edgeCaseTests: true,
103
+ mockGeneration: true,
104
+ mutationTesting: false,
105
+ maxTestsPerFunction: 10,
106
+ includeComments: true,
107
+ },
108
+ exclude: ['node_modules/**', 'dist/**', 'coverage/**'],
109
+ include: ['src/**/*.{ts,tsx,js,jsx}'],
110
+ };
111
+
112
+ const content = yaml.dump(sample, { indent: 2, lineWidth: 100 });
113
+ fs.writeFileSync(outputPath, content, 'utf-8');
114
+ }