PRIX / src /test-generator.ts
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
10.5 kB
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()