| "use strict"; |
| |
| |
| |
| |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| var desc = Object.getOwnPropertyDescriptor(m, k); |
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { |
| desc = { enumerable: true, get: function() { return m[k]; } }; |
| } |
| Object.defineProperty(o, k2, desc); |
| }) : (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| o[k2] = m[k]; |
| })); |
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { |
| Object.defineProperty(o, "default", { enumerable: true, value: v }); |
| }) : function(o, v) { |
| o["default"] = v; |
| }); |
| var __importStar = (this && this.__importStar) || (function () { |
| var ownKeys = function(o) { |
| ownKeys = Object.getOwnPropertyNames || function (o) { |
| var ar = []; |
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; |
| return ar; |
| }; |
| return ownKeys(o); |
| }; |
| return function (mod) { |
| if (mod && mod.__esModule) return mod; |
| var result = {}; |
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); |
| __setModuleDefault(result, mod); |
| return result; |
| }; |
| })(); |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.TestRunner = void 0; |
| exports.runTestCase = runTestCase; |
| const fs = __importStar(require("fs")); |
| const path = __importStar(require("path")); |
| const context_1 = require("../context"); |
| const commenter_1 = require("../commenter"); |
| const octokit_1 = require("../octokit"); |
| const review_1 = require("../review"); |
| const options_1 = require("../options"); |
| const prompts_1 = require("../prompts"); |
| const mock_github_1 = require("./mock-github"); |
| class TestRunner { |
| options; |
| currentTest = null; |
| constructor(options) { |
| this.options = { |
| verbose: false, |
| skipAutoPR: true, |
| mockAI: false, |
| ...options |
| }; |
| |
| if (!fs.existsSync(this.options.outputDir)) { |
| fs.mkdirSync(this.options.outputDir, { recursive: true }); |
| } |
| } |
| |
| |
| |
| loadTestCases() { |
| const tests = []; |
| const categories = fs |
| .readdirSync(this.options.testsDir) |
| .filter(f => fs.statSync(path.join(this.options.testsDir, f)).isDirectory()); |
| for (const category of categories) { |
| const categoryPath = path.join(this.options.testsDir, category); |
| const testDirs = fs |
| .readdirSync(categoryPath) |
| .filter(f => fs.statSync(path.join(categoryPath, f)).isDirectory()); |
| for (const testDir of testDirs) { |
| const testPath = path.join(categoryPath, testDir); |
| const expectedPath = path.join(testPath, 'expected.json'); |
| if (!fs.existsSync(expectedPath)) { |
| console.warn(`⚠️ Skipping ${testDir}: no expected.json found`); |
| continue; |
| } |
| const expected = JSON.parse(fs.readFileSync(expectedPath, 'utf8')); |
| tests.push({ |
| name: testDir, |
| path: testPath, |
| category, |
| basePath: path.join(testPath, 'base'), |
| headPath: path.join(testPath, 'head'), |
| expected |
| }); |
| } |
| } |
| return tests; |
| } |
| |
| |
| |
| async runTestCase(testCase) { |
| const startTime = Date.now(); |
| this.currentTest = testCase.name; |
| console.log(`\n🧪 Running test: ${testCase.category}/${testCase.name}`); |
| try { |
| |
| const files = this.loadDiffFiles(testCase); |
| if (files.length === 0) { |
| throw new Error('No files found in base/head directories'); |
| } |
| |
| const diffFiles = this.generateDiffs(files); |
| |
| const mockContext = { |
| number: 1, |
| title: `[EVAL] ${testCase.name}`, |
| body: 'Test PR for evaluation', |
| base: { ref: testCase.basePath, sha: 'base-sha' }, |
| head: { ref: testCase.headPath, sha: 'head-sha' }, |
| files: diffFiles |
| }; |
| |
| const logFile = path.join(this.options.outputDir, `${testCase.name}.log`); |
| const mockLayer = new mock_github_1.MockGitHubLayer(mockContext, { logFile }); |
| const probotContext = mockLayer.createMockProbotContext(); |
| |
| (0, mock_github_1.suppressGitHubCalls)(); |
| |
| const mockOctokit = mockLayer.createMockOctokit(); |
| const store = { |
| probotContext, |
| octokit: mockOctokit, |
| repo: { owner: 'mock-owner', repo: 'mock-repo' }, |
| workingDir: testCase.headPath |
| }; |
| let output = { |
| detectedIssues: false, |
| autoPRTriggered: false, |
| suggestions: [], |
| comments: [], |
| errors: [], |
| executionTime: 0, |
| changedLines: 0 |
| }; |
| |
| await context_1.als.run(store, async () => { |
| |
| (0, commenter_1.setCommenterContext)(probotContext); |
| (0, octokit_1.setOctokit)(probotContext.octokit); |
| |
| const options = new options_1.Options(this.options.verbose || false, |
| false, |
| true, |
| '0', |
| false, |
| false, |
| null, |
| '', |
| 'openai/gpt-oss-120b:free', |
| 'openai/gpt-oss-120b:free', |
| '0.0', |
| '3', |
| '120000', |
| '6', |
| '6', |
| 'https://openrouter.ai/api/v1', |
| 'en-US', |
| false, |
| !this.options.skipAutoPR, |
| false |
| ); |
| const prompts = new prompts_1.Prompts(); |
| try { |
| |
| await (0, review_1.run)(probotContext, options, prompts); |
| |
| const comments = mockLayer.getComments(); |
| const prs = mockLayer.getCreatedPRs(); |
| |
| const changedLines = diffFiles.reduce((sum, f) => { |
| return (sum + |
| f.diff |
| .split('\n') |
| .filter(l => l.startsWith('+') || l.startsWith('-')).length); |
| }, 0); |
| |
| const suggestions = this.parseSuggestionsFromComments(comments); |
| output = { |
| detectedIssues: comments.length > 0 || suggestions.length > 0, |
| issueType: this.inferIssueType(comments), |
| confidence: this.inferConfidence(comments), |
| autoPRTriggered: prs.length > 0, |
| suggestions, |
| comments, |
| errors: [], |
| executionTime: Date.now() - startTime, |
| changedLines |
| }; |
| } |
| catch (err) { |
| output.errors.push(err.message); |
| console.error(`❌ Pipeline error: ${err.message}`); |
| } |
| }); |
| return output; |
| } |
| catch (err) { |
| return { |
| detectedIssues: false, |
| autoPRTriggered: false, |
| suggestions: [], |
| comments: [], |
| errors: [err.message], |
| executionTime: Date.now() - startTime, |
| changedLines: 0 |
| }; |
| } |
| } |
| |
| |
| |
| loadDiffFiles(testCase) { |
| const files = []; |
| if (!fs.existsSync(testCase.headPath)) { |
| return files; |
| } |
| const headFiles = this.getAllFiles(testCase.headPath); |
| for (const relPath of headFiles) { |
| const headFile = path.join(testCase.headPath, relPath); |
| const baseFile = path.join(testCase.basePath, relPath); |
| const headContent = fs.readFileSync(headFile, 'utf8'); |
| const baseContent = fs.existsSync(baseFile) |
| ? fs.readFileSync(baseFile, 'utf8') |
| : ''; |
| files.push({ |
| filename: relPath, |
| base: baseContent, |
| head: headContent |
| }); |
| } |
| return files; |
| } |
| |
| |
| |
| getAllFiles(dir, basePath = '') { |
| const files = []; |
| const items = fs.readdirSync(dir); |
| for (const item of items) { |
| const fullPath = path.join(dir, item); |
| const relPath = basePath ? path.join(basePath, item) : item; |
| if (fs.statSync(fullPath).isDirectory()) { |
| files.push(...this.getAllFiles(fullPath, relPath)); |
| } |
| else { |
| files.push(relPath); |
| } |
| } |
| return files; |
| } |
| |
| |
| |
| generateDiffs(files) { |
| return files.map(f => { |
| const diff = this.createUnifiedDiff(f.filename, f.base, f.head); |
| return { |
| filename: f.filename, |
| baseContent: f.base, |
| headContent: f.head, |
| diff |
| }; |
| }); |
| } |
| |
| |
| |
| createUnifiedDiff(filename, base, head) { |
| const baseLines = base.split('\n'); |
| const headLines = head.split('\n'); |
| let diff = `--- a/${filename}\n+++ b/${filename}\n`; |
| |
| const maxLen = Math.max(baseLines.length, headLines.length); |
| for (let i = 0; i < maxLen; i++) { |
| const baseLine = baseLines[i] || ''; |
| const headLine = headLines[i] || ''; |
| if (baseLine !== headLine) { |
| if (baseLine !== undefined) { |
| diff += `-${baseLine}\n`; |
| } |
| if (headLine !== undefined) { |
| diff += `+${headLine}\n`; |
| } |
| } |
| else { |
| diff += ` ${baseLine}\n`; |
| } |
| } |
| return diff; |
| } |
| |
| |
| |
| parseSuggestionsFromComments(comments) { |
| const suggestions = []; |
| for (const comment of comments) { |
| |
| const suggestionMatch = comment.body.match(/```suggestion\n([\s\S]*?)\n```/); |
| if (suggestionMatch) { |
| suggestions.push({ |
| filename: comment.path, |
| startLine: comment.line || 1, |
| endLine: comment.line || 1, |
| suggestion: suggestionMatch[1].trim() |
| }); |
| } |
| } |
| return suggestions; |
| } |
| |
| |
| |
| inferIssueType(comments) { |
| const types = ['security', 'bug', 'performance', 'style']; |
| const text = comments |
| .map(c => c.body) |
| .join(' ') |
| .toLowerCase(); |
| for (const type of types) { |
| if (text.includes(type)) |
| return type; |
| } |
| return 'style'; |
| } |
| |
| |
| |
| inferConfidence(comments) { |
| const text = comments |
| .map(c => c.body) |
| .join(' ') |
| .toLowerCase(); |
| if (text.includes('high confidence') || text.includes('critical')) |
| return 'high'; |
| if (text.includes('low confidence') || text.includes('minor')) |
| return 'low'; |
| return 'medium'; |
| } |
| } |
| exports.TestRunner = TestRunner; |
| |
| |
| |
| async function runTestCase(testPath, options) { |
| const expectedPath = path.join(testPath, 'expected.json'); |
| if (!fs.existsSync(expectedPath)) { |
| throw new Error(`No expected.json found at ${testPath}`); |
| } |
| const expected = JSON.parse(fs.readFileSync(expectedPath, 'utf8')); |
| const testCase = { |
| name: path.basename(testPath), |
| path: testPath, |
| category: path.basename(path.dirname(testPath)), |
| basePath: path.join(testPath, 'base'), |
| headPath: path.join(testPath, 'head'), |
| expected |
| }; |
| const runner = new TestRunner({ |
| testsDir: path.dirname(path.dirname(testPath)), |
| outputDir: path.join(testPath, '..', '..', 'output'), |
| ...options |
| }); |
| return runner.runTestCase(testCase); |
| } |
| |