"use strict"; /** * VisualRegressionCollector — captures screenshots and compares against baselines. * Uses pixelmatch for pixel-level comparison and sharp for image normalization. */ 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.VisualRegressionCollector = exports.DEFAULT_VISUAL_CONFIG = void 0; exports.compareScreenshots = compareScreenshots; const crypto = __importStar(require("crypto")); const path = __importStar(require("path")); const fs = __importStar(require("fs")); exports.DEFAULT_VISUAL_CONFIG = { enabled: true, threshold: 0.001, screenshotFullPage: false, ignoreSelectors: [], }; async function compareScreenshots(baselinePath, currentPath, diffOutputPath, threshold = 0.1) { // Dynamic imports to avoid loading heavy deps at startup const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default; const pixelmatch = (await Promise.resolve().then(() => __importStar(require('pixelmatch')))).default; const [baselineRaw, currentRaw] = await Promise.all([ sharp(baselinePath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }), sharp(currentPath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }), ]); const { width, height } = baselineRaw.info; const diffBuffer = Buffer.alloc(width * height * 4); const diffPixels = pixelmatch(baselineRaw.data, currentRaw.data, diffBuffer, width, height, { threshold }); const totalPixels = width * height; const diffPercent = totalPixels > 0 ? diffPixels / totalPixels : 0; // Write diff image await sharp(diffBuffer, { raw: { width, height, channels: 4 } }) .png() .toFile(diffOutputPath); return { diffPixels, diffPercent, hasDiff: diffPixels > 0 }; } class VisualRegressionCollector { constructor(outputDir, repo, config = {}) { this.outputDir = outputDir; this.repo = repo; this.config = { ...exports.DEFAULT_VISUAL_CONFIG, ...config }; } /** * Process a screenshot for visual regression. * Returns an anomaly if a regression is detected, otherwise null. */ async processScreenshot(screenshotPath, state, sessionId, actionTrace) { if (!this.config.enabled) return null; const comparisonId = crypto.randomUUID(); const baseline = this.repo.findBaselineByStateId(state.id); if (!baseline) { // No baseline: create a new_state comparison record this.repo.createComparison({ id: comparisonId, sessionId, stateId: state.id, currentScreenshotPath: screenshotPath, status: 'new_state', }); return null; } // Compare against baseline const diffDir = path.join(this.outputDir, 'visual', comparisonId); if (!fs.existsSync(diffDir)) { fs.mkdirSync(diffDir, { recursive: true }); } const diffPath = path.join(diffDir, 'diff.png'); let diffPixels = 0; let diffPercent = 0; try { const result = await compareScreenshots(baseline.screenshot_path, screenshotPath, diffPath, this.config.threshold); diffPixels = result.diffPixels; diffPercent = result.diffPercent; } catch { // If comparison fails (e.g. image format issues), skip return null; } const thresholdPct = this.config.threshold; const status = diffPercent > thresholdPct ? 'failed' : 'passed'; this.repo.createComparison({ id: comparisonId, sessionId, stateId: state.id, baselineId: baseline.id, currentScreenshotPath: screenshotPath, diffScreenshotPath: status === 'failed' ? diffPath : undefined, diffPixels, diffPercent, status, }); if (status !== 'failed') return null; // Determine severity from diff percent const pct = diffPercent * 100; let severity; if (pct > 15) severity = 'critical'; else if (pct > 5) severity = 'high'; else if (pct > 1) severity = 'medium'; else severity = 'low'; const anomaly = { id: crypto.randomUUID(), type: 'visual_regression', severity, observationId: state.id, actionTrace, description: `Visual regression detected: ${(pct).toFixed(2)}% of pixels changed`, evidence: { screenshotPath: diffPath, rawErrors: [`Diff: ${diffPixels} pixels (${(pct).toFixed(2)}%)`], }, timestamp: Date.now(), }; return anomaly; } } exports.VisualRegressionCollector = VisualRegressionCollector;