Spaces:
Paused
Paused
| 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<TestGenerationResult | null> { | |
| 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<string, string> = { | |
| 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 ` | |
| <details> | |
| <summary>🧪 Auto-Generated Test Case (${confidenceLabel} confidence)</summary> | |
| **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. | |
| </details> | |
| ` | |
| } | |
| } | |
| export const testGenerator = new TestGenerator() | |