release-gate.test.ts

7.3 KB · raw

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  gateResultFromPlaywright,8  validateGateResult,9  REQUIRED_BROWSER_PROJECTS,10  type GateResult,11  type ProjectResult,12} from "./release-gate";1314const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");1516function project(name: string, overrides: Partial<ProjectResult> = {}): ProjectResult {17  return { name, totalTests: 5, passed: 5, skipped: 0, flaky: 0, ...overrides };18}1920describe("PB-26-210: gate validation — fail-closed conditions", () => {21  it("reads the project name from Playwright test records", () => {22    expect(gateResultFromPlaywright({23      suites: [{24        title: "security-sentinels.spec.ts",25        specs: [{ tests: [{ projectName: "chrome-release", results: [{ status: "passed" }] }] }],26      }],27    })).toEqual({ projects: [project("chrome-release", { totalTests: 1, passed: 1 })] });28  });2930  it("passes a clean result with the required project and no skips", () => {31    expect(validateGateResult({ projects: [project("chrome-release")] })).toEqual([]);32  });3334  it("fails on zero discovered tests across all projects", () => {35    const result: GateResult = {36      projects: [project("chrome-release", { totalTests: 0 })],37    };38    const errors = validateGateResult(result);39    expect(errors).toContain("zero tests discovered across all projects");40    expect(errors.some((e) => e.includes("zero tests"))).toBe(true);41  });4243  it("fails on a skipped required test", () => {44    const result: GateResult = {45      projects: [project("chrome-release", { skipped: 2 })],46    };47    const errors = validateGateResult(result);48    expect(errors.some((e) => e.includes("chrome-release") && e.includes("skipped"))).toBe(true);49  });5051  it("fails on an empty project (zero tests in one required project)", () => {52    const result: GateResult = {53      projects: [project("chrome-release", { totalTests: 0, passed: 0 })],54    };55    const errors = validateGateResult(result);56    expect(errors.some((e) => e.includes("chrome-release") && e.includes("zero tests"))).toBe(true);57  });5859  it("fails on a missing required project name", () => {60    const result: GateResult = {61      projects: [],62    };63    const errors = validateGateResult(result);64    expect(errors.some((e) => e.includes("chrome-release") && e.includes("missing"))).toBe(true);65  });6667  it("fails on flaky retries that remain flaky", () => {68    const result: GateResult = {69      projects: [project("chrome-release", { flaky: 1 })],70    };71    const errors = validateGateResult(result);72    expect(errors.some((e) => e.includes("chrome-release") && e.includes("flaky"))).toBe(true);73  });7475  it("fails when any required-project test did not pass", () => {76    const errors = validateGateResult({77      projects: [project("chrome-release", { totalTests: 5, passed: 4 })],78    });79    expect(errors.some((error) => error.includes("did not pass"))).toBe(true);80  });8182  it("fails on a simulated missing fixture (project exists but zero tests and zero passed)", () => {83    const result: GateResult = {84      projects: [project("chrome-release", { totalTests: 0, passed: 0, skipped: 0 })],85    };86    const errors = validateGateResult(result);87    expect(errors.length).toBeGreaterThan(0);88  });89});9091describe("PB-26-210: browser gate remains diagnostic rather than release-blocking", () => {92  const RELEASE_SCRIPTS = ["scripts/release.ts", "scripts/release-firefox.ts"];9394  for (const scriptPath of RELEASE_SCRIPTS) {95    it(`${scriptPath} calls the CI gate before any packaging or upload`, () => {96      const source = readFileSync(join(ROOT, scriptPath), "utf8");97      // PB-26-211: CI is invoked via runCommand("npm", ["run", "ci"], ...).98      // Look for the CI invocation pattern.99      const ciIndex = source.indexOf('"run", "ci"');100      expect(ciIndex).toBeGreaterThan(-1);101102      // Packaging/upload calls appear after CI. PB-26-211: these are now103      // function calls to uploadChrome / signFirefox / packageFirefoxSourceZip.104      // Match calls (with paren), not import names.105      const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageSourceArchive\(/);106      if (packageIndex > -1) {107        expect(ciIndex).toBeLessThan(packageIndex);108      }109    });110  }111112  it("scripts/ci.ts does not invoke the browser gate wrapper", () => {113    const source = readFileSync(join(ROOT, "scripts/ci.ts"), "utf8");114    expect(source).not.toContain("scripts/run-release-browser-gate.ts");115    expect(source).not.toContain("scripts/test-firefox-passkeys.mjs");116  });117118  it("the mandatory wrapper runs real Chrome and combined real-Firefox evidence", () => {119    const source = readFileSync(join(ROOT, "scripts/run-release-browser-gate.ts"), "utf8");120    expect(source).toContain("--project=chrome-release");121    expect(source).not.toContain("--project=firefox-release");122    expect(source).toContain("scripts/test-firefox-sender-context.mjs");123    expect(source).toContain("scripts/test-firefox-passkeys.mjs");124    expect(source).toContain("process.stderr.write(chromeResult.stdout)");125126    const firefoxSource = readFileSync(join(ROOT, "scripts/test-firefox-passkeys.mjs"), "utf8");127    expect(firefoxSource).toContain("firefox-sender-probe.js");128    expect(firefoxSource).toContain('sendSenderMessage({ type: "GET_RECOVERY_WORDS" })');129    expect(firefoxSource).toContain("peach_session_dk");130  });131132  it("the Chrome context honors headless release-gate execution", () => {133    const source = readFileSync(join(ROOT, "tests/helpers/vault.ts"), "utf8");134    expect(source).toContain('headless: process.env.PEACH_RELEASE_BROWSER_GATE === "1"');135  });136});137138describe("PB-26-210: playwright config defines required release project", () => {139  it("playwright.config.ts defines a chrome-release project", () => {140    const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");141    expect(source).toContain("chrome-release");142  });143144  it("REQUIRED_BROWSER_PROJECTS matches the projects Playwright actually defines", () => {145    expect(REQUIRED_BROWSER_PROJECTS).toEqual(["chrome-release"]);146    const source = readFileSync(join(ROOT, "playwright.config.ts"), "utf8");147    expect(source).toContain('name: "chrome-release"');148    expect(source).not.toContain('name: "firefox-release"');149  });150151  it("release entry points do not force the browser gate in their CI child environment", () => {152    for (const scriptPath of ["scripts/release.ts", "scripts/release-firefox.ts"]) {153      const source = readFileSync(join(ROOT, scriptPath), "utf8");154      expect(source).not.toContain("PEACH_RELEASE_BROWSER_GATE");155      expect(source).toContain("env: safeEnv()");156    }157  });158159  it("CI invokes the validator against Playwright JSON output", () => {160    const source = readFileSync(join(ROOT, "scripts/run-release-browser-gate.ts"), "utf8");161    expect(source).toContain("assertValidPlaywrightJsonReport");162  });163});164165describe("PB-26-209: Firefox passkey browser probe", () => {166  it("is a valid WebDriver async-script function body", () => {167    const source = readFileSync(join(ROOT, "scripts/test-firefox-passkeys.mjs"), "utf8");168    const probe = source.match(/const PROBE_SCRIPT = `([\s\S]*?)`;\n/);169170    expect(probe?.[1]).toBeDefined();171    expect(() => new Function(probe![1]!)).not.toThrow();172  });173});