/** * PB-26-210: Release browser-gate validation. * * Pure functions that verify a gate result has no silent failures. * A release cannot report success with zero tests, unexpected skips, * flaky retries, or missing required projects. */ export interface ProjectResult { name: string; totalTests: number; passed: number; skipped: number; flaky: number; } export interface GateResult { projects: ProjectResult[]; } export const REQUIRED_BROWSER_PROJECTS = ["chrome-release"] as const; export interface PlaywrightJsonReport { suites?: Array<{ title?: string; specs?: Array<{ tests?: Array<{ status?: string; results?: Array<{ status?: string }> }> }>; suites?: PlaywrightJsonReport["suites"]; }>; } export function gateResultFromPlaywright(report: PlaywrightJsonReport): GateResult { const projects = new Map(); const visit = (suites: PlaywrightJsonReport["suites"] = []) => { for (const suite of suites) { if (suite.title && REQUIRED_BROWSER_PROJECTS.includes(suite.title as (typeof REQUIRED_BROWSER_PROJECTS)[number])) { const project = projects.get(suite.title) ?? { name: suite.title, totalTests: 0, passed: 0, skipped: 0, flaky: 0 }; for (const spec of suite.specs ?? []) for (const test of spec.tests ?? []) { project.totalTests++; const results = test.results ?? []; const status = results.at(-1)?.status ?? test.status; if (status === "skipped") project.skipped++; else if (status === "passed") project.passed++; if (results.length > 1 && results.some((result) => result.status === "passed")) project.flaky++; } projects.set(suite.title, project); } visit(suite.suites); } }; visit(report.suites); return { projects: [...projects.values()] }; } export function validatePlaywrightJsonReport(report: PlaywrightJsonReport): string[] { return validateGateResult(gateResultFromPlaywright(report)); } export function formatGateErrors(errors: string[]): never { throw new Error(`Release browser gate validation failed:\n ${errors.join("\n ")}`); } export function assertValidPlaywrightJsonReport(report: PlaywrightJsonReport): void { const errors = validatePlaywrightJsonReport(report); if (errors.length > 0) formatGateErrors(errors); } /** * Validate a gate result. Returns a list of error strings — empty means pass. * The release gate fails (non-zero) on any error. */ export function validateGateResult( result: GateResult, requiredProjects: readonly string[] = REQUIRED_BROWSER_PROJECTS, ): string[] { const errors: string[] = []; const totalTests = result.projects.reduce((sum, p) => sum + p.totalTests, 0); if (totalTests === 0) { errors.push("zero tests discovered across all projects"); } for (const name of requiredProjects) { const project = result.projects.find((p) => p.name === name); if (!project) { errors.push(`required project "${name}" is missing`); continue; } if (project.totalTests === 0) { errors.push(`project "${name}" has zero tests`); } if (project.skipped > 0) { errors.push(`project "${name}" has ${project.skipped} skipped test(s)`); } if (project.flaky > 0) { errors.push(`project "${name}" has ${project.flaky} flaky retry/retr(y)ies`); } } return errors; }