import {Bot} from './bot' /* eslint-disable no-console */ const logInfo = console.log /* eslint-enable no-console */ export interface TestGenerationResult { testCase: string testFile: string language: string testType: 'unit' | 'integration' | 'e2e' confidence: number } export interface BugAnalysis { bugType: string severity: string functionName: string parameters: string[] expectedBehavior: string actualBehavior: string } export class TestGenerator { private readonly MIN_CONFIDENCE = 0.75 async generateTestCase( filename: string, bugComment: string, remedy: string, heavyBot: Bot ): Promise { try { const bugAnalysis = this.parseBugComment(bugComment) if (!bugAnalysis) { logInfo('Could not parse bug comment for test generation') return null } const ext = filename.split('.').pop()?.toLowerCase() const language = this.getLanguage(ext) const [testCaseResponse] = await heavyBot.chat( this.buildTestPrompt(filename, bugAnalysis, remedy, language), {} ) if (!testCaseResponse || testCaseResponse.length < 20) { return null } const testCase = this.extractTestCode(testCaseResponse, language) if (!testCase) { return null } const testFile = this.findTestFile(filename) const testType = this.inferTestType(bugAnalysis) const confidence = this.estimateConfidence(bugAnalysis) if (confidence < this.MIN_CONFIDENCE) { logInfo(`Test generation confidence ${confidence} below threshold`) return null } return { testCase, testFile, language, testType, confidence } } catch (e) { logInfo(`Test generation failed: ${e}`) return null } } private parseBugComment(comment: string): BugAnalysis | null { try { const bugType = this.extractBugType(comment) const severity = this.extractSeverity(comment) const functionName = this.extractFunctionName(comment) const parameters = this.extractParameters(comment) const {expected, actual} = this.extractBehavior(comment) if (!bugType && !functionName) { return null } return { bugType: bugType || 'general', severity: severity || 'minor', functionName: functionName || '', parameters, expectedBehavior: expected || '', actualBehavior: actual || '' } } catch (e) { return null } } private extractBugType(comment: string): string | null { const patterns = [ /null\s*pointer|NPE|null\s*check/i, /division\s*by\s*zero|divide\s*by\s*0/i, /race\s*condition|concurrent|deadlock/i, /memory\s*leak|dispose|resource/i, /sql\s*injection|injection/i, /xss|cross.*site|innerHTML/i, /async.*await|promise/i, /off\s*by\s*one|index.*bound/i, /type.*error|cannot\s*read|i[is] undefined/i, /security|vulnerability|auth/i ] const keywords = [ 'null pointer', 'division by zero', 'race condition', 'memory leak', 'sql injection', 'xss', 'async await', 'off by one', 'type error', 'security vulnerability' ] const lowerComment = comment.toLowerCase() for (let i = 0; i < patterns.length; i++) { if (patterns[i].test(lowerComment)) { return keywords[i] } } return null } private extractSeverity(comment: string): string | null { if (/\[CRITICAL\]|critical|severe|p0|critical/i.test(comment)) return 'critical' if (/\[MAJOR\]|major|important|p1/i.test(comment)) return 'major' if (/\[MINOR\]|minor|p2/i.test(comment)) return 'minor' return 'info' } private extractFunctionName(comment: string): string | null { const patterns = [ /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/i, /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/, /method\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/i, /`([a-zA-Z_$][a-zA-Z0-9_$]*)`/ ] for (const pattern of patterns) { const match = comment.match(pattern) if (match && match[1]) { return match[1] } } return null } private extractParameters(comment: string): string[] { const params: string[] = [] const paramPatterns = [ /`(.*?)`/g, /parameter[s]?:\s*([a-zA-Z0-9_$,\s]+)/gi, /arg[s]?\s+([a-zA-Z0-9_$,\s]+)/gi ] for (const pattern of paramPatterns) { let match while ((match = pattern.exec(comment)) !== null) { if (match[1]) { const parts = match[1].split(/[,\s]+/).filter(p => p.length > 0) params.push(...parts) } } } return [...new Set(params)].slice(0, 5) } private extractBehavior(comment: string): {expected: string; actual: string} { let expected = '' let actual = '' const expectedMatch = comment.match(/expected[:\s]+([^.]+)/i) if (expectedMatch) expected = expectedMatch[1].trim() const actualMatch = comment.match(/(?:actual|got|but\s*got)[:\s]+([^.]+)/i) if (actualMatch) actual = actualMatch[1].trim() const returnMatch = comment.match(/returns?\s+([^.]+)/i) if (returnMatch && !actual) actual = returnMatch[1].trim() return {expected, actual} } private getLanguage(ext: string | undefined): string { const map: Record = { ts: 'TypeScript', tsx: 'TypeScript', js: 'JavaScript', jsx: 'JavaScript', py: 'Python', go: 'Go', java: 'Java', rs: 'Rust', rb: 'Ruby', cs: 'C#' } return map[ext || ''] || 'JavaScript' } private findTestFile(sourceFile: string): string { const testPatterns = [ { pattern: /^(src|lib|app)\/(.+)\.(ts|js|py|go)$/, replacement: '$1/$2.test.$3' }, { pattern: /^(src|lib|app)\/(.+)\.(ts|js|py|go)$/, replacement: '$1/$2.spec.$3' }, {pattern: /^(.+)\.(ts|js|py|go)$/, replacement: '$1.test.$2'}, {pattern: /^(.+)\.(ts|js|py|go)$/, replacement: '$1.spec.$2'} ] for (const {pattern, replacement} of testPatterns) { const testFile = sourceFile.replace(pattern, replacement) if (testFile !== sourceFile) { return testFile } } const ext = sourceFile.split('.').pop() return sourceFile.replace(`.${ext}`, `.test.${ext}`) } private inferTestType(bug: BugAnalysis): 'unit' | 'integration' | 'e2e' { const dbPatterns = /database|query|sql|crud|repository/i const apiPatterns = /api|endpoint|http|fetch|request/i const uiPatterns = /click|render|component|ui|button/i const content = `${bug.bugType} ${bug.functionName}`.toLowerCase() if (dbPatterns.test(content)) return 'integration' if (apiPatterns.test(content)) return 'integration' if (uiPatterns.test(content)) return 'e2e' return 'unit' } private estimateConfidence(bug: BugAnalysis): number { let confidence = 0.3 if (!bug.functionName || !bug.bugType) { return confidence } confidence = 0.5 if (bug.functionName) confidence += 0.15 if (bug.bugType) confidence += 0.1 if (bug.expectedBehavior && bug.actualBehavior) confidence += 0.15 if (bug.parameters.length > 0) confidence += 0.05 if (bug.bugType === 'null pointer' || bug.bugType === 'division by zero') { confidence += 0.1 } if ( bug.bugType === 'sql injection' || bug.bugType === 'security vulnerability' ) { confidence += 0.15 } return Math.min(1, confidence) } private buildTestPrompt( filename: string, bug: BugAnalysis, remedy: string, language: string ): string { return `Generate a minimal unit test that reproduces a bug in ${filename}. Bug Analysis: - Type: ${bug.bugType} - Severity: ${bug.severity} - Function: ${bug.functionName} - Parameters: ${bug.parameters.join(', ') || 'unknown'} - Expected behavior: ${bug.expectedBehavior || 'correct handling'} - Actual behavior: ${bug.actualBehavior || 'incorrect handling'} Remediation applied: \`\`\` ${remedy} \`\`\` Requirements: 1. Generate ONLY the test code, no explanations 2. The test should FAIL before the fix and PASS after the fix 3. Include realistic test data that triggers the bug 4. Use ${language} testing conventions 5. Keep it minimal - just enough to reproduce the issue 6. Include a comment explaining what edge case is being tested Format: Return ONLY the test code in a code block labeled with the language.` } private extractTestCode(response: string, language: string): string | null { const patterns = [ new RegExp(`\`\`\`${language.toLowerCase()}[\\s\\S]*?\`\`\``), new RegExp(`\`\`\`${language}[\\s\\S]*?\`\`\``), new RegExp('```[a-z]*\\n([\\s\\S]*?)```') ] for (const pattern of patterns) { const match = response.match(pattern) if (match) { return match[1].trim() } } const codeBlockMatch = response.match(/```[\s\S]*?\n([\s\S]*?)```/) if (codeBlockMatch) { return codeBlockMatch[1].trim() } return response.trim().length > 50 ? response.trim() : null } formatTestSuggestion(result: TestGenerationResult): string { const confidenceLabel = result.confidence >= 0.8 ? 'High' : result.confidence >= 0.6 ? 'Medium' : 'Low' const autoGenWarning = `// ⚠️ AUTO-GENERATED BY PRIX - REVIEW BEFORE COMMIT // DO NOT COMMIT WITHOUT UNDERSTANDING THIS TEST ` return `
🧪 Auto-Generated Test Case (${confidenceLabel} confidence) **Test File:** \`${result.testFile}\` **Type:** ${result.testType} **Confidence:** ${Math.round(result.confidence * 100)}% \`\`\`${result.language.toLowerCase()} ${autoGenWarning}${result.testCase} \`\`\` > ⚠️ Review this test carefully before committing. It was auto-generated based on the bug analysis.
` } } export const testGenerator = new TestGenerator()