/** * run-state.test.ts — run-state reconciliation, error hygiene, and the * .gitignore guard (issues surfaced by the MagnaFluo all-run: "partial" for * all-failed runs, stale errors on completed checks, staged artifacts). */ import { describe, expect, it, beforeEach, afterEach } from "bun:test"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { ensureRunStateIgnored, initRunState, markCheckStatus, reconcileRunStatus, } from "../src/run-state.js"; /** Build a run state whose checks carry the given statuses. */ function stateWith( ...statuses: Array<[name: string, status: string]> ): ReturnType { const state = initRunState( "/virtual/cwd", statuses.map(([name]) => ({ name, label: name })), ); for (const [name, status] of statuses) { markCheckStatus( state, name, status as "complete" | "failed" | "skipped", status === "failed" ? "boom" : undefined, ); } return state; } describe("reconcileRunStatus", () => { it("is in_progress while nothing is terminal", () => { expect(reconcileRunStatus(stateWith())).toBe("in_progress"); expect( reconcileRunStatus(initRunState("/virt", [{ name: "a", label: "a" }])), ).toBe("in_progress"); }); it("is complete only when every check is complete", () => { expect( reconcileRunStatus(stateWith(["a", "complete"], ["b", "complete"])), ).toBe("complete"); }); it("is partial when some checks failed and others completed", () => { expect( reconcileRunStatus(stateWith(["a", "complete"], ["b", "failed"])), ).toBe("partial"); }); it("is partial when checks were skipped", () => { expect( reconcileRunStatus(stateWith(["a", "complete"], ["b", "skipped"])), ).toBe("partial"); }); it("is failed when every check failed (not partial)", () => { expect( reconcileRunStatus(stateWith(["a", "failed"], ["b", "failed"])), ).toBe("failed"); expect(reconcileRunStatus(stateWith(["a", "failed"]))).toBe("failed"); }); it("is partial for a mixed failed/skipped run (some degraded, none ok)", () => { expect( reconcileRunStatus(stateWith(["a", "failed"], ["b", "skipped"])), ).toBe("partial"); }); }); describe("check error hygiene", () => { it("a failed check records its error", () => { const s = stateWith(["a", "failed"]); expect(s.checks.a?.error).toBe("boom"); }); it("a later success clears the stale error (resume-complete invariant)", () => { const s = stateWith(["a", "failed"]); expect(s.checks.a?.error).toBe("boom"); markCheckStatus(s, "a", "complete"); expect(s.checks.a?.error).toBeUndefined(); expect(s.checks.a?.status).toBe("complete"); }); }); describe("ensureRunStateIgnored", () => { let cwd: string; beforeEach(async () => { cwd = await mkdtemp(join(tmpdir(), "pygium-git-")); await mkdir(join(cwd, ".git"), { recursive: true }); // pretend it's a work tree }); afterEach(async () => { await rm(cwd, { recursive: true, force: true }); }); it("creates .gitignore with .pygienium/ when absent", async () => { expect(await ensureRunStateIgnored(cwd)).toBe(true); const content = await readFile(join(cwd, ".gitignore"), "utf8"); expect(content).toContain(".pygienium/"); }); it("appends to an existing .gitignore without the marker", async () => { await writeFile(join(cwd, ".gitignore"), "node_modules/\n", "utf8"); expect(await ensureRunStateIgnored(cwd)).toBe(true); const content = await readFile(join(cwd, ".gitignore"), "utf8"); expect(content).toContain(".pygienium/"); expect(content).toContain("node_modules/"); }); it("leaves an existing marker untouched and reports no change", async () => { await writeFile( join(cwd, ".gitignore"), ".pygienium/\nnode_modules/\n", "utf8", ); expect(await ensureRunStateIgnored(cwd)).toBe(false); const content = await readFile(join(cwd, ".gitignore"), "utf8"); expect(content).toBe(".pygienium/\nnode_modules/\n"); }); it("is a no-op outside a git work tree", async () => { const plain = await mkdtemp(join(tmpdir(), "pygium-nogit-")); try { expect(await ensureRunStateIgnored(plain)).toBe(false); await expect( readFile(join(plain, ".gitignore"), "utf8"), ).rejects.toThrow(); } finally { await rm(plain, { recursive: true, force: true }); } }); });