autotest-ai 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/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # autotest-ai
2
+
3
+ **AI-powered test generator. Generate unit tests from your code using Claude or OpenAI.**
4
+
5
+ Stop writing tests manually. Let AI analyze your code and generate comprehensive unit tests.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/autotest-ai.svg)](https://www.npmjs.com/package/autotest-ai)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ - **AI-Powered**: Uses Claude or OpenAI to understand your code and generate meaningful tests
16
+ - **Multiple Frameworks**: Supports Jest, Vitest, Node.js test runner, and Mocha
17
+ - **Edge Cases**: Automatically generates tests for edge cases (null, undefined, empty values)
18
+ - **TypeScript Support**: Generates TypeScript or JavaScript tests
19
+ - **CLI & API**: Use from command line or programmatically
20
+ - **Batch Processing**: Generate tests for entire directories
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install -g autotest-ai
28
+ ```
29
+
30
+ Or use with npx:
31
+ ```bash
32
+ npx autotest-ai src/utils.ts
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Set your API key
40
+
41
+ ```bash
42
+ # For Claude (recommended)
43
+ export ANTHROPIC_API_KEY=your-api-key
44
+
45
+ # For OpenAI
46
+ export OPENAI_API_KEY=your-api-key
47
+ ```
48
+
49
+ ### 2. Generate tests
50
+
51
+ ```bash
52
+ # Generate tests for a file
53
+ autotest src/utils.ts
54
+
55
+ # Generate tests for all files in a directory
56
+ autotest src/
57
+ ```
58
+
59
+ ### 3. Run your tests
60
+
61
+ ```bash
62
+ npm test
63
+ ```
64
+
65
+ ---
66
+
67
+ ## CLI Usage
68
+
69
+ ```bash
70
+ autotest <file|directory> [options]
71
+ ```
72
+
73
+ ### Options
74
+
75
+ | Option | Description |
76
+ |--------|-------------|
77
+ | `-p, --provider <claude\|openai>` | AI provider (default: claude) |
78
+ | `-m, --model <model>` | Model to use |
79
+ | `-f, --framework <framework>` | Test framework: jest, vitest, node, mocha |
80
+ | `-o, --output <dir>` | Output directory (default: __tests__) |
81
+ | `--no-edge-cases` | Skip edge case tests |
82
+ | `--js` | Generate JavaScript instead of TypeScript |
83
+ | `--dry-run` | Print tests without writing files |
84
+ | `-h, --help` | Show help |
85
+
86
+ ### Examples
87
+
88
+ ```bash
89
+ # Generate Jest tests (default)
90
+ autotest src/utils.ts
91
+
92
+ # Generate Vitest tests
93
+ autotest src/utils.ts --framework vitest
94
+
95
+ # Use OpenAI instead of Claude
96
+ autotest src/utils.ts --provider openai
97
+
98
+ # Preview generated tests without writing
99
+ autotest src/utils.ts --dry-run
100
+
101
+ # Generate tests for entire src directory
102
+ autotest src/
103
+
104
+ # Generate JavaScript tests
105
+ autotest src/utils.js --js
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Programmatic API
111
+
112
+ ```typescript
113
+ import { generateTests, generateAndWriteTests } from 'autotest-ai';
114
+
115
+ // Generate tests (returns test code)
116
+ const result = await generateTests('src/utils.ts', {
117
+ provider: 'claude',
118
+ framework: 'jest',
119
+ edgeCases: true,
120
+ });
121
+
122
+ console.log(result.testCode);
123
+ console.log(`Functions tested: ${result.functions.join(', ')}`);
124
+
125
+ // Generate and write tests to file
126
+ const written = await generateAndWriteTests('src/utils.ts', {
127
+ framework: 'vitest',
128
+ outputDir: '__tests__',
129
+ });
130
+
131
+ console.log(`Test file created: ${written.testFile}`);
132
+ ```
133
+
134
+ ### API Reference
135
+
136
+ #### `generateTests(filePath, config?)`
137
+
138
+ Generates tests for a file and returns the result.
139
+
140
+ ```typescript
141
+ interface GenerateResult {
142
+ sourceFile: string; // Original source file path
143
+ testFile: string; // Generated test file path
144
+ testCode: string; // Generated test code
145
+ functions: string[]; // Functions that were tested
146
+ tokensUsed?: number; // API tokens used
147
+ }
148
+ ```
149
+
150
+ #### `generateAndWriteTests(filePath, config?)`
151
+
152
+ Generates tests and writes them to a file.
153
+
154
+ #### `generateTestsForDirectory(dirPath, config?)`
155
+
156
+ Generates tests for all source files in a directory.
157
+
158
+ ### Config Options
159
+
160
+ ```typescript
161
+ interface AutoTestConfig {
162
+ provider?: 'claude' | 'openai'; // AI provider
163
+ apiKey?: string; // API key (or use env var)
164
+ model?: string; // Model to use
165
+ framework?: 'jest' | 'vitest' | 'node' | 'mocha';
166
+ outputDir?: string; // Output directory
167
+ edgeCases?: boolean; // Include edge cases
168
+ typescript?: boolean; // Generate TypeScript
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Example Output
175
+
176
+ ### Input: `src/math.ts`
177
+ ```typescript
178
+ export function add(a: number, b: number): number {
179
+ return a + b;
180
+ }
181
+
182
+ export function divide(a: number, b: number): number {
183
+ if (b === 0) throw new Error('Division by zero');
184
+ return a / b;
185
+ }
186
+ ```
187
+
188
+ ### Generated: `__tests__/math.test.ts`
189
+ ```typescript
190
+ import { describe, it, expect } from '@jest/globals';
191
+ import { add, divide } from '../src/math';
192
+
193
+ describe('add', () => {
194
+ it('should add two positive numbers', () => {
195
+ expect(add(2, 3)).toBe(5);
196
+ });
197
+
198
+ it('should add negative numbers', () => {
199
+ expect(add(-1, -1)).toBe(-2);
200
+ });
201
+
202
+ it('should handle zero', () => {
203
+ expect(add(0, 5)).toBe(5);
204
+ expect(add(5, 0)).toBe(5);
205
+ });
206
+ });
207
+
208
+ describe('divide', () => {
209
+ it('should divide two numbers', () => {
210
+ expect(divide(10, 2)).toBe(5);
211
+ });
212
+
213
+ it('should handle decimal results', () => {
214
+ expect(divide(5, 2)).toBe(2.5);
215
+ });
216
+
217
+ it('should throw error for division by zero', () => {
218
+ expect(() => divide(10, 0)).toThrow('Division by zero');
219
+ });
220
+ });
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Supported Test Frameworks
226
+
227
+ | Framework | Command |
228
+ |-----------|---------|
229
+ | **Jest** | `autotest file.ts --framework jest` |
230
+ | **Vitest** | `autotest file.ts --framework vitest` |
231
+ | **Node.js** | `autotest file.ts --framework node` |
232
+ | **Mocha** | `autotest file.ts --framework mocha` |
233
+
234
+ ---
235
+
236
+ ## Environment Variables
237
+
238
+ | Variable | Description |
239
+ |----------|-------------|
240
+ | `ANTHROPIC_API_KEY` | API key for Claude |
241
+ | `OPENAI_API_KEY` | API key for OpenAI |
242
+
243
+ ---
244
+
245
+ ## Tips
246
+
247
+ 1. **Better tests with better code**: Well-documented functions with clear types produce better tests
248
+ 2. **Review generated tests**: AI-generated tests are a starting point, review and adjust as needed
249
+ 3. **Use --dry-run first**: Preview tests before writing to files
250
+ 4. **Combine with TDD**: Generate initial tests, then refine as you develop
251
+
252
+ ---
253
+
254
+ ## Support
255
+
256
+ If autotest-ai saved you time:
257
+ - Star the [GitHub repo](https://github.com/willzhangfly/autotest-ai)
258
+ - [Buy me a coffee](https://buymeacoffee.com/willzhangfly)
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,201 @@
1
+ // src/index.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var DEFAULT_CONFIG = {
5
+ provider: "claude",
6
+ apiKey: "",
7
+ model: "claude-sonnet-4-20250514",
8
+ framework: "jest",
9
+ outputDir: "__tests__",
10
+ edgeCases: true,
11
+ typescript: true
12
+ };
13
+ async function callClaude(prompt, apiKey, model) {
14
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ "x-api-key": apiKey,
19
+ "anthropic-version": "2023-06-01"
20
+ },
21
+ body: JSON.stringify({
22
+ model,
23
+ max_tokens: 4096,
24
+ messages: [{ role: "user", content: prompt }]
25
+ })
26
+ });
27
+ if (!response.ok) {
28
+ const error = await response.text();
29
+ throw new Error(`Claude API error: ${response.status} - ${error}`);
30
+ }
31
+ const data = await response.json();
32
+ return {
33
+ content: data.content[0].text,
34
+ tokensUsed: data.usage.input_tokens + data.usage.output_tokens
35
+ };
36
+ }
37
+ async function callOpenAI(prompt, apiKey, model) {
38
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ "Authorization": `Bearer ${apiKey}`
43
+ },
44
+ body: JSON.stringify({
45
+ model,
46
+ max_tokens: 4096,
47
+ messages: [{ role: "user", content: prompt }]
48
+ })
49
+ });
50
+ if (!response.ok) {
51
+ const error = await response.text();
52
+ throw new Error(`OpenAI API error: ${response.status} - ${error}`);
53
+ }
54
+ const data = await response.json();
55
+ return {
56
+ content: data.choices[0].message.content,
57
+ tokensUsed: data.usage.total_tokens
58
+ };
59
+ }
60
+ function buildPrompt(code, framework, edgeCases, typescript) {
61
+ const frameworkImports = {
62
+ jest: `import { describe, it, expect } from '@jest/globals';`,
63
+ vitest: `import { describe, it, expect } from 'vitest';`,
64
+ node: `import { describe, it } from 'node:test';
65
+ import assert from 'node:assert';`,
66
+ mocha: `import { describe, it } from 'mocha';
67
+ import { expect } from 'chai';`
68
+ };
69
+ const assertStyle = framework === "node" ? "assert" : "expect";
70
+ return `You are an expert test writer. Generate comprehensive unit tests for the following code.
71
+
72
+ ## Requirements:
73
+ - Use ${framework} test framework
74
+ - ${typescript ? "Use TypeScript" : "Use JavaScript"}
75
+ - Import the functions being tested from the source file
76
+ - ${edgeCases ? "Include edge cases (null, undefined, empty, boundary values)" : "Focus on happy path tests"}
77
+ - Each test should have a clear, descriptive name
78
+ - Use ${assertStyle} style assertions
79
+ - Group related tests in describe blocks
80
+
81
+ ## Framework imports:
82
+ ${frameworkImports[framework]}
83
+
84
+ ## Source Code:
85
+ \`\`\`${typescript ? "typescript" : "javascript"}
86
+ ${code}
87
+ \`\`\`
88
+
89
+ ## Output:
90
+ Generate ONLY the test code, no explanations. Start with imports and end with the last test.
91
+ The test file should be complete and runnable.
92
+ Import functions from '../src/filename' (adjust path as needed).`;
93
+ }
94
+ function extractFunctionNames(code) {
95
+ const functions = [];
96
+ const funcDecl = /function\s+(\w+)\s*\(/g;
97
+ let match;
98
+ while ((match = funcDecl.exec(code)) !== null) {
99
+ functions.push(match[1]);
100
+ }
101
+ const arrowFunc = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/g;
102
+ while ((match = arrowFunc.exec(code)) !== null) {
103
+ functions.push(match[1]);
104
+ }
105
+ const arrowSingle = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\w+\s*=>/g;
106
+ while ((match = arrowSingle.exec(code)) !== null) {
107
+ functions.push(match[1]);
108
+ }
109
+ const exportFunc = /export\s+(?:async\s+)?function\s+(\w+)/g;
110
+ while ((match = exportFunc.exec(code)) !== null) {
111
+ if (!functions.includes(match[1])) {
112
+ functions.push(match[1]);
113
+ }
114
+ }
115
+ const methods = /^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*{/gm;
116
+ while ((match = methods.exec(code)) !== null) {
117
+ if (!["constructor", "if", "for", "while", "switch"].includes(match[1])) {
118
+ if (!functions.includes(match[1])) {
119
+ functions.push(match[1]);
120
+ }
121
+ }
122
+ }
123
+ return [...new Set(functions)];
124
+ }
125
+ async function generateTests(filePath, config = {}) {
126
+ const cfg = { ...DEFAULT_CONFIG, ...config };
127
+ const apiKey = cfg.apiKey || (cfg.provider === "claude" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY) || "";
128
+ if (!apiKey) {
129
+ throw new Error(
130
+ `API key required. Set ${cfg.provider === "claude" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} environment variable or pass apiKey in config.`
131
+ );
132
+ }
133
+ const absolutePath = path.resolve(filePath);
134
+ if (!fs.existsSync(absolutePath)) {
135
+ throw new Error(`File not found: ${absolutePath}`);
136
+ }
137
+ const code = fs.readFileSync(absolutePath, "utf-8");
138
+ const functions = extractFunctionNames(code);
139
+ if (functions.length === 0) {
140
+ throw new Error(`No functions found in ${filePath}`);
141
+ }
142
+ const prompt = buildPrompt(code, cfg.framework, cfg.edgeCases, cfg.typescript);
143
+ const model = cfg.model || (cfg.provider === "claude" ? "claude-sonnet-4-20250514" : "gpt-4o");
144
+ const callAI = cfg.provider === "claude" ? callClaude : callOpenAI;
145
+ const result = await callAI(prompt, apiKey, model);
146
+ let testCode = result.content;
147
+ const codeBlockMatch = testCode.match(/```(?:typescript|javascript|ts|js)?\n([\s\S]*?)```/);
148
+ if (codeBlockMatch) {
149
+ testCode = codeBlockMatch[1];
150
+ }
151
+ const ext = cfg.typescript ? ".test.ts" : ".test.js";
152
+ const baseName = path.basename(filePath, path.extname(filePath));
153
+ const testFileName = baseName + ext;
154
+ const testFilePath = path.join(path.dirname(absolutePath), cfg.outputDir, testFileName);
155
+ return {
156
+ sourceFile: absolutePath,
157
+ testFile: testFilePath,
158
+ testCode: testCode.trim(),
159
+ functions,
160
+ tokensUsed: result.tokensUsed
161
+ };
162
+ }
163
+ async function generateAndWriteTests(filePath, config = {}) {
164
+ const result = await generateTests(filePath, config);
165
+ const outputDir = path.dirname(result.testFile);
166
+ if (!fs.existsSync(outputDir)) {
167
+ fs.mkdirSync(outputDir, { recursive: true });
168
+ }
169
+ fs.writeFileSync(result.testFile, result.testCode);
170
+ return result;
171
+ }
172
+ async function generateTestsForDirectory(dirPath, config = {}) {
173
+ const results = [];
174
+ const absolutePath = path.resolve(dirPath);
175
+ const files = fs.readdirSync(absolutePath);
176
+ const sourceFiles = files.filter(
177
+ (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".test.ts") && !f.endsWith(".test.js") && !f.endsWith(".spec.ts") && !f.endsWith(".spec.js") && !f.endsWith(".d.ts")
178
+ );
179
+ for (const file of sourceFiles) {
180
+ const filePath = path.join(absolutePath, file);
181
+ try {
182
+ const result = await generateAndWriteTests(filePath, config);
183
+ results.push(result);
184
+ } catch (error) {
185
+ console.error(`Error generating tests for ${file}:`, error);
186
+ }
187
+ }
188
+ return results;
189
+ }
190
+ var index_default = {
191
+ generateTests,
192
+ generateAndWriteTests,
193
+ generateTestsForDirectory
194
+ };
195
+
196
+ export {
197
+ generateTests,
198
+ generateAndWriteTests,
199
+ generateTestsForDirectory,
200
+ index_default
201
+ };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node