import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { gateResultFromPlaywright, validateGateResult, REQUIRED_BROWSER_PROJECTS, type GateResult, type ProjectResult, } from "./release-gate"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); function project(name: string, overrides: Partial = {}): ProjectResult { return { name, totalTests: 5, passed: 5, skipped: 0, flaky: 0, ...overrides }; } describe("PB-26-210: gate validation — fail-closed conditions", () => { it("reads the project name from Playwright test records", () => { expect(gateResultFromPlaywright({ suites: [{ title: "security-sentinels.spec.ts", specs: [{ tests: [{ projectName: "chrome-release", results: [{ status: "passed" }] }] }], }], })).toEqual({ projects: [project("chrome-release", { totalTests: 1, passed: 1 })] }); }); it("passes a clean result with the required project and no skips", () => { expect(validateGateResult({ projects: [project("chrome-release")] })).toEqual([]); }); it("fails on zero discovered tests across all projects", () => { const result: GateResult = { projects: [project("chrome-release", { totalTests: 0 })], }; const errors = validateGateResult(result); expect(errors).toContain("zero tests discovered across all projects"); expect(errors.some((e) => e.includes("zero tests"))).toBe(true); }); it("fails on a skipped required test", () => { const result: GateResult = { projects: [project("chrome-release", { skipped: 2 })], }; const errors = validateGateResult(result); expect(errors.some((e) => e.includes("chrome-release") && e.includes("skipped"))).toBe(true); }); it("fails on an empty project (zero tests in one required project)", () => { const result: GateResult = { projects: [project("chrome-release", { totalTests: 0, passed: 0 })], }; const errors = validateGateResult(result); expect(errors.some((e) => e.includes("chrome-release") && e.includes("zero tests"))).toBe(true); }); it("fails on a missing required project name", () => { const result: GateResult = { projects: [], }; const errors = validateGateResult(result); expect(errors.some((e) => e.includes("chrome-release") && e.includes("missing"))).toBe(true); }); it("fails on flaky retries that remain flaky", () => { const result: GateResult = { projects: [project("chrome-release", { flaky: 1 })], }; const errors = validateGateResult(result); expect(errors.some((e) => e.includes("chrome-release") && e.includes("flaky"))).toBe(true); }); it("fails when any required-project test did not pass", () => { const errors = validateGateResult({ projects: [project("chrome-release", { totalTests: 5, passed: 4 })], }); expect(errors.some((error) => error.includes("did not pass"))).toBe(true); }); it("fails on a simulated missing fixture (project exists but zero tests and zero passed)", () => { const result: GateResult = { projects: [project("chrome-release", { totalTests: 0, passed: 0, skipped: 0 })], }; const errors = validateGateResult(result); expect(errors.length).toBeGreaterThan(0); }); }); describe("PB-26-210: release scripts invoke the CI gate and cannot bypass the browser phase", () => { const RELEASE_SCRIPTS = ["scripts/release.ts", "scripts/release-firefox.ts"]; for (const scriptPath of RELEASE_SCRIPTS) { it(`${scriptPath} calls the CI gate before any packaging or upload`, () => { const source = readFileSync(join(ROOT, scriptPath), "utf8"); // PB-26-211: CI is invoked via runCommand("npm", ["run", "ci"], ...). // Look for the CI invocation pattern. const ciIndex = source.indexOf('"run", "ci"'); expect(ciIndex).toBeGreaterThan(-1); // Packaging/upload calls appear after CI. PB-26-211: these are now // function calls to uploadChrome / signFirefox / packageFirefoxSourceZip. // Match calls (with paren), not import names. const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageSourceArchive\(/); if (packageIndex > -1) { expect(ciIndex).toBeLessThan(packageIndex); } }); } it("scripts/ci.ts invokes one mandatory browser gate wrapper", () => { const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8"); expect(source.match(/scripts\/run-release-browser-gate\.ts/g)).toHaveLength(1); expect(source).not.toContain("scripts/test-firefox-passkeys.mjs"); }); it("the mandatory wrapper runs real Chrome and combined real-Firefox evidence", () => { const source = readFileSync(join(ROOT, "scripts/run-release-browser-gate.ts"), "utf8"); expect(source).toContain("--project=chrome-release"); expect(source).not.toContain("--project=firefox-release"); expect(source).toContain("scripts/test-firefox-sender-context.mjs"); expect(source).toContain("scripts/test-firefox-passkeys.mjs"); expect(source).toContain("process.stderr.write(chromeResult.stdout)"); const firefoxSource = readFileSync(join(ROOT, "scripts/test-firefox-passkeys.mjs"), "utf8"); expect(firefoxSource).toContain("firefox-sender-probe.js"); expect(firefoxSource).toContain('sendSenderMessage({ type: "GET_RECOVERY_WORDS" })'); expect(firefoxSource).toContain("peach_session_dk"); }); it("the Chrome context honors headless release-gate execution", () => { const source = readFileSync(join(ROOT, "tests/helpers/vault.ts"), "utf8"); expect(source).toContain('headless: process.env.PEACH_RELEASE_BROWSER_GATE === "1"'); }); }); describe("PB-26-210: playwright config defines required release project", () => { it("playwright.config.ts defines a chrome-release project", () => { const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8"); expect(source).toContain("chrome-release"); }); it("REQUIRED_BROWSER_PROJECTS matches the projects Playwright actually defines", () => { expect(REQUIRED_BROWSER_PROJECTS).toEqual(["chrome-release"]); const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8"); expect(source).toContain('name: "chrome-release"'); expect(source).not.toContain('name: "firefox-release"'); }); it("release entry points force the browser gate in their CI child environment", () => { for (const scriptPath of ["scripts/release.ts", "scripts/release-firefox.ts"]) { const source = readFileSync(join(ROOT, scriptPath), "utf8"); expect(source).toContain("PEACH_RELEASE_BROWSER_GATE"); } }); it("CI invokes the validator against Playwright JSON output", () => { const source = readFileSync(join(ROOT, "scripts/run-release-browser-gate.ts"), "utf8"); expect(source).toContain("assertValidPlaywrightJsonReport"); }); }); describe("PB-26-209: Firefox passkey browser probe", () => { it("is a valid WebDriver async-script function body", () => { const source = readFileSync(join(ROOT, "scripts/test-firefox-passkeys.mjs"), "utf8"); const probe = source.match(/const PROBE_SCRIPT = `([\s\S]*?)`;\n/); expect(probe?.[1]).toBeDefined(); expect(() => new Function(probe![1]!)).not.toThrow(); }); });