release-gate.test.ts

5.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}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      // PB-26-211: CI is invoked via runCommand("npm", ["run", "ci"], ...).93      // Look for the CI invocation pattern.94      const ciIndex = source.indexOf('"run", "ci"');95      expect(ciIndex).toBeGreaterThan(-1);9697      // Packaging/upload calls appear after CI. PB-26-211: these are now98      // function calls to uploadChrome / signFirefox / packageFirefoxSourceZip.99      // Match calls (with paren), not import names.100      const packageIndex = source.search(/uploadChrome\(|signFirefox\(|packageSourceArchive\(/);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    // When the browser gate is enabled, both Chrome and Firefox steps must be116    // present in the steps array — not behind a comment or a skip condition117    // that would make them disappear entirely.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});