1import { readFileSync } from "node:fs";2import { fileURLToPath } from "node:url";3import { dirname, join, resolve } from "node:path";4import { describe, expect, it } from "vitest";56import {7 validateGateResult,8 REQUIRED_BROWSER_PROJECTS,9 type GateResult,10 type ProjectResult,11} from "./release-gate";1213const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");1415function project(name: string, overrides: Partial<ProjectResult> = {}): ProjectResult {16 return { name, totalTests: 5, passed: 5, skipped: 0, flaky: 0, ...overrides };17}1819function passingGate(): GateResult {20 return {21 projects: [22 project("chrome-release"),23 project("firefox-release"),24 ],25 };26}2728describe("PB-26-210: gate validation — fail-closed conditions", () => {29 it("passes a clean result with both required projects and no skips", () => {30 expect(validateGateResult(passingGate())).toEqual([]);31 });3233 it("fails on zero discovered tests across all projects", () => {34 const result: GateResult = {35 projects: [project("chrome-release", { totalTests: 0 }), project("firefox-release", { totalTests: 0 })],36 };37 const errors = validateGateResult(result);38 expect(errors).toContain("zero tests discovered across all projects");39 expect(errors.some((e) => e.includes("zero tests"))).toBe(true);40 });4142 it("fails on a skipped required test", () => {43 const result: GateResult = {44 projects: [project("chrome-release", { skipped: 2 }), project("firefox-release")],45 };46 const errors = validateGateResult(result);47 expect(errors.some((e) => e.includes("chrome-release") && e.includes("skipped"))).toBe(true);48 });4950 it("fails on an empty project (zero tests in one required project)", () => {51 const result: GateResult = {52 projects: [project("chrome-release", { totalTests: 0, passed: 0 }), project("firefox-release")],53 };54 const errors = validateGateResult(result);55 expect(errors.some((e) => e.includes("chrome-release") && e.includes("zero tests"))).toBe(true);56 });5758 it("fails on a missing required project name", () => {59 const result: GateResult = {60 projects: [project("chrome-release")],61 };62 const errors = validateGateResult(result);63 expect(errors.some((e) => e.includes("firefox-release") && e.includes("missing"))).toBe(true);64 });6566 it("fails on flaky retries that remain flaky", () => {67 const result: GateResult = {68 projects: [project("chrome-release", { flaky: 1 }), project("firefox-release")],69 };70 const errors = validateGateResult(result);71 expect(errors.some((e) => e.includes("chrome-release") && e.includes("flaky"))).toBe(true);72 });7374 it("fails on a simulated missing fixture (project exists but zero tests and zero passed)", () => {75 const result: GateResult = {76 projects: [77 project("chrome-release", { totalTests: 0, passed: 0, skipped: 0 }),78 project("firefox-release", { totalTests: 0, passed: 0, skipped: 0 }),79 ],80 };81 const errors = validateGateResult(result);82 expect(errors.length).toBeGreaterThan(0);83 });84});8586describe("PB-26-210: release scripts invoke the CI gate and cannot bypass the browser phase", () => {87 const RELEASE_SCRIPTS = ["scripts/release.ts", "scripts/release-firefox.ts"];8889 for (const scriptPath of RELEASE_SCRIPTS) {90 it(`${scriptPath} calls the CI gate before any packaging or upload`, () => {91 const source = readFileSync(join(ROOT, scriptPath), "utf8");92 93 94 const ciIndex = source.indexOf('"run", "ci"');95 expect(ciIndex).toBeGreaterThan(-1);9697 98 99 100 const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageFirefoxSourceZip\(/);101 if (packageIndex > -1) {102 expect(ciIndex).toBeLessThan(packageIndex);103 }104 });105 }106107 it("scripts/ci.ts includes both Chrome and Firefox release browser gate steps", () => {108 const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");109 expect(source).toMatch(/Chrome.*release.*browser|chrome-release/i);110 expect(source).toMatch(/Firefox.*release.*browser|firefox.*passkey/i);111 });112113 it("scripts/ci.ts does not silently skip the browser gate when enabled", () => {114 const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");115 116 117 118 expect(source).toContain("chrome-release");119 expect(source).toContain("firefox");120 });121});122123describe("PB-26-210: playwright config defines required release project", () => {124 it("playwright.config.ts defines a chrome-release project", () => {125 const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");126 expect(source).toContain("chrome-release");127 });128129 it("REQUIRED_BROWSER_PROJECTS includes chrome-release and firefox-release", () => {130 expect(REQUIRED_BROWSER_PROJECTS).toContain("chrome-release");131 expect(REQUIRED_BROWSER_PROJECTS).toContain("firefox-release");132 });133});