| |
| |
| |
|
|
|
|
| import {
|
| TestOutput,
|
| ExpectedResult,
|
| TestScore,
|
| TestResult,
|
| TestCase,
|
| IssueType,
|
| ConfidenceLevel
|
| } from './types'
|
|
|
| export interface EvaluationOptions {
|
| strictConfidence?: boolean
|
| strictIssueType?: boolean
|
| allowPartialMatches?: boolean
|
| }
|
|
|
| export class Evaluator {
|
| private options: EvaluationOptions
|
|
|
| constructor(options: EvaluationOptions = {}) {
|
| this.options = {
|
| strictConfidence: false,
|
| strictIssueType: false,
|
| allowPartialMatches: true,
|
| ...options
|
| }
|
| }
|
|
|
| |
| |
|
|
| evaluate(testCase: TestCase, output: TestOutput): TestResult {
|
| const startTime = Date.now()
|
| const score = this.calculateScore(output, testCase.expected)
|
|
|
| return {
|
| testName: testCase.name,
|
| testPath: testCase.path,
|
| category: testCase.category,
|
| passed: score.overall,
|
| score,
|
| output,
|
| expected: testCase.expected,
|
| executionTime: output.executionTime,
|
| timestamp: new Date().toISOString()
|
| }
|
| }
|
|
|
| |
| |
|
|
| calculateScore(output: TestOutput, expected: ExpectedResult): TestScore {
|
|
|
| const detectionMatch = expected.shouldDetectIssue === output.detectedIssues
|
|
|
|
|
| let typeMatch = true
|
| if (expected.shouldDetectIssue && expected.issueType && output.issueType) {
|
| typeMatch = this.matchIssueType(expected.issueType, output.issueType)
|
| }
|
|
|
|
|
| let confidenceMatch = true
|
| if (expected.expectedConfidence && output.confidence) {
|
| confidenceMatch = this.matchConfidence(
|
| expected.expectedConfidence,
|
| output.confidence
|
| )
|
| }
|
|
|
|
|
| const autoPRMatch = expected.shouldAutoPR === output.autoPRTriggered
|
|
|
|
|
| let suggestionCountMatch = true
|
| if (expected.expectedSuggestions) {
|
| suggestionCountMatch =
|
| expected.expectedSuggestions.length === output.suggestions.length
|
| }
|
|
|
|
|
| const allChecks = [
|
| detectionMatch,
|
| typeMatch,
|
| confidenceMatch,
|
| autoPRMatch,
|
| suggestionCountMatch
|
| ]
|
|
|
|
|
| const overall =
|
| detectionMatch &&
|
| (typeMatch || !this.options.strictIssueType) &&
|
| (confidenceMatch || !this.options.strictConfidence) &&
|
| autoPRMatch
|
|
|
| return {
|
| detection: detectionMatch,
|
| classification: typeMatch,
|
| confidence: confidenceMatch,
|
| autoPR: autoPRMatch,
|
| overall,
|
| details: {
|
| detectionMatch,
|
| typeMatch,
|
| confidenceMatch,
|
| autoPRMatch,
|
| suggestionCountMatch
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| private matchIssueType(expected: IssueType, actual: IssueType): boolean {
|
| if (expected === actual) return true
|
|
|
| if (!this.options.strictIssueType) {
|
|
|
| const securityTypes: IssueType[] = ['security', 'bug']
|
| const bugTypes: IssueType[] = ['bug', 'mutation']
|
|
|
| if (securityTypes.includes(expected) && securityTypes.includes(actual))
|
| return true
|
| if (bugTypes.includes(expected) && bugTypes.includes(actual)) return true
|
| }
|
|
|
| return false
|
| }
|
|
|
| |
| |
|
|
| private matchConfidence(
|
| expected: ConfidenceLevel,
|
| actual: ConfidenceLevel
|
| ): boolean {
|
| if (expected === actual) return true
|
|
|
| if (!this.options.strictConfidence) {
|
|
|
| const levels: ConfidenceLevel[] = ['low', 'medium', 'high']
|
| const expectedIdx = levels.indexOf(expected)
|
| const actualIdx = levels.indexOf(actual)
|
|
|
|
|
| return Math.abs(expectedIdx - actualIdx) <= 1
|
| }
|
|
|
| return false
|
| }
|
|
|
| |
| |
|
|
| generateReport(result: TestResult): string {
|
| const lines = [
|
| `\n${'='.repeat(60)}`,
|
| `Test: ${result.testName} (${result.category})`,
|
| `${'='.repeat(60)}`,
|
| `Status: ${result.passed ? 'β
PASS' : 'β FAIL'}`,
|
| `Execution Time: ${result.executionTime}ms`,
|
| '',
|
| '--- Expected ---',
|
| ` Detect Issue: ${result.expected.shouldDetectIssue}`,
|
| ` Issue Type: ${result.expected.issueType || 'any'}`,
|
| ` Confidence: ${result.expected.expectedConfidence || 'any'}`,
|
| ` Auto-PR: ${result.expected.shouldAutoPR}`,
|
| '',
|
| '--- Actual ---',
|
| ` Detected: ${result.output.detectedIssues}`,
|
| ` Type: ${result.output.issueType || 'none'}`,
|
| ` Confidence: ${result.output.confidence || 'none'}`,
|
| ` Auto-PR: ${result.output.autoPRTriggered}`,
|
| ` Suggestions: ${result.output.suggestions.length}`,
|
| ` Comments: ${result.output.comments.length}`,
|
| '',
|
| '--- Score Breakdown ---',
|
| ` Detection: ${result.score.detection ? 'β
' : 'β'}`,
|
| ` Classification: ${result.score.classification ? 'β
' : 'β'}`,
|
| ` Confidence: ${result.score.confidence ? 'β
' : 'β'}`,
|
| ` Auto-PR: ${result.score.autoPR ? 'β
' : 'β'}`,
|
| '',
|
| '--- Errors ---',
|
| ...(result.output.errors.length > 0
|
| ? result.output.errors.map(e => ` β ${e}`)
|
| : [' None'])
|
| ]
|
|
|
| return lines.join('\n')
|
| }
|
|
|
| |
| |
|
|
| compareResults(
|
| baseline: TestResult,
|
| current: TestResult
|
| ): {regressed: boolean; changes: string[]} {
|
| const changes: string[] = []
|
|
|
| if (baseline.passed && !current.passed) {
|
| changes.push(`Test ${current.testName} started failing`)
|
| }
|
|
|
| if (!baseline.passed && current.passed) {
|
| changes.push(`Test ${current.testName} now passing (improvement)`)
|
| }
|
|
|
| if (baseline.score.overall && !current.score.overall) {
|
| changes.push(`Overall score degraded for ${current.testName}`)
|
| }
|
|
|
| return {
|
| regressed: changes.some(
|
| c => c.includes('failing') || c.includes('degraded')
|
| ),
|
| changes
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| export function evaluate(
|
| output: TestOutput,
|
| expected: ExpectedResult,
|
| options?: EvaluationOptions
|
| ): TestScore {
|
| const evaluator = new Evaluator(options)
|
| return evaluator.calculateScore(output, expected)
|
| }
|
|
|