import { assertStringIncludes } from "@std/assert"; import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { EXCLUSIONS_PATH, LEGACY_PATH, RATCHET_GUIDE, } from "#scripts/check-coverage-exclusions/exclusion-ratchet.ts"; import { baseEntries, ratchetExit, } from "#scripts/check-coverage-exclusions/ratchet-run.ts"; import { gitShow, holds, lsTree, stubRun } from "#test-utils/git-stub.ts"; const module = (entries: string[]): string => `${entries.map((entry) => ` "${entry}",`).join("\n")}\n`; describe("baseEntries", () => { test("reads the data module at the merge base", async () => { const run = stubRun({ [lsTree("base123", EXCLUSIONS_PATH)]: holds(EXCLUSIONS_PATH), [gitShow("base123", EXCLUSIONS_PATH)]: { code: 0, stdout: module(["src/a.ts"]), }, }); expect(await baseEntries(run, "base123")).toEqual(["src/a.ts"]); }); test("falls back to the gate module the list moved out of", async () => { const run = stubRun({ [lsTree("base123", EXCLUSIONS_PATH)]: { code: 0, stdout: "", }, [lsTree("base123", LEGACY_PATH)]: holds(LEGACY_PATH), [gitShow("base123", LEGACY_PATH)]: { code: 0, stdout: module(["src/old.ts"]), }, }); expect(await baseEntries(run, "base123")).toEqual(["src/old.ts"]); }); test("answers an empty base when neither file exists there", async () => { const run = stubRun({ [lsTree("base123", EXCLUSIONS_PATH)]: { code: 0, stdout: "" }, [lsTree("base123", LEGACY_PATH)]: { code: 0, stdout: "" }, }); expect(await baseEntries(run, "base123")).toEqual([]); }); test("surfaces a probe failure that is not a missing path", async () => { const run = stubRun({ [lsTree("base123", EXCLUSIONS_PATH)]: { code: 128, stderr: "fatal: bad object base123", stdout: "", }, }); await expect(baseEntries(run, "base123")).rejects.toThrow(/bad object/); }); test("surfaces a read failure after the path checks out", async () => { const run = stubRun({ [lsTree("base123", EXCLUSIONS_PATH)]: holds(EXCLUSIONS_PATH), [gitShow("base123", EXCLUSIONS_PATH)]: { code: 128, stderr: "fatal: bad object base123", stdout: "", }, }); await expect(baseEntries(run, "base123")).rejects.toThrow(/bad object/); }); }); describe("ratchetExit", () => { const lines: string[] = []; const output = { log: (line: string) => lines.push(line), logError: (line: string) => lines.push(line), }; /** Run the ratchet over `head` against a base that holds `base`, with * `runtime` as the array the gate imports and the report lines collected * for the assertions. */ const exitFor = async ( head: string[], base: string[], runtime: readonly string[] = head, ): Promise => { lines.length = 0; const run = stubRun({ "git merge-base HEAD origin/main": { code: 0, stdout: "base123" }, [lsTree("base123", EXCLUSIONS_PATH)]: holds(EXCLUSIONS_PATH), [gitShow("base123", EXCLUSIONS_PATH)]: { code: 0, stdout: module(base), }, }); return ratchetExit(run, module(head), runtime, output); }; test("exits 0 and reports nothing added on a clean list", async () => { expect(await exitFor(["src/a.ts"], ["src/a.ts"])).toBe(0); expect(lines).toEqual([ "The coverage exclusion list adds nothing against origin/main.", ]); }); test("exits 1 and names each added entry", async () => { expect(await exitFor(["src/a.ts", "src/new.ts"], ["src/a.ts"])).toBe(1); assertStringIncludes(lines.join("\n"), "src/new.ts"); assertStringIncludes(lines.join("\n"), RATCHET_GUIDE); }); test("fails loudly when an entry hides from the parser", async () => { const head = [ "export const COVERAGE_EXCLUSIONS = [", ' "src/a.ts",', ' "src/b.ts", /* hidden from the line parser */', "];", "", ].join("\n"); lines.length = 0; const run = stubRun({ "git merge-base HEAD origin/main": { code: 0, stdout: "base123" }, [gitShow("base123", EXCLUSIONS_PATH)]: { code: 0, stdout: module(["src/a.ts"]), }, }); await expect( ratchetExit(run, head, ["src/a.ts", "src/b.ts"], output), ).rejects.toThrow(/could not read 1 exclusion entries.*src\/b\.ts/s); }); test("fails loudly when origin/main is missing", async () => { lines.length = 0; const run = stubRun({ "git merge-base HEAD origin/main": { code: 128, stderr: "fatal: not a valid revision", stdout: "", }, }); await expect(ratchetExit(run, module([]), [], output)).rejects.toThrow( /git fetch origin main/, ); }); });