UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

521 lines (463 loc) 14.9 kB
/** * generate-tests.ts - AI-powered test generation MCP tool handler * * Generates tests for code using AI-powered test generation with support * for multiple test types (unit, integration, E2E, property, mutation, fuzz) * and frameworks (vitest, jest, mocha, pytest, junit). */ import { z } from 'zod'; // Input schema for generate-tests tool export const GenerateTestsInputSchema = z.object({ targetPath: z.string().describe('Path to file/directory to test'), testType: z .enum(['unit', 'integration', 'e2e', 'property', 'mutation', 'fuzz']) .default('unit') .describe('Type of test to generate'), framework: z .enum(['vitest', 'jest', 'mocha', 'pytest', 'junit']) .optional() .describe('Test framework to use'), coverage: z .object({ target: z.number().min(0).max(100).default(80).describe('Target coverage %'), focusGaps: z.boolean().default(true).describe('Focus on coverage gaps'), }) .optional() .describe('Coverage configuration'), style: z .enum(['tdd-london', 'tdd-chicago', 'bdd', 'example-based']) .default('tdd-london') .describe('Test style methodology'), language: z .enum(['typescript', 'javascript', 'python', 'java', 'go', 'rust']) .optional() .describe('Programming language (auto-detected if not specified)'), includeEdgeCases: z.boolean().default(true).describe('Generate edge case tests'), includeMocks: z.boolean().default(true).describe('Generate mocks/stubs'), maxTests: z.number().min(1).max(100).default(20).describe('Maximum tests to generate'), }); export type GenerateTestsInput = z.infer<typeof GenerateTestsInputSchema>; // Output structure export interface GenerateTestsOutput { success: boolean; testFile: string; tests: GeneratedTest[]; coverage: CoverageEstimate; metadata: TestGenerationMetadata; } export interface GeneratedTest { name: string; type: 'unit' | 'integration' | 'e2e' | 'property' | 'mutation' | 'fuzz'; description: string; code: string; targetFunction?: string; targetClass?: string; edgeCase: boolean; assertions: number; } export interface CoverageEstimate { lineCoverage: number; branchCoverage: number; functionCoverage: number; uncoveredLines: number[]; gaps: CoverageGap[]; } export interface CoverageGap { type: 'line' | 'branch' | 'function'; location: string; reason: string; priority: 'low' | 'medium' | 'high'; } export interface TestGenerationMetadata { generatedAt: string; framework: string; style: string; totalTests: number; executionTimeMs: number; modelUsed: string; tokensUsed: number; } // Tool context interface export interface ToolContext { get<T>(key: string): T | undefined; } /** * MCP Tool Handler for generate-tests */ export async function handler( input: GenerateTestsInput, context: ToolContext ): Promise<{ content: Array<{ type: 'text'; text: string }> }> { const startTime = Date.now(); try { // Validate input const validatedInput = GenerateTestsInputSchema.parse(input); // Get bridge from context (would be set by plugin initialization) const bridge = context.get<{ searchSimilarPatterns: (q: string, k: number) => Promise<unknown[]> }>('aqe.bridge'); // Detect language from file extension if not specified const language = validatedInput.language || detectLanguage(validatedInput.targetPath); // Determine framework based on language if not specified const framework = validatedInput.framework || getDefaultFramework(language); // Generate tests based on configuration const generatedTests = await generateTests({ targetPath: validatedInput.targetPath, testType: validatedInput.testType, framework, style: validatedInput.style, language, includeEdgeCases: validatedInput.includeEdgeCases, includeMocks: validatedInput.includeMocks, maxTests: validatedInput.maxTests, coverageTarget: validatedInput.coverage?.target ?? 80, focusGaps: validatedInput.coverage?.focusGaps ?? true, bridge, }); // Build result const result: GenerateTestsOutput = { success: true, testFile: getTestFilePath(validatedInput.targetPath, framework), tests: generatedTests.tests, coverage: generatedTests.coverageEstimate, metadata: { generatedAt: new Date().toISOString(), framework, style: validatedInput.style, totalTests: generatedTests.tests.length, executionTimeMs: Date.now() - startTime, modelUsed: generatedTests.modelUsed, tokensUsed: generatedTests.tokensUsed, }, }; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: errorMessage, metadata: { generatedAt: new Date().toISOString(), executionTimeMs: Date.now() - startTime, }, }, null, 2 ), }, ], }; } } // Helper functions function detectLanguage(filePath: string): string { const ext = filePath.split('.').pop()?.toLowerCase(); const langMap: Record<string, string> = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', py: 'python', java: 'java', go: 'go', rs: 'rust', }; return langMap[ext || ''] || 'typescript'; } function getDefaultFramework(language: string): string { const frameworkMap: Record<string, string> = { typescript: 'vitest', javascript: 'jest', python: 'pytest', java: 'junit', go: 'go-test', rust: 'cargo-test', }; return frameworkMap[language] || 'vitest'; } function getTestFilePath(sourcePath: string, framework: string): string { const pathParts = sourcePath.split('/'); const fileName = pathParts.pop() || 'unknown'; const baseName = fileName.replace(/\.[^.]+$/, ''); const testSuffix = framework === 'pytest' ? '_test' : '.test'; const ext = framework === 'pytest' ? '.py' : '.ts'; return [...pathParts, '__tests__', `${baseName}${testSuffix}${ext}`].join('/'); } interface GenerateTestsConfig { targetPath: string; testType: string; framework: string; style: string; language: string; includeEdgeCases: boolean; includeMocks: boolean; maxTests: number; coverageTarget: number; focusGaps: boolean; bridge?: { searchSimilarPatterns: (q: string, k: number) => Promise<unknown[]> }; } interface GenerateTestsResult { tests: GeneratedTest[]; coverageEstimate: CoverageEstimate; modelUsed: string; tokensUsed: number; } async function generateTests(config: GenerateTestsConfig): Promise<GenerateTestsResult> { // Search for similar test patterns if bridge is available let patterns: unknown[] = []; if (config.bridge) { try { patterns = await config.bridge.searchSimilarPatterns( `${config.testType} test ${config.language} ${config.framework}`, 5 ); } catch { // Continue without patterns } } // Generate tests based on type and configuration const tests: GeneratedTest[] = []; // Generate based on test type switch (config.testType) { case 'unit': tests.push(...generateUnitTests(config)); break; case 'integration': tests.push(...generateIntegrationTests(config)); break; case 'e2e': tests.push(...generateE2ETests(config)); break; case 'property': tests.push(...generatePropertyTests(config)); break; case 'mutation': tests.push(...generateMutationTests(config)); break; case 'fuzz': tests.push(...generateFuzzTests(config)); break; } // Add edge cases if requested if (config.includeEdgeCases) { tests.push(...generateEdgeCaseTests(config, tests.length)); } // Limit to maxTests const limitedTests = tests.slice(0, config.maxTests); return { tests: limitedTests, coverageEstimate: estimateCoverage(limitedTests, config.coverageTarget), modelUsed: patterns.length > 0 ? 'pattern-enhanced' : 'base', tokensUsed: estimateTokens(limitedTests), }; } function generateUnitTests(config: GenerateTestsConfig): GeneratedTest[] { const baseName = config.targetPath.split('/').pop()?.replace(/\.[^.]+$/, '') || 'module'; return [ { name: `should create ${baseName} instance`, type: 'unit', description: `Verify ${baseName} can be instantiated correctly`, code: generateTestCode(config.framework, config.style, 'instance creation'), targetClass: baseName, edgeCase: false, assertions: 2, }, { name: `should handle valid input`, type: 'unit', description: `Verify ${baseName} processes valid input correctly`, code: generateTestCode(config.framework, config.style, 'valid input'), targetFunction: 'process', edgeCase: false, assertions: 3, }, { name: `should throw on invalid input`, type: 'unit', description: `Verify ${baseName} throws appropriate error for invalid input`, code: generateTestCode(config.framework, config.style, 'error handling'), targetFunction: 'process', edgeCase: true, assertions: 2, }, ]; } function generateIntegrationTests(config: GenerateTestsConfig): GeneratedTest[] { const baseName = config.targetPath.split('/').pop()?.replace(/\.[^.]+$/, '') || 'module'; return [ { name: `should integrate with dependencies`, type: 'integration', description: `Verify ${baseName} works with its dependencies`, code: generateTestCode(config.framework, config.style, 'integration'), edgeCase: false, assertions: 4, }, { name: `should handle async operations`, type: 'integration', description: `Verify async workflows complete successfully`, code: generateTestCode(config.framework, config.style, 'async'), edgeCase: false, assertions: 3, }, ]; } function generateE2ETests(config: GenerateTestsConfig): GeneratedTest[] { return [ { name: `should complete full workflow`, type: 'e2e', description: `Verify end-to-end user workflow`, code: generateTestCode(config.framework, config.style, 'e2e workflow'), edgeCase: false, assertions: 5, }, ]; } function generatePropertyTests(config: GenerateTestsConfig): GeneratedTest[] { return [ { name: `should satisfy invariant properties`, type: 'property', description: `Property-based test for invariants`, code: generateTestCode(config.framework, config.style, 'property'), edgeCase: false, assertions: 1, }, ]; } function generateMutationTests(config: GenerateTestsConfig): GeneratedTest[] { return [ { name: `should detect mutations`, type: 'mutation', description: `Mutation testing to verify test strength`, code: generateTestCode(config.framework, config.style, 'mutation'), edgeCase: false, assertions: 3, }, ]; } function generateFuzzTests(config: GenerateTestsConfig): GeneratedTest[] { return [ { name: `should handle random input`, type: 'fuzz', description: `Fuzz testing with random input generation`, code: generateTestCode(config.framework, config.style, 'fuzz'), edgeCase: true, assertions: 2, }, ]; } function generateEdgeCaseTests(config: GenerateTestsConfig, existingCount: number): GeneratedTest[] { const maxEdgeCases = Math.min(5, config.maxTests - existingCount); if (maxEdgeCases <= 0) return []; const edgeCases: GeneratedTest[] = [ { name: `should handle null input`, type: config.testType as GeneratedTest['type'], description: `Edge case: null input handling`, code: generateTestCode(config.framework, config.style, 'null'), edgeCase: true, assertions: 2, }, { name: `should handle empty input`, type: config.testType as GeneratedTest['type'], description: `Edge case: empty input handling`, code: generateTestCode(config.framework, config.style, 'empty'), edgeCase: true, assertions: 2, }, { name: `should handle boundary values`, type: config.testType as GeneratedTest['type'], description: `Edge case: boundary value testing`, code: generateTestCode(config.framework, config.style, 'boundary'), edgeCase: true, assertions: 3, }, ]; return edgeCases.slice(0, maxEdgeCases); } function generateTestCode(framework: string, style: string, scenario: string): string { const templates: Record<string, string> = { vitest: `import { describe, it, expect } from 'vitest'; describe('${scenario}', () => { it('should handle ${scenario}', () => { // Arrange const input = /* test input */; // Act const result = /* call function */; // Assert expect(result).toBeDefined(); }); });`, jest: `describe('${scenario}', () => { it('should handle ${scenario}', () => { // Arrange const input = /* test input */; // Act const result = /* call function */; // Assert expect(result).toBeDefined(); }); });`, pytest: `import pytest def test_${scenario.replace(/\s+/g, '_')}(): # Arrange input_data = None # test input # Act result = None # call function # Assert assert result is not None`, }; return templates[framework] || templates.vitest; } function estimateCoverage(tests: GeneratedTest[], target: number): CoverageEstimate { const baselineCoverage = Math.min(tests.length * 8, 95); const adjustedCoverage = Math.min(baselineCoverage, target + 10); return { lineCoverage: adjustedCoverage, branchCoverage: Math.max(adjustedCoverage - 10, 0), functionCoverage: Math.min(adjustedCoverage + 5, 100), uncoveredLines: [], gaps: adjustedCoverage < target ? [ { type: 'branch', location: 'error handling paths', reason: 'Complex branching logic not fully covered', priority: 'high', }, ] : [], }; } function estimateTokens(tests: GeneratedTest[]): number { return tests.reduce((sum, test) => sum + test.code.length / 4, 0); } // Export tool definition for MCP registration export const toolDefinition = { name: 'aqe/generate-tests', description: 'Generate tests for code using AI-powered test generation', category: 'test-generation', version: '3.2.3', inputSchema: GenerateTestsInputSchema, handler, }; export default toolDefinition;