xibecode 0.3.8 → 0.4.4
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 +282 -26
- package/dist/commands/chat.d.ts +1 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +106 -8
- package/dist/commands/chat.js.map +1 -1
- package/dist/core/modes.d.ts +1 -1
- package/dist/core/modes.d.ts.map +1 -1
- package/dist/core/modes.js +7 -8
- package/dist/core/modes.js.map +1 -1
- package/dist/core/session-bridge.d.ts +105 -0
- package/dist/core/session-bridge.d.ts.map +1 -0
- package/dist/core/session-bridge.js +243 -0
- package/dist/core/session-bridge.js.map +1 -0
- package/dist/core/tools.d.ts +4 -0
- package/dist/core/tools.d.ts.map +1 -1
- package/dist/core/tools.js +171 -0
- package/dist/core/tools.js.map +1 -1
- package/dist/index.js +42 -35
- package/dist/index.js.map +1 -1
- package/dist/tools/test-generator.d.ts +157 -0
- package/dist/tools/test-generator.d.ts.map +1 -0
- package/dist/tools/test-generator.js +893 -0
- package/dist/tools/test-generator.js.map +1 -0
- package/dist/webui/server.d.ts +96 -0
- package/dist/webui/server.d.ts.map +1 -0
- package/dist/webui/server.js +2304 -0
- package/dist/webui/server.js.map +1 -0
- package/package.json +16 -6
- package/webui-dist/assets/index-BXqucsdn.js +353 -0
- package/webui-dist/assets/index-BXqucsdn.js.map +1 -0
- package/webui-dist/assets/index-D1NjcKN3.css +32 -0
- package/webui-dist/assets/xterm-BjznWQZK.js +10 -0
- package/webui-dist/assets/xterm-BjznWQZK.js.map +1 -0
- package/webui-dist/assets/xterm-addon-fit-CPOtr3CW.js +2 -0
- package/webui-dist/assets/xterm-addon-fit-CPOtr3CW.js.map +1 -0
- package/webui-dist/assets/xterm-addon-web-links-z5RYd3SS.js +2 -0
- package/webui-dist/assets/xterm-addon-web-links-z5RYd3SS.js.map +1 -0
- package/webui-dist/index.html +15 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Test Generator
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates test cases for functions, classes, and modules.
|
|
5
|
+
* Supports multiple testing frameworks: Vitest, Jest, Mocha, pytest, Go test.
|
|
6
|
+
*
|
|
7
|
+
* @module tools/test-generator
|
|
8
|
+
* @since 0.4.0
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { TestRunnerDetector } from '../utils/testRunner.js';
|
|
13
|
+
/**
|
|
14
|
+
* AI-powered test generator that analyzes code and generates comprehensive test suites
|
|
15
|
+
*/
|
|
16
|
+
export class TestGenerator {
|
|
17
|
+
workingDir;
|
|
18
|
+
testDetector;
|
|
19
|
+
constructor(workingDir = process.cwd()) {
|
|
20
|
+
this.workingDir = workingDir;
|
|
21
|
+
this.testDetector = new TestRunnerDetector(workingDir);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Analyze a source file to extract testable components
|
|
25
|
+
*/
|
|
26
|
+
async analyzeFile(filePath) {
|
|
27
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
28
|
+
? filePath
|
|
29
|
+
: path.join(this.workingDir, filePath);
|
|
30
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
31
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
32
|
+
let language;
|
|
33
|
+
switch (ext) {
|
|
34
|
+
case '.ts':
|
|
35
|
+
case '.tsx':
|
|
36
|
+
language = 'typescript';
|
|
37
|
+
break;
|
|
38
|
+
case '.js':
|
|
39
|
+
case '.jsx':
|
|
40
|
+
case '.mjs':
|
|
41
|
+
language = 'javascript';
|
|
42
|
+
break;
|
|
43
|
+
case '.py':
|
|
44
|
+
language = 'python';
|
|
45
|
+
break;
|
|
46
|
+
case '.go':
|
|
47
|
+
language = 'go';
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
language = 'javascript';
|
|
51
|
+
}
|
|
52
|
+
const analysis = {
|
|
53
|
+
filePath: absolutePath,
|
|
54
|
+
language,
|
|
55
|
+
functions: [],
|
|
56
|
+
classes: [],
|
|
57
|
+
exports: [],
|
|
58
|
+
imports: [],
|
|
59
|
+
};
|
|
60
|
+
if (language === 'typescript' || language === 'javascript') {
|
|
61
|
+
this.analyzeJSTS(content, analysis);
|
|
62
|
+
}
|
|
63
|
+
else if (language === 'python') {
|
|
64
|
+
this.analyzePython(content, analysis);
|
|
65
|
+
}
|
|
66
|
+
else if (language === 'go') {
|
|
67
|
+
this.analyzeGo(content, analysis);
|
|
68
|
+
}
|
|
69
|
+
return analysis;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Analyze JavaScript/TypeScript code
|
|
73
|
+
*/
|
|
74
|
+
analyzeJSTS(content, analysis) {
|
|
75
|
+
// Extract imports
|
|
76
|
+
const importRegex = /import\s+(?:(?:\{([^}]+)\})|(?:(\w+)))\s+from\s+['"]([^'"]+)['"]/g;
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
79
|
+
const namedImports = match[1] ? match[1].split(',').map(i => i.trim().split(' as ')[0]) : [];
|
|
80
|
+
const defaultImport = match[2] ? [match[2]] : [];
|
|
81
|
+
analysis.imports.push({
|
|
82
|
+
module: match[3],
|
|
83
|
+
imports: [...namedImports, ...defaultImport],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Extract exports
|
|
87
|
+
const exportRegex = /export\s+(?:default\s+)?(?:(?:const|let|var|function|class|async\s+function)\s+)?(\w+)/g;
|
|
88
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
89
|
+
if (match[1] && !analysis.exports.includes(match[1])) {
|
|
90
|
+
analysis.exports.push(match[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Extract functions
|
|
94
|
+
const funcPatterns = [
|
|
95
|
+
// Regular functions
|
|
96
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
|
|
97
|
+
// Arrow functions assigned to const/let
|
|
98
|
+
/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)(?:\s*:\s*([^=]+))?\s*=>/g,
|
|
99
|
+
// Method definitions in objects/classes
|
|
100
|
+
/(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
|
|
101
|
+
];
|
|
102
|
+
for (const pattern of funcPatterns) {
|
|
103
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
104
|
+
const funcName = match[1];
|
|
105
|
+
const paramsStr = match[2] || '';
|
|
106
|
+
const returnType = match[3]?.trim();
|
|
107
|
+
// Skip constructors and common non-function matches
|
|
108
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(funcName)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Check if already added
|
|
112
|
+
if (analysis.functions.some(f => f.name === funcName)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const params = this.parseJSTSParams(paramsStr);
|
|
116
|
+
const isAsync = content.substring(Math.max(0, match.index - 20), match.index).includes('async');
|
|
117
|
+
const isExported = analysis.exports.includes(funcName);
|
|
118
|
+
// Estimate complexity based on function body
|
|
119
|
+
const funcBodyMatch = content.substring(match.index).match(/\{([\s\S]*?)\}/);
|
|
120
|
+
const funcBody = funcBodyMatch ? funcBodyMatch[1] : '';
|
|
121
|
+
const complexity = this.estimateComplexity(funcBody);
|
|
122
|
+
// Detect dependencies
|
|
123
|
+
const dependencies = this.detectDependencies(funcBody, analysis.imports);
|
|
124
|
+
// Detect side effects
|
|
125
|
+
const sideEffects = this.detectSideEffects(funcBody);
|
|
126
|
+
analysis.functions.push({
|
|
127
|
+
name: funcName,
|
|
128
|
+
params,
|
|
129
|
+
returnType,
|
|
130
|
+
isAsync,
|
|
131
|
+
isExported,
|
|
132
|
+
complexity,
|
|
133
|
+
dependencies,
|
|
134
|
+
sideEffects,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Extract classes
|
|
139
|
+
const classRegex = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{/g;
|
|
140
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
141
|
+
const className = match[1];
|
|
142
|
+
const classBody = this.extractClassBody(content, match.index);
|
|
143
|
+
const classAnalysis = {
|
|
144
|
+
name: className,
|
|
145
|
+
methods: [],
|
|
146
|
+
properties: [],
|
|
147
|
+
isExported: analysis.exports.includes(className),
|
|
148
|
+
};
|
|
149
|
+
// Extract methods from class body
|
|
150
|
+
const methodRegex = /(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g;
|
|
151
|
+
let methodMatch;
|
|
152
|
+
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
153
|
+
const methodName = methodMatch[1];
|
|
154
|
+
if (methodName === 'constructor') {
|
|
155
|
+
classAnalysis.constructorMethod = {
|
|
156
|
+
name: 'constructor',
|
|
157
|
+
params: this.parseJSTSParams(methodMatch[2] || ''),
|
|
158
|
+
isAsync: false,
|
|
159
|
+
isExported: false,
|
|
160
|
+
complexity: 'low',
|
|
161
|
+
dependencies: [],
|
|
162
|
+
sideEffects: [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
classAnalysis.methods.push({
|
|
167
|
+
name: methodName,
|
|
168
|
+
params: this.parseJSTSParams(methodMatch[2] || ''),
|
|
169
|
+
returnType: methodMatch[3]?.trim(),
|
|
170
|
+
isAsync: classBody.substring(Math.max(0, methodMatch.index - 10), methodMatch.index).includes('async'),
|
|
171
|
+
isExported: false,
|
|
172
|
+
complexity: 'medium',
|
|
173
|
+
dependencies: [],
|
|
174
|
+
sideEffects: [],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Extract properties
|
|
179
|
+
const propRegex = /(?:(?:public|private|protected)\s+)?(?:readonly\s+)?(\w+)(?:\s*:\s*([^;=]+))?(?:\s*=)?/g;
|
|
180
|
+
let propMatch;
|
|
181
|
+
while ((propMatch = propRegex.exec(classBody)) !== null) {
|
|
182
|
+
const propName = propMatch[1];
|
|
183
|
+
if (!['constructor', 'static', 'async', 'get', 'set'].includes(propName)) {
|
|
184
|
+
const visibility = classBody.substring(Math.max(0, propMatch.index - 20), propMatch.index);
|
|
185
|
+
classAnalysis.properties.push({
|
|
186
|
+
name: propName,
|
|
187
|
+
type: propMatch[2]?.trim(),
|
|
188
|
+
visibility: visibility.includes('private') ? 'private'
|
|
189
|
+
: visibility.includes('protected') ? 'protected'
|
|
190
|
+
: 'public',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
analysis.classes.push(classAnalysis);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Analyze Python code
|
|
199
|
+
*/
|
|
200
|
+
analyzePython(content, analysis) {
|
|
201
|
+
// Extract imports
|
|
202
|
+
const importRegex = /(?:from\s+([\w.]+)\s+import\s+([^#\n]+)|import\s+([\w.]+))/g;
|
|
203
|
+
let match;
|
|
204
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
205
|
+
if (match[1]) {
|
|
206
|
+
analysis.imports.push({
|
|
207
|
+
module: match[1],
|
|
208
|
+
imports: match[2].split(',').map(i => i.trim().split(' as ')[0]),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (match[3]) {
|
|
212
|
+
analysis.imports.push({
|
|
213
|
+
module: match[3],
|
|
214
|
+
imports: [match[3].split('.').pop()],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Extract functions
|
|
219
|
+
const funcRegex = /(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:]+))?\s*:/g;
|
|
220
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
221
|
+
const funcName = match[1];
|
|
222
|
+
if (funcName.startsWith('_') && !funcName.startsWith('__')) {
|
|
223
|
+
continue; // Skip private functions
|
|
224
|
+
}
|
|
225
|
+
const paramsStr = match[2] || '';
|
|
226
|
+
const params = this.parsePythonParams(paramsStr);
|
|
227
|
+
const isAsync = content.substring(Math.max(0, match.index - 10), match.index).includes('async');
|
|
228
|
+
analysis.functions.push({
|
|
229
|
+
name: funcName,
|
|
230
|
+
params,
|
|
231
|
+
returnType: match[3]?.trim(),
|
|
232
|
+
isAsync,
|
|
233
|
+
isExported: !funcName.startsWith('_'),
|
|
234
|
+
complexity: 'medium',
|
|
235
|
+
dependencies: [],
|
|
236
|
+
sideEffects: [],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Extract classes
|
|
240
|
+
const classRegex = /class\s+(\w+)(?:\s*\(([^)]*)\))?\s*:/g;
|
|
241
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
242
|
+
const className = match[1];
|
|
243
|
+
analysis.classes.push({
|
|
244
|
+
name: className,
|
|
245
|
+
methods: [],
|
|
246
|
+
properties: [],
|
|
247
|
+
isExported: !className.startsWith('_'),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Analyze Go code
|
|
253
|
+
*/
|
|
254
|
+
analyzeGo(content, analysis) {
|
|
255
|
+
// Extract imports
|
|
256
|
+
const importRegex = /import\s+(?:\(\s*([\s\S]*?)\s*\)|"([^"]+)")/g;
|
|
257
|
+
let match;
|
|
258
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
259
|
+
if (match[1]) {
|
|
260
|
+
const imports = match[1].split('\n')
|
|
261
|
+
.map(line => line.trim().replace(/^"([^"]+)"$/, '$1'))
|
|
262
|
+
.filter(Boolean);
|
|
263
|
+
imports.forEach(imp => {
|
|
264
|
+
analysis.imports.push({
|
|
265
|
+
module: imp,
|
|
266
|
+
imports: [imp.split('/').pop()],
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else if (match[2]) {
|
|
271
|
+
analysis.imports.push({
|
|
272
|
+
module: match[2],
|
|
273
|
+
imports: [match[2].split('/').pop()],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Extract functions
|
|
278
|
+
const funcRegex = /func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*\(?([^{]+)\)?)?\s*\{/g;
|
|
279
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
280
|
+
const funcName = match[3];
|
|
281
|
+
const receiver = match[2];
|
|
282
|
+
const paramsStr = match[4] || '';
|
|
283
|
+
const returnType = match[5]?.trim();
|
|
284
|
+
const isExported = funcName[0] === funcName[0].toUpperCase();
|
|
285
|
+
analysis.functions.push({
|
|
286
|
+
name: receiver ? `${receiver}.${funcName}` : funcName,
|
|
287
|
+
params: this.parseGoParams(paramsStr),
|
|
288
|
+
returnType,
|
|
289
|
+
isAsync: false,
|
|
290
|
+
isExported,
|
|
291
|
+
complexity: 'medium',
|
|
292
|
+
dependencies: [],
|
|
293
|
+
sideEffects: [],
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Extract structs (as classes)
|
|
297
|
+
const structRegex = /type\s+(\w+)\s+struct\s*\{/g;
|
|
298
|
+
while ((match = structRegex.exec(content)) !== null) {
|
|
299
|
+
analysis.classes.push({
|
|
300
|
+
name: match[1],
|
|
301
|
+
methods: [],
|
|
302
|
+
properties: [],
|
|
303
|
+
isExported: match[1][0] === match[1][0].toUpperCase(),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Generate test cases for analyzed code
|
|
309
|
+
*/
|
|
310
|
+
async generateTests(analysis, config = {}) {
|
|
311
|
+
const { framework = await this.detectFramework(analysis.language), outputDir, includeEdgeCases = true, includeMocks = true, maxTestsPerFunction = 5, style = 'describe-it', } = config;
|
|
312
|
+
const testCases = [];
|
|
313
|
+
// Generate tests for each function
|
|
314
|
+
for (const func of analysis.functions) {
|
|
315
|
+
if (!func.isExported)
|
|
316
|
+
continue;
|
|
317
|
+
const funcTests = this.generateFunctionTests(func, {
|
|
318
|
+
includeEdgeCases,
|
|
319
|
+
maxTests: maxTestsPerFunction,
|
|
320
|
+
});
|
|
321
|
+
testCases.push(...funcTests);
|
|
322
|
+
}
|
|
323
|
+
// Generate tests for each class
|
|
324
|
+
for (const cls of analysis.classes) {
|
|
325
|
+
if (!cls.isExported)
|
|
326
|
+
continue;
|
|
327
|
+
const classTests = this.generateClassTests(cls, {
|
|
328
|
+
includeEdgeCases,
|
|
329
|
+
maxTests: maxTestsPerFunction,
|
|
330
|
+
});
|
|
331
|
+
testCases.push(...classTests);
|
|
332
|
+
}
|
|
333
|
+
// Generate test file content
|
|
334
|
+
const testContent = this.generateTestFileContent(analysis, testCases, framework, style, includeMocks);
|
|
335
|
+
// Determine output path
|
|
336
|
+
const sourceFileName = path.basename(analysis.filePath);
|
|
337
|
+
const ext = path.extname(sourceFileName);
|
|
338
|
+
const baseName = sourceFileName.replace(ext, '');
|
|
339
|
+
let testFileName;
|
|
340
|
+
let testDir;
|
|
341
|
+
switch (analysis.language) {
|
|
342
|
+
case 'python':
|
|
343
|
+
testFileName = `test_${baseName}.py`;
|
|
344
|
+
testDir = outputDir || path.join(path.dirname(analysis.filePath), 'tests');
|
|
345
|
+
break;
|
|
346
|
+
case 'go':
|
|
347
|
+
testFileName = `${baseName}_test.go`;
|
|
348
|
+
testDir = outputDir || path.dirname(analysis.filePath);
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
testFileName = `${baseName}.test${ext}`;
|
|
352
|
+
testDir = outputDir || path.join(path.dirname(analysis.filePath), '__tests__');
|
|
353
|
+
}
|
|
354
|
+
const testFilePath = path.join(testDir, testFileName);
|
|
355
|
+
return {
|
|
356
|
+
testFilePath,
|
|
357
|
+
framework,
|
|
358
|
+
content: testContent,
|
|
359
|
+
testCases,
|
|
360
|
+
coverage: {
|
|
361
|
+
functions: analysis.functions.filter(f => f.isExported).length,
|
|
362
|
+
branches: Math.round(testCases.length * 0.8),
|
|
363
|
+
lines: Math.round(testCases.length * 10),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Generate test cases for a function
|
|
369
|
+
*/
|
|
370
|
+
generateFunctionTests(func, options) {
|
|
371
|
+
const tests = [];
|
|
372
|
+
// Basic functionality test
|
|
373
|
+
tests.push({
|
|
374
|
+
name: `should execute ${func.name} successfully`,
|
|
375
|
+
description: `Test basic functionality of ${func.name}`,
|
|
376
|
+
type: 'unit',
|
|
377
|
+
assertion: `expect(${func.name}(${this.generateDefaultArgs(func.params)})).toBeDefined()`,
|
|
378
|
+
});
|
|
379
|
+
// Return type test
|
|
380
|
+
if (func.returnType) {
|
|
381
|
+
tests.push({
|
|
382
|
+
name: `should return correct type from ${func.name}`,
|
|
383
|
+
description: `Test return type of ${func.name}`,
|
|
384
|
+
type: 'unit',
|
|
385
|
+
assertion: this.generateTypeAssertion(func.name, func.returnType, func.params),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
// Async test
|
|
389
|
+
if (func.isAsync) {
|
|
390
|
+
tests.push({
|
|
391
|
+
name: `should resolve ${func.name} promise`,
|
|
392
|
+
description: `Test async behavior of ${func.name}`,
|
|
393
|
+
type: 'unit',
|
|
394
|
+
assertion: `await expect(${func.name}(${this.generateDefaultArgs(func.params)})).resolves.toBeDefined()`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (options.includeEdgeCases) {
|
|
398
|
+
// Edge case tests for each parameter
|
|
399
|
+
for (const param of func.params) {
|
|
400
|
+
const edgeCases = this.generateEdgeCases(param);
|
|
401
|
+
for (const edgeCase of edgeCases.slice(0, 2)) {
|
|
402
|
+
tests.push({
|
|
403
|
+
name: `should handle ${edgeCase.name} for ${param.name}`,
|
|
404
|
+
description: `Edge case test: ${edgeCase.description}`,
|
|
405
|
+
type: 'edge',
|
|
406
|
+
input: edgeCase.value,
|
|
407
|
+
assertion: edgeCase.assertion,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Error handling test
|
|
412
|
+
tests.push({
|
|
413
|
+
name: `should handle errors in ${func.name}`,
|
|
414
|
+
description: `Test error handling`,
|
|
415
|
+
type: 'error',
|
|
416
|
+
assertion: func.isAsync
|
|
417
|
+
? `await expect(${func.name}(${this.generateInvalidArgs(func.params)})).rejects.toThrow()`
|
|
418
|
+
: `expect(() => ${func.name}(${this.generateInvalidArgs(func.params)})).toThrow()`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return tests.slice(0, options.maxTests);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Generate test cases for a class
|
|
425
|
+
*/
|
|
426
|
+
generateClassTests(cls, options) {
|
|
427
|
+
const tests = [];
|
|
428
|
+
// Constructor test
|
|
429
|
+
if (cls.constructorMethod) {
|
|
430
|
+
tests.push({
|
|
431
|
+
name: `should create ${cls.name} instance`,
|
|
432
|
+
description: `Test ${cls.name} constructor`,
|
|
433
|
+
type: 'unit',
|
|
434
|
+
assertion: `const instance = new ${cls.name}(${this.generateDefaultArgs(cls.constructorMethod.params)}); expect(instance).toBeInstanceOf(${cls.name})`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Method tests
|
|
438
|
+
for (const method of cls.methods) {
|
|
439
|
+
tests.push({
|
|
440
|
+
name: `${cls.name}.${method.name} should work correctly`,
|
|
441
|
+
description: `Test ${method.name} method of ${cls.name}`,
|
|
442
|
+
type: 'unit',
|
|
443
|
+
assertion: method.isAsync
|
|
444
|
+
? `await expect(instance.${method.name}(${this.generateDefaultArgs(method.params)})).resolves.toBeDefined()`
|
|
445
|
+
: `expect(instance.${method.name}(${this.generateDefaultArgs(method.params)})).toBeDefined()`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// Property tests
|
|
449
|
+
for (const prop of cls.properties.filter(p => p.visibility === 'public')) {
|
|
450
|
+
tests.push({
|
|
451
|
+
name: `${cls.name}.${prop.name} should be accessible`,
|
|
452
|
+
description: `Test ${prop.name} property of ${cls.name}`,
|
|
453
|
+
type: 'unit',
|
|
454
|
+
assertion: `expect(instance.${prop.name}).toBeDefined()`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return tests.slice(0, options.maxTests);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Generate test file content based on framework
|
|
461
|
+
*/
|
|
462
|
+
generateTestFileContent(analysis, testCases, framework, style, includeMocks) {
|
|
463
|
+
const lines = [];
|
|
464
|
+
const relativePath = this.getRelativeImportPath(analysis.filePath);
|
|
465
|
+
switch (framework) {
|
|
466
|
+
case 'vitest':
|
|
467
|
+
return this.generateVitestContent(analysis, testCases, relativePath, style, includeMocks);
|
|
468
|
+
case 'jest':
|
|
469
|
+
return this.generateJestContent(analysis, testCases, relativePath, style, includeMocks);
|
|
470
|
+
case 'mocha':
|
|
471
|
+
return this.generateMochaContent(analysis, testCases, relativePath, style, includeMocks);
|
|
472
|
+
case 'pytest':
|
|
473
|
+
return this.generatePytestContent(analysis, testCases, relativePath, includeMocks);
|
|
474
|
+
case 'go':
|
|
475
|
+
return this.generateGoTestContent(analysis, testCases, includeMocks);
|
|
476
|
+
default:
|
|
477
|
+
return this.generateVitestContent(analysis, testCases, relativePath, style, includeMocks);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Generate Vitest test content
|
|
482
|
+
*/
|
|
483
|
+
generateVitestContent(analysis, testCases, relativePath, style, includeMocks) {
|
|
484
|
+
const lines = [];
|
|
485
|
+
// Imports
|
|
486
|
+
lines.push(`import { describe, it, expect, beforeEach, afterEach${includeMocks ? ', vi' : ''} } from 'vitest';`);
|
|
487
|
+
const exports = analysis.exports.filter(e => analysis.functions.some(f => f.name === e) ||
|
|
488
|
+
analysis.classes.some(c => c.name === e));
|
|
489
|
+
if (exports.length > 0) {
|
|
490
|
+
lines.push(`import { ${exports.join(', ')} } from '${relativePath}';`);
|
|
491
|
+
}
|
|
492
|
+
lines.push('');
|
|
493
|
+
if (includeMocks) {
|
|
494
|
+
lines.push('// Mock setup');
|
|
495
|
+
lines.push('beforeEach(() => {');
|
|
496
|
+
lines.push(' vi.clearAllMocks();');
|
|
497
|
+
lines.push('});');
|
|
498
|
+
lines.push('');
|
|
499
|
+
}
|
|
500
|
+
// Group tests by function/class
|
|
501
|
+
const groupedTests = this.groupTestCases(testCases, analysis);
|
|
502
|
+
for (const [groupName, tests] of Object.entries(groupedTests)) {
|
|
503
|
+
if (style === 'describe-it') {
|
|
504
|
+
lines.push(`describe('${groupName}', () => {`);
|
|
505
|
+
for (const test of tests) {
|
|
506
|
+
const asyncPrefix = test.assertion?.includes('await') ? 'async ' : '';
|
|
507
|
+
lines.push(` it('${test.name}', ${asyncPrefix}() => {`);
|
|
508
|
+
if (test.assertion) {
|
|
509
|
+
lines.push(` ${test.assertion};`);
|
|
510
|
+
}
|
|
511
|
+
lines.push(' });');
|
|
512
|
+
lines.push('');
|
|
513
|
+
}
|
|
514
|
+
lines.push('});');
|
|
515
|
+
lines.push('');
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
for (const test of tests) {
|
|
519
|
+
const asyncPrefix = test.assertion?.includes('await') ? 'async ' : '';
|
|
520
|
+
lines.push(`test('${groupName}: ${test.name}', ${asyncPrefix}() => {`);
|
|
521
|
+
if (test.assertion) {
|
|
522
|
+
lines.push(` ${test.assertion};`);
|
|
523
|
+
}
|
|
524
|
+
lines.push('});');
|
|
525
|
+
lines.push('');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return lines.join('\n');
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Generate Jest test content
|
|
533
|
+
*/
|
|
534
|
+
generateJestContent(analysis, testCases, relativePath, style, includeMocks) {
|
|
535
|
+
const lines = [];
|
|
536
|
+
const exports = analysis.exports.filter(e => analysis.functions.some(f => f.name === e) ||
|
|
537
|
+
analysis.classes.some(c => c.name === e));
|
|
538
|
+
if (exports.length > 0) {
|
|
539
|
+
lines.push(`const { ${exports.join(', ')} } = require('${relativePath}');`);
|
|
540
|
+
}
|
|
541
|
+
lines.push('');
|
|
542
|
+
if (includeMocks) {
|
|
543
|
+
lines.push('// Mock setup');
|
|
544
|
+
lines.push('beforeEach(() => {');
|
|
545
|
+
lines.push(' jest.clearAllMocks();');
|
|
546
|
+
lines.push('});');
|
|
547
|
+
lines.push('');
|
|
548
|
+
}
|
|
549
|
+
const groupedTests = this.groupTestCases(testCases, analysis);
|
|
550
|
+
for (const [groupName, tests] of Object.entries(groupedTests)) {
|
|
551
|
+
lines.push(`describe('${groupName}', () => {`);
|
|
552
|
+
for (const test of tests) {
|
|
553
|
+
const asyncPrefix = test.assertion?.includes('await') ? 'async ' : '';
|
|
554
|
+
lines.push(` ${style === 'test' ? 'test' : 'it'}('${test.name}', ${asyncPrefix}() => {`);
|
|
555
|
+
if (test.assertion) {
|
|
556
|
+
lines.push(` ${test.assertion};`);
|
|
557
|
+
}
|
|
558
|
+
lines.push(' });');
|
|
559
|
+
lines.push('');
|
|
560
|
+
}
|
|
561
|
+
lines.push('});');
|
|
562
|
+
lines.push('');
|
|
563
|
+
}
|
|
564
|
+
return lines.join('\n');
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Generate Mocha test content
|
|
568
|
+
*/
|
|
569
|
+
generateMochaContent(analysis, testCases, relativePath, style, includeMocks) {
|
|
570
|
+
const lines = [];
|
|
571
|
+
lines.push(`const { expect } = require('chai');`);
|
|
572
|
+
if (includeMocks) {
|
|
573
|
+
lines.push(`const sinon = require('sinon');`);
|
|
574
|
+
}
|
|
575
|
+
const exports = analysis.exports.filter(e => analysis.functions.some(f => f.name === e) ||
|
|
576
|
+
analysis.classes.some(c => c.name === e));
|
|
577
|
+
if (exports.length > 0) {
|
|
578
|
+
lines.push(`const { ${exports.join(', ')} } = require('${relativePath}');`);
|
|
579
|
+
}
|
|
580
|
+
lines.push('');
|
|
581
|
+
if (includeMocks) {
|
|
582
|
+
lines.push('beforeEach(() => {');
|
|
583
|
+
lines.push(' sinon.restore();');
|
|
584
|
+
lines.push('});');
|
|
585
|
+
lines.push('');
|
|
586
|
+
}
|
|
587
|
+
const groupedTests = this.groupTestCases(testCases, analysis);
|
|
588
|
+
for (const [groupName, tests] of Object.entries(groupedTests)) {
|
|
589
|
+
lines.push(`describe('${groupName}', () => {`);
|
|
590
|
+
for (const test of tests) {
|
|
591
|
+
const asyncPrefix = test.assertion?.includes('await') ? 'async ' : '';
|
|
592
|
+
lines.push(` it('${test.name}', ${asyncPrefix}() => {`);
|
|
593
|
+
if (test.assertion) {
|
|
594
|
+
// Convert Jest-style to Chai-style assertions
|
|
595
|
+
const chaiAssertion = test.assertion
|
|
596
|
+
.replace(/expect\(([^)]+)\)\.toBeDefined\(\)/g, 'expect($1).to.exist')
|
|
597
|
+
.replace(/expect\(([^)]+)\)\.toBeInstanceOf\(([^)]+)\)/g, 'expect($1).to.be.instanceOf($2)')
|
|
598
|
+
.replace(/\.toThrow\(\)/g, '.to.throw()');
|
|
599
|
+
lines.push(` ${chaiAssertion};`);
|
|
600
|
+
}
|
|
601
|
+
lines.push(' });');
|
|
602
|
+
lines.push('');
|
|
603
|
+
}
|
|
604
|
+
lines.push('});');
|
|
605
|
+
lines.push('');
|
|
606
|
+
}
|
|
607
|
+
return lines.join('\n');
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Generate pytest test content
|
|
611
|
+
*/
|
|
612
|
+
generatePytestContent(analysis, testCases, relativePath, includeMocks) {
|
|
613
|
+
const lines = [];
|
|
614
|
+
lines.push('import pytest');
|
|
615
|
+
if (includeMocks) {
|
|
616
|
+
lines.push('from unittest.mock import Mock, patch, MagicMock');
|
|
617
|
+
}
|
|
618
|
+
// Convert relative path to Python import
|
|
619
|
+
const moduleName = relativePath.replace(/\//g, '.').replace(/\.py$/, '');
|
|
620
|
+
const exports = analysis.exports.filter(e => analysis.functions.some(f => f.name === e) ||
|
|
621
|
+
analysis.classes.some(c => c.name === e));
|
|
622
|
+
if (exports.length > 0) {
|
|
623
|
+
lines.push(`from ${moduleName} import ${exports.join(', ')}`);
|
|
624
|
+
}
|
|
625
|
+
lines.push('');
|
|
626
|
+
lines.push('');
|
|
627
|
+
const groupedTests = this.groupTestCases(testCases, analysis);
|
|
628
|
+
for (const [groupName, tests] of Object.entries(groupedTests)) {
|
|
629
|
+
lines.push(`class Test${groupName.replace(/\W/g, '')}:`);
|
|
630
|
+
lines.push(` """Tests for ${groupName}"""`);
|
|
631
|
+
lines.push('');
|
|
632
|
+
for (const test of tests) {
|
|
633
|
+
const funcName = `test_${test.name.toLowerCase().replace(/\s+/g, '_').replace(/[^\w]/g, '')}`;
|
|
634
|
+
const asyncPrefix = test.assertion?.includes('await') ? 'async ' : '';
|
|
635
|
+
lines.push(` ${asyncPrefix}def ${funcName}(self):`);
|
|
636
|
+
lines.push(` """${test.description}"""`);
|
|
637
|
+
if (test.assertion) {
|
|
638
|
+
// Convert to pytest assertions
|
|
639
|
+
const pytestAssertion = this.convertToPytestAssertion(test.assertion, groupName);
|
|
640
|
+
lines.push(` ${pytestAssertion}`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
lines.push(' pass');
|
|
644
|
+
}
|
|
645
|
+
lines.push('');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return lines.join('\n');
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Generate Go test content
|
|
652
|
+
*/
|
|
653
|
+
generateGoTestContent(analysis, testCases, includeMocks) {
|
|
654
|
+
const lines = [];
|
|
655
|
+
// Get package name from file
|
|
656
|
+
const packageName = path.basename(path.dirname(analysis.filePath));
|
|
657
|
+
lines.push(`package ${packageName}`);
|
|
658
|
+
lines.push('');
|
|
659
|
+
lines.push('import (');
|
|
660
|
+
lines.push(' "testing"');
|
|
661
|
+
if (includeMocks) {
|
|
662
|
+
lines.push(' "github.com/stretchr/testify/assert"');
|
|
663
|
+
lines.push(' "github.com/stretchr/testify/mock"');
|
|
664
|
+
}
|
|
665
|
+
lines.push(')');
|
|
666
|
+
lines.push('');
|
|
667
|
+
const groupedTests = this.groupTestCases(testCases, analysis);
|
|
668
|
+
for (const [groupName, tests] of Object.entries(groupedTests)) {
|
|
669
|
+
for (const test of tests) {
|
|
670
|
+
const funcName = `Test${groupName.replace(/\W/g, '')}_${test.name.replace(/\s+/g, '_').replace(/[^\w]/g, '')}`;
|
|
671
|
+
lines.push(`func ${funcName}(t *testing.T) {`);
|
|
672
|
+
lines.push(` // ${test.description}`);
|
|
673
|
+
if (includeMocks) {
|
|
674
|
+
lines.push(` // Add test implementation`);
|
|
675
|
+
lines.push(` assert.NotNil(t, nil) // TODO: Implement test`);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
lines.push(` // TODO: Implement test`);
|
|
679
|
+
}
|
|
680
|
+
lines.push('}');
|
|
681
|
+
lines.push('');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return lines.join('\n');
|
|
685
|
+
}
|
|
686
|
+
// Helper methods
|
|
687
|
+
parseJSTSParams(paramsStr) {
|
|
688
|
+
if (!paramsStr.trim())
|
|
689
|
+
return [];
|
|
690
|
+
return paramsStr.split(',').map(param => {
|
|
691
|
+
const [nameWithType, ...rest] = param.trim().split(':');
|
|
692
|
+
const name = nameWithType.replace('?', '').trim();
|
|
693
|
+
const type = rest.join(':').trim() || undefined;
|
|
694
|
+
const optional = nameWithType.includes('?') || param.includes('=');
|
|
695
|
+
return { name, type, optional };
|
|
696
|
+
}).filter(p => p.name && !p.name.startsWith('...'));
|
|
697
|
+
}
|
|
698
|
+
parsePythonParams(paramsStr) {
|
|
699
|
+
if (!paramsStr.trim())
|
|
700
|
+
return [];
|
|
701
|
+
return paramsStr.split(',').map(param => {
|
|
702
|
+
const [nameWithDefault, typeAnnotation] = param.split(':');
|
|
703
|
+
const name = nameWithDefault.split('=')[0].trim();
|
|
704
|
+
const type = typeAnnotation?.split('=')[0].trim();
|
|
705
|
+
const optional = param.includes('=') || param.includes('None');
|
|
706
|
+
return { name, type, optional };
|
|
707
|
+
}).filter(p => p.name && p.name !== 'self' && p.name !== 'cls' && !p.name.startsWith('*'));
|
|
708
|
+
}
|
|
709
|
+
parseGoParams(paramsStr) {
|
|
710
|
+
if (!paramsStr.trim())
|
|
711
|
+
return [];
|
|
712
|
+
const params = [];
|
|
713
|
+
const parts = paramsStr.split(',');
|
|
714
|
+
for (const part of parts) {
|
|
715
|
+
const trimmed = part.trim();
|
|
716
|
+
const match = trimmed.match(/(\w+)\s+(\S+)/);
|
|
717
|
+
if (match) {
|
|
718
|
+
params.push({
|
|
719
|
+
name: match[1],
|
|
720
|
+
type: match[2],
|
|
721
|
+
optional: false,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return params;
|
|
726
|
+
}
|
|
727
|
+
extractClassBody(content, startIndex) {
|
|
728
|
+
let braceCount = 0;
|
|
729
|
+
let started = false;
|
|
730
|
+
let endIndex = startIndex;
|
|
731
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
732
|
+
if (content[i] === '{') {
|
|
733
|
+
braceCount++;
|
|
734
|
+
started = true;
|
|
735
|
+
}
|
|
736
|
+
else if (content[i] === '}') {
|
|
737
|
+
braceCount--;
|
|
738
|
+
if (started && braceCount === 0) {
|
|
739
|
+
endIndex = i;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return content.substring(startIndex, endIndex + 1);
|
|
745
|
+
}
|
|
746
|
+
estimateComplexity(funcBody) {
|
|
747
|
+
const controlStructures = (funcBody.match(/\b(if|else|for|while|switch|catch|try)\b/g) || []).length;
|
|
748
|
+
const nestedBlocks = (funcBody.match(/\{[^{}]*\{/g) || []).length;
|
|
749
|
+
if (controlStructures > 5 || nestedBlocks > 3)
|
|
750
|
+
return 'high';
|
|
751
|
+
if (controlStructures > 2 || nestedBlocks > 1)
|
|
752
|
+
return 'medium';
|
|
753
|
+
return 'low';
|
|
754
|
+
}
|
|
755
|
+
detectDependencies(funcBody, imports) {
|
|
756
|
+
const deps = [];
|
|
757
|
+
for (const imp of imports) {
|
|
758
|
+
for (const name of imp.imports) {
|
|
759
|
+
if (funcBody.includes(name)) {
|
|
760
|
+
deps.push(name);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return deps;
|
|
765
|
+
}
|
|
766
|
+
detectSideEffects(funcBody) {
|
|
767
|
+
const effects = [];
|
|
768
|
+
if (/console\.(log|warn|error|info)/.test(funcBody))
|
|
769
|
+
effects.push('console');
|
|
770
|
+
if (/fs\.(write|append|unlink|mkdir|rm)/.test(funcBody))
|
|
771
|
+
effects.push('filesystem');
|
|
772
|
+
if (/fetch|axios|http\./.test(funcBody))
|
|
773
|
+
effects.push('network');
|
|
774
|
+
if (/\bprocess\./.test(funcBody))
|
|
775
|
+
effects.push('process');
|
|
776
|
+
if (/\bglobal\b|\bwindow\b/.test(funcBody))
|
|
777
|
+
effects.push('global');
|
|
778
|
+
return effects;
|
|
779
|
+
}
|
|
780
|
+
async detectFramework(language) {
|
|
781
|
+
if (language === 'python')
|
|
782
|
+
return 'pytest';
|
|
783
|
+
if (language === 'go')
|
|
784
|
+
return 'go';
|
|
785
|
+
const testInfo = await this.testDetector.detectTestRunner();
|
|
786
|
+
return testInfo.runner || 'vitest';
|
|
787
|
+
}
|
|
788
|
+
getRelativeImportPath(filePath) {
|
|
789
|
+
const ext = path.extname(filePath);
|
|
790
|
+
const baseName = path.basename(filePath, ext);
|
|
791
|
+
return `../${baseName}`;
|
|
792
|
+
}
|
|
793
|
+
groupTestCases(testCases, analysis) {
|
|
794
|
+
const groups = {};
|
|
795
|
+
for (const test of testCases) {
|
|
796
|
+
// Extract function/class name from test name
|
|
797
|
+
const match = test.name.match(/(\w+)/);
|
|
798
|
+
const groupName = match ? match[1] : 'General';
|
|
799
|
+
if (!groups[groupName]) {
|
|
800
|
+
groups[groupName] = [];
|
|
801
|
+
}
|
|
802
|
+
groups[groupName].push(test);
|
|
803
|
+
}
|
|
804
|
+
return groups;
|
|
805
|
+
}
|
|
806
|
+
generateDefaultArgs(params) {
|
|
807
|
+
return params.map(p => {
|
|
808
|
+
if (p.type?.includes('string'))
|
|
809
|
+
return "''";
|
|
810
|
+
if (p.type?.includes('number') || p.type?.includes('int'))
|
|
811
|
+
return '0';
|
|
812
|
+
if (p.type?.includes('boolean') || p.type?.includes('bool'))
|
|
813
|
+
return 'false';
|
|
814
|
+
if (p.type?.includes('[]') || p.type?.includes('Array'))
|
|
815
|
+
return '[]';
|
|
816
|
+
if (p.type?.includes('object') || p.type?.includes('Record'))
|
|
817
|
+
return '{}';
|
|
818
|
+
if (p.optional)
|
|
819
|
+
return 'undefined';
|
|
820
|
+
return 'null';
|
|
821
|
+
}).join(', ');
|
|
822
|
+
}
|
|
823
|
+
generateInvalidArgs(params) {
|
|
824
|
+
if (params.length === 0)
|
|
825
|
+
return 'null';
|
|
826
|
+
return params.map(() => 'undefined').join(', ');
|
|
827
|
+
}
|
|
828
|
+
generateTypeAssertion(funcName, returnType, params) {
|
|
829
|
+
const args = this.generateDefaultArgs(params);
|
|
830
|
+
if (returnType.includes('string')) {
|
|
831
|
+
return `expect(typeof ${funcName}(${args})).toBe('string')`;
|
|
832
|
+
}
|
|
833
|
+
if (returnType.includes('number')) {
|
|
834
|
+
return `expect(typeof ${funcName}(${args})).toBe('number')`;
|
|
835
|
+
}
|
|
836
|
+
if (returnType.includes('boolean')) {
|
|
837
|
+
return `expect(typeof ${funcName}(${args})).toBe('boolean')`;
|
|
838
|
+
}
|
|
839
|
+
if (returnType.includes('[]') || returnType.includes('Array')) {
|
|
840
|
+
return `expect(Array.isArray(${funcName}(${args}))).toBe(true)`;
|
|
841
|
+
}
|
|
842
|
+
if (returnType.includes('Promise')) {
|
|
843
|
+
return `expect(${funcName}(${args})).toBeInstanceOf(Promise)`;
|
|
844
|
+
}
|
|
845
|
+
return `expect(${funcName}(${args})).toBeDefined()`;
|
|
846
|
+
}
|
|
847
|
+
generateEdgeCases(param) {
|
|
848
|
+
const edgeCases = [];
|
|
849
|
+
if (param.type?.includes('string')) {
|
|
850
|
+
edgeCases.push({ name: 'empty string', description: 'Empty string input', value: "''", assertion: 'expect(result).toBeDefined()' }, { name: 'whitespace', description: 'Whitespace only input', value: "' '", assertion: 'expect(result).toBeDefined()' }, { name: 'special chars', description: 'Special characters', value: "'<>&\"'", assertion: 'expect(result).toBeDefined()' });
|
|
851
|
+
}
|
|
852
|
+
if (param.type?.includes('number') || param.type?.includes('int')) {
|
|
853
|
+
edgeCases.push({ name: 'zero', description: 'Zero value', value: '0', assertion: 'expect(result).toBeDefined()' }, { name: 'negative', description: 'Negative number', value: '-1', assertion: 'expect(result).toBeDefined()' }, { name: 'large number', description: 'Large number', value: 'Number.MAX_SAFE_INTEGER', assertion: 'expect(result).toBeDefined()' });
|
|
854
|
+
}
|
|
855
|
+
if (param.type?.includes('[]') || param.type?.includes('Array')) {
|
|
856
|
+
edgeCases.push({ name: 'empty array', description: 'Empty array', value: '[]', assertion: 'expect(result).toBeDefined()' }, { name: 'single element', description: 'Single element array', value: '[1]', assertion: 'expect(result).toBeDefined()' });
|
|
857
|
+
}
|
|
858
|
+
if (param.optional) {
|
|
859
|
+
edgeCases.push({ name: 'undefined', description: 'Undefined optional param', value: 'undefined', assertion: 'expect(result).toBeDefined()' }, { name: 'null', description: 'Null optional param', value: 'null', assertion: 'expect(result).toBeDefined()' });
|
|
860
|
+
}
|
|
861
|
+
return edgeCases;
|
|
862
|
+
}
|
|
863
|
+
convertToPytestAssertion(assertion, funcName) {
|
|
864
|
+
return assertion
|
|
865
|
+
.replace(/expect\(([^)]+)\)\.toBeDefined\(\)/g, 'assert $1 is not None')
|
|
866
|
+
.replace(/expect\(([^)]+)\)\.toBeInstanceOf\(([^)]+)\)/g, 'assert isinstance($1, $2)')
|
|
867
|
+
.replace(/expect\(([^)]+)\)\.toBe\(([^)]+)\)/g, 'assert $1 == $2')
|
|
868
|
+
.replace(/expect\(([^)]+)\)\.toEqual\(([^)]+)\)/g, 'assert $1 == $2')
|
|
869
|
+
.replace(/\.toThrow\(\)/g, '')
|
|
870
|
+
.replace(/expect\(\(\) => ([^)]+)\)/g, 'with pytest.raises(Exception): $1');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Write generated tests to file
|
|
875
|
+
*/
|
|
876
|
+
export async function writeTestFile(generatedTest) {
|
|
877
|
+
const dir = path.dirname(generatedTest.testFilePath);
|
|
878
|
+
await fs.mkdir(dir, { recursive: true });
|
|
879
|
+
await fs.writeFile(generatedTest.testFilePath, generatedTest.content, 'utf-8');
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Quick function to generate and optionally write tests
|
|
883
|
+
*/
|
|
884
|
+
export async function generateTestsForFile(filePath, config = {}, writeToFile = false) {
|
|
885
|
+
const generator = new TestGenerator();
|
|
886
|
+
const analysis = await generator.analyzeFile(filePath);
|
|
887
|
+
const tests = await generator.generateTests(analysis, config);
|
|
888
|
+
if (writeToFile) {
|
|
889
|
+
await writeTestFile(tests);
|
|
890
|
+
}
|
|
891
|
+
return tests;
|
|
892
|
+
}
|
|
893
|
+
//# sourceMappingURL=test-generator.js.map
|