release-gate.test.ts

6.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  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      // PB-26-211: CI is invoked via runCommand("npm", ["run", "ci"], ...).84      // Look for the CI invocation pattern.85      const ciIndex = source.indexOf('"run", "ci"');86      expect(ciIndex).toBeGreaterThan(-1);8788      // Packaging/upload calls appear after CI. PB-26-211: these are now89      // function calls to uploadChrome / signFirefox / packageFirefoxSourceZip.90      // Match calls (with paren), not import names.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    // When the browser gate is enabled, both Chrome and Firefox steps must be107    // present in the steps array — not behind a comment or a skip condition108    // that would make them disappear entirely.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});