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.
Files changed (38) hide show
  1. package/README.md +282 -26
  2. package/dist/commands/chat.d.ts +1 -0
  3. package/dist/commands/chat.d.ts.map +1 -1
  4. package/dist/commands/chat.js +106 -8
  5. package/dist/commands/chat.js.map +1 -1
  6. package/dist/core/modes.d.ts +1 -1
  7. package/dist/core/modes.d.ts.map +1 -1
  8. package/dist/core/modes.js +7 -8
  9. package/dist/core/modes.js.map +1 -1
  10. package/dist/core/session-bridge.d.ts +105 -0
  11. package/dist/core/session-bridge.d.ts.map +1 -0
  12. package/dist/core/session-bridge.js +243 -0
  13. package/dist/core/session-bridge.js.map +1 -0
  14. package/dist/core/tools.d.ts +4 -0
  15. package/dist/core/tools.d.ts.map +1 -1
  16. package/dist/core/tools.js +171 -0
  17. package/dist/core/tools.js.map +1 -1
  18. package/dist/index.js +42 -35
  19. package/dist/index.js.map +1 -1
  20. package/dist/tools/test-generator.d.ts +157 -0
  21. package/dist/tools/test-generator.d.ts.map +1 -0
  22. package/dist/tools/test-generator.js +893 -0
  23. package/dist/tools/test-generator.js.map +1 -0
  24. package/dist/webui/server.d.ts +96 -0
  25. package/dist/webui/server.d.ts.map +1 -0
  26. package/dist/webui/server.js +2304 -0
  27. package/dist/webui/server.js.map +1 -0
  28. package/package.json +16 -6
  29. package/webui-dist/assets/index-BXqucsdn.js +353 -0
  30. package/webui-dist/assets/index-BXqucsdn.js.map +1 -0
  31. package/webui-dist/assets/index-D1NjcKN3.css +32 -0
  32. package/webui-dist/assets/xterm-BjznWQZK.js +10 -0
  33. package/webui-dist/assets/xterm-BjznWQZK.js.map +1 -0
  34. package/webui-dist/assets/xterm-addon-fit-CPOtr3CW.js +2 -0
  35. package/webui-dist/assets/xterm-addon-fit-CPOtr3CW.js.map +1 -0
  36. package/webui-dist/assets/xterm-addon-web-links-z5RYd3SS.js +2 -0
  37. package/webui-dist/assets/xterm-addon-web-links-z5RYd3SS.js.map +1 -0
  38. 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