import { TestGeneratorPort, TestGeneratorOptions } from '../../domain/ports/TestGeneratorPort'; import { TestSuggestion, TestSuggestionImpl, TestCase } from '../../domain/entities/TestSuggestion'; import { CodeBlock } from '../../domain/entities/CodeBlock'; import { LLMPort } from '../../domain/ports/LLMPort'; export class TestGeneratorAdapter implements TestGeneratorPort { constructor(private readonly llmPort: LLMPort) {} async generateTests( codeBlock: CodeBlock, options: TestGeneratorOptions ): Promise { try { const prompt = this.createTestGenerationPrompt(codeBlock, options); const messages = [ { role: 'system' as const, content: 'You are an expert at writing comprehensive unit tests. Generate high-quality test code that covers edge cases, error conditions, and normal usage scenarios.', }, { role: 'user' as const, content: prompt, }, ]; const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 3000, }); return this.parseTestSuggestions(response.content, codeBlock, options); } catch (error) { throw new Error(`Failed to generate tests: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateTestsForMultiple( codeBlocks: CodeBlock[], options: TestGeneratorOptions ): Promise { const allSuggestions: TestSuggestion[] = []; for (const codeBlock of codeBlocks) { const suggestions = await this.generateTests(codeBlock, options); allSuggestions.push(...suggestions); } return allSuggestions; } async generateTestSetup( language: string, framework: string, dependencies: string[] ): Promise { try { const prompt = this.createTestSetupPrompt(language, framework, dependencies); const messages = [ { role: 'system' as const, content: 'You are an expert at setting up test environments. Generate comprehensive test setup code that includes proper imports, configuration, and helper functions.', }, { role: 'user' as const, content: prompt, }, ]; const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 1500, }); return response.content; } catch (error) { throw new Error(`Failed to generate test setup: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateTestTeardown( language: string, framework: string ): Promise { try { const prompt = this.createTestTeardownPrompt(language, framework); const messages = [ { role: 'system' as const, content: 'You are an expert at writing test teardown code. Generate proper cleanup code that ensures tests don\'t interfere with each other.', }, { role: 'user' as const, content: prompt, }, ]; const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 1000, }); return response.content; } catch (error) { throw new Error(`Failed to generate test teardown: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async analyzeTestCoverage( sourceFiles: CodeBlock[], testFiles: CodeBlock[] ): Promise<{ readonly totalLines: number; readonly coveredLines: number; readonly coveragePercentage: number; readonly uncoveredLines: Array<{ readonly file: string; readonly line: number; readonly content: string; }>; }> { // This is a simplified implementation // In a real implementation, you'd use actual coverage tools const totalLines = sourceFiles.reduce((sum, file) => sum + file.endLine - file.startLine + 1, 0); const coveredLines = Math.floor(totalLines * 0.8); // Assume 80% coverage for demo const coveragePercentage = (coveredLines / totalLines) * 100; const uncoveredLines: Array<{ readonly file: string; readonly line: number; readonly content: string; }> = []; // Simple heuristic: assume some lines are uncovered for (const file of sourceFiles) { const lines = file.content.split('\n'); for (let i = 0; i < lines.length; i += 5) { // Every 5th line is uncovered uncoveredLines.push({ file: file.filePath, line: file.startLine + i, content: lines[i] || '', }); } } return { totalLines, coveredLines, coveragePercentage, uncoveredLines, }; } async suggestMissingTestCases( codeBlock: CodeBlock, existingTests: CodeBlock[] ): Promise { try { const prompt = this.createMissingTestCasesPrompt(codeBlock, existingTests); const messages = [ { role: 'system' as const, content: 'You are an expert at analyzing test coverage. Identify missing test cases and edge cases that should be covered by tests.', }, { role: 'user' as const, content: prompt, }, ]; const response = await this.llmPort.generateResponse(messages, { temperature: 0.4, maxTokens: 2000, }); return this.parseTestSuggestions(response.content, codeBlock, { framework: 'jest', testType: 'unit', language: codeBlock.language, }); } catch (error) { throw new Error(`Failed to suggest missing test cases: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async validateTestQuality(testCode: string): Promise<{ readonly isValid: boolean; readonly issues: Array<{ readonly type: 'error' | 'warning' | 'suggestion'; readonly message: string; readonly line?: number; }>; }> { try { const prompt = this.createTestQualityValidationPrompt(testCode); const messages = [ { role: 'system' as const, content: 'You are an expert at reviewing test code quality. Analyze the test code and identify issues, best practices violations, and suggestions for improvement.', }, { role: 'user' as const, content: prompt, }, ]; const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 1500, }); return this.parseQualityValidation(response.content); } catch (error) { return { isValid: false, issues: [{ type: 'error', message: `Failed to validate test quality: ${error instanceof Error ? error.message : 'Unknown error'}`, }], }; } } private createTestGenerationPrompt(codeBlock: CodeBlock, options: TestGeneratorOptions): string { const frameworkInstructions = this.getFrameworkInstructions(options.language, options.framework); return ` Generate comprehensive ${options.testType} tests for the following ${options.language} code using ${options.framework}: File: ${codeBlock.filePath} Lines: ${codeBlock.startLine}-${codeBlock.endLine} Code: \`\`\`${codeBlock.language} ${codeBlock.content} \`\`\` Requirements: - Framework: ${options.framework} - Test Type: ${options.testType} - Language: ${options.language} - Include edge cases and error conditions - Cover normal usage scenarios - ${options.includeSetup ? 'Include setup code' : 'No setup code needed'} - ${options.includeTeardown ? 'Include teardown code' : 'No teardown code needed'} - Target coverage: ${options.coverageTarget || 80}% ${frameworkInstructions} Please provide the test code in JSON format: { "testCode": "complete test code in the correct language and framework${options.language === 'dart' ? ' - MUST be actual Dart code using Flutter test framework' : ''}", "setupCode": "${options.includeSetup ? 'setup code if needed' : ''}", "testCases": [ { "name": "test case name", "description": "what this test does", "input": {"param1": "value1"}, "expectedOutput": "expected result", "expectedBehavior": "what should happen" } ], "description": "overall description of the tests" } ${options.language === 'dart' ? 'CRITICAL: For Dart code, the testCode field MUST contain actual Dart code using Flutter test framework, NOT pseudocode or shell scripts!' : ''} `; } private getFrameworkInstructions(language: string, framework: string): string { switch (language.toLowerCase()) { case 'dart': return ` CRITICAL: This is Dart code for a Flutter application. You MUST generate actual Dart test code using Flutter's testing framework: REQUIRED IMPORTS: import 'package:flutter_test/flutter_test.dart'; REQUIRED STRUCTURE: group('TestGroupName', () { test('test description', () { // Arrange // Act // Assert expect(actual, expected); }); }); IMPORTANT RULES: - Generate actual Dart code, NOT pseudocode or generic test format - Use Flutter's test() and group() functions - Use expect() for assertions with proper Dart syntax - Use proper Dart variable declarations (String, int, bool, etc.) - Use proper Dart string interpolation with \${variable} - For functions, test the actual function calls - Use proper Dart null safety (String? for nullable types) EXAMPLE FOR debugPrint: group('debugPrint tests', () { test('should print key with prefix', () { String key = 'testKey'; String expectedOutput = 'key: \$key'; // Mock debugPrint to capture output String? capturedOutput; debugPrint = (String? message) { capturedOutput = message; }; debugPrint('key: \$key'); expect(capturedOutput, expectedOutput); }); }); GENERATE ACTUAL DART CODE, NOT PSEUDOCODE!`; case 'python': return ` IMPORTANT: This is Python code. Generate tests using pytest: - Use pytest syntax with test_ functions - Use assert statements - Use pytest fixtures for setup - Example structure: def test_function_name(): # Arrange # Act # Assert assert result == expected_value`; case 'java': return ` IMPORTANT: This is Java code. Generate tests using JUnit: - Use @Test annotations - Use assertEquals, assertTrue, assertFalse - Use @BeforeEach and @AfterEach for setup/teardown - Example structure: @Test void testMethodName() { // Arrange // Act // Assert assertEquals(expected, actual); }`; case 'csharp': return ` IMPORTANT: This is C# code. Generate tests using NUnit or MSTest: - Use [Test] attributes - Use Assert.AreEqual, Assert.IsTrue, etc. - Use [SetUp] and [TearDown] for setup/teardown - Example structure: [Test] public void TestMethodName() { // Arrange // Act // Assert Assert.AreEqual(expected, actual); }`; default: return ` IMPORTANT: Generate tests using the appropriate testing framework for ${language}: - Use the standard testing conventions for ${language} - Follow ${language} best practices - Use proper assertions and test structure`; } } private createTestSetupPrompt(language: string, framework: string, dependencies: string[]): string { return ` Generate test setup code for ${language} using ${framework} framework. Dependencies: ${dependencies.join(', ')} Include: - Proper imports and requires - Test configuration - Mock setup if needed - Helper functions - Common test utilities Provide clean, production-ready setup code. `; } private createTestTeardownPrompt(language: string, framework: string): string { return ` Generate test teardown code for ${language} using ${framework} framework. Include: - Cleanup of resources - Reset of mocks - Database cleanup if applicable - File system cleanup if applicable - Memory cleanup Provide clean, production-ready teardown code. `; } private createMissingTestCasesPrompt(codeBlock: CodeBlock, existingTests: CodeBlock[]): string { const existingTestContent = existingTests.map(test => test.content).join('\n'); return ` Analyze the following code and existing tests to identify missing test cases: Source Code: \`\`\`${codeBlock.language} ${codeBlock.content} \`\`\` Existing Tests: \`\`\` ${existingTestContent} \`\`\` Identify: 1. Missing edge cases 2. Uncovered error conditions 3. Boundary value tests 4. Integration scenarios 5. Performance tests if applicable Provide suggestions in JSON format: [ { "testCode": "test code for missing case", "description": "what this test covers", "reason": "why this test is important" } ] `; } private createTestQualityValidationPrompt(testCode: string): string { return ` Analyze the quality of this test code and identify issues: \`\`\` ${testCode} \`\`\` Check for: 1. Test structure and organization 2. Naming conventions 3. Assertion quality 4. Test isolation 5. Code coverage 6. Best practices 7. Maintainability Provide feedback in JSON format: { "isValid": true/false, "issues": [ { "type": "error|warning|suggestion", "message": "description of the issue", "line": 42 } ] } `; } private parseTestSuggestions(response: string, codeBlock: CodeBlock, options: TestGeneratorOptions): TestSuggestion[] { try { const parsed = JSON.parse(response); if (parsed.testCode) { // Single test suggestion const suggestion = new TestSuggestionImpl() .id(`test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) .testType(options.testType) .framework(options.framework) .description(parsed.description || 'Generated test') .testCode(parsed.testCode) .targetFile(codeBlock.filePath); if (parsed.setupCode) { suggestion.setupCode(parsed.setupCode); } if (parsed.testCases && Array.isArray(parsed.testCases)) { suggestion.testCases(parsed.testCases); } return [suggestion.build()]; } else if (Array.isArray(parsed)) { // Multiple test suggestions return parsed.map((item, index) => { const suggestion = new TestSuggestionImpl() .id(`test-${Date.now()}-${index}`) .testType(options.testType) .framework(options.framework) .description(item.description || 'Generated test') .testCode(item.testCode) .targetFile(codeBlock.filePath); if (item.testCases && Array.isArray(item.testCases)) { suggestion.testCases(item.testCases); } return suggestion.build(); }); } } catch (error) { // If JSON parsing fails, create a basic test suggestion const suggestion = new TestSuggestionImpl() .id(`test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) .testType(options.testType) .framework(options.framework) .description('Generated test') .testCode(response) .targetFile(codeBlock.filePath) .testCases([]) .build(); return [suggestion]; } return []; } private parseQualityValidation(response: string): { readonly isValid: boolean; readonly issues: Array<{ readonly type: 'error' | 'warning' | 'suggestion'; readonly message: string; readonly line?: number; }>; } { try { const parsed = JSON.parse(response); return { isValid: parsed.isValid || false, issues: parsed.issues || [], }; } catch (error) { return { isValid: false, issues: [{ type: 'error', message: 'Failed to parse quality validation response', }], }; } } }