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}1819describe("PB-26-210: gate validation — fail-closed conditions", () => {20 it("passes a clean result with the required project and no skips", () => {21 expect(validateGateResult({ projects: [project("chrome-release"), project("firefox-release")] })).toEqual([]);22 });2324 it("fails on zero discovered tests across all projects", () => {25 const result: GateResult = {26 projects: [project("chrome-release", { totalTests: 0 }), project("firefox-release", { totalTests: 0 })],27 };28 const errors = validateGateResult(result);29 expect(errors).toContain("zero tests discovered across all projects");30 expect(errors.some((e) => e.includes("zero tests"))).toBe(true);31 });3233 it("fails on a skipped required test", () => {34 const result: GateResult = {35 projects: [project("chrome-release", { skipped: 2 }), project("firefox-release")],36 };37 const errors = validateGateResult(result);38 expect(errors.some((e) => e.includes("chrome-release") && e.includes("skipped"))).toBe(true);39 });4041 it("fails on an empty project (zero tests in one required project)", () => {42 const result: GateResult = {43 projects: [project("chrome-release", { totalTests: 0, passed: 0 }), project("firefox-release")],44 };45 const errors = validateGateResult(result);46 expect(errors.some((e) => e.includes("chrome-release") && e.includes("zero tests"))).toBe(true);47 });4849 it("fails on a missing required project name", () => {50 const result: GateResult = {51 projects: [project("firefox-release")],52 };53 const errors = validateGateResult(result);54 expect(errors.some((e) => e.includes("chrome-release") && e.includes("missing"))).toBe(true);55 });5657 it("fails on flaky retries that remain flaky", () => {58 const result: GateResult = {59 projects: [project("chrome-release", { flaky: 1 }), project("firefox-release")],60 };61 const errors = validateGateResult(result);62 expect(errors.some((e) => e.includes("chrome-release") && e.includes("flaky"))).toBe(true);63 });6465 it("fails on a simulated missing fixture (project exists but zero tests and zero passed)", () => {66 const result: GateResult = {67 projects: [68 project("chrome-release", { totalTests: 0, passed: 0, skipped: 0 }),69 project("firefox-release", { totalTests: 0, passed: 0, skipped: 0 }),70 ],71 };72 const errors = validateGateResult(result);73 expect(errors.length).toBeGreaterThan(0);74 });75});7677describe("PB-26-210: release scripts invoke the CI gate and cannot bypass the browser phase", () => {78 const RELEASE_SCRIPTS = ["scripts/release.ts", "scripts/release-firefox.ts"];7980 for (const scriptPath of RELEASE_SCRIPTS) {81 it(`${scriptPath} calls the CI gate before any packaging or upload`, () => {82 const source = readFileSync(join(ROOT, scriptPath), "utf8");83 84 85 const ciIndex = source.indexOf('"run", "ci"');86 expect(ciIndex).toBeGreaterThan(-1);8788 89 90 91 const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageSourceArchive\(/);92 if (packageIndex > -1) {93 expect(ciIndex).toBeLessThan(packageIndex);94 }95 });96 }9798 it("scripts/ci.ts includes both Chrome and Firefox release browser gate steps", () => {99 const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");100 expect(source).toMatch(/Chrome.*release.*browser|chrome-release/i);101 expect(source).toMatch(/Firefox.*release.*browser|firefox.*passkey/i);102 });103104 it("scripts/ci.ts does not silently skip the browser gate when enabled", () => {105 const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");106 107 108 109 expect(source).toContain("chrome-release");110 expect(source).toContain("firefox");111 });112});113114describe("PB-26-210: playwright config defines required release project", () => {115 it("playwright.config.ts defines a chrome-release project", () => {116 const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");117 expect(source).toContain("chrome-release");118 });119120 it("REQUIRED_BROWSER_PROJECTS matches the projects Playwright actually defines", () => {121 expect(REQUIRED_BROWSER_PROJECTS).toEqual(["chrome-release", "firefox-release"]);122 const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");123 expect(source).toContain('name: "chrome-release"');124 expect(source).toContain('name: "firefox-release"');125 });126127 it("release entry points force the browser gate in their CI child environment", () => {128 for (const scriptPath of ["scripts/release.ts", "scripts/release-firefox.ts"]) {129 const source = readFileSync(join(ROOT, scriptPath), "utf8");130 expect(source).toContain("PEACH_RELEASE_BROWSER_GATE");131 }132 });133134 it("CI invokes the validator against Playwright JSON output", () => {135 const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");136 expect(source).toContain("run-release-browser-gate");137 expect(source).toContain("validateGateResult");138 });139});140141describe("PB-26-209: Firefox passkey browser probe", () => {142 it("is a valid WebDriver async-script function body", () => {143 const source = readFileSync(join(ROOT, "scripts/test-firefox-passkeys.mjs"), "utf8");144 const probe = source.match(/const PROBE_SCRIPT = `([\s\S]*?)`;\n/);145146 expect(probe?.[1]).toBeDefined();147 expect(() => new Function(probe![1]!)).not.toThrow();148 });149});