/* RFC-1324 AC-5/AC-6: runStagePlan must exit non-zero naming the failing stage, and deploy stages must report `skipped` — never `pass` — when deploy secrets are absent. Does not run the real cold pipeline — stages are injected doubles. RFC-1324: initial — injected failing stage, abort-cascade skips, secrets-absent skips, always-stage teardown. RFC-1324 */ import { describe, expect, it } from "vitest"; import { runStagePlan, type ColdEnv, type ColdStage } from "./cold.ts"; const logger = { info: () => {}, event: () => {}, getEvents: () => [], warn: () => {}, section: () => {}, error: () => {}, success: () => {}, }; function makeEnv(overrides: Partial = {}): ColdEnv { return { io: {} as ColdEnv["io"], logger: logger as unknown as ColdEnv["logger"], coldRoot: "/tmp/cold", cloneRoot: "/tmp/cold/werkstatt", cacheRoot: "/tmp/cold/systems-cache", gitRoot: "/tmp/cold/systems-git", fixture: "valid-minimal", systemId: "e2e-valid-minimal", missionId: "m000001", platformCommit: "abc", withDeploy: false, secrets: { accountId: undefined, apiToken: undefined }, intake: null, perStepTimeout: 5000, ...overrides, }; } const ok = (name: string): ColdStage => ({ name, run: async () => {} }); describe("runStagePlan (RFC-1324 AC-5)", () => { it("a failing stage fails and names itself; later stages are skipped", async () => { const stages: ColdStage[] = [ ok("prepare-cold-root"), { name: "register-site", run: async () => { throw new Error("register blew up"); }, }, ok("ship-closed"), ok("report"), ]; const steps = await runStagePlan(stages, makeEnv()); expect(steps.find((s) => s.step === "register-site")?.status).toBe("fail"); expect(steps.find((s) => s.step === "register-site")?.error).toContain("register blew up"); expect(steps.find((s) => s.step === "ship-closed")?.status).toBe("skipped"); expect(steps.find((s) => s.step === "ship-closed")?.skippedReason).toContain( "aborted after 'register-site'", ); expect(steps.find((s) => s.step === "report")?.status).toBe("skipped"); }); it("an `always` stage still runs after a failure (teardown semantics)", async () => { let teardownRan = false; const stages: ColdStage[] = [ { name: "dev-deploy", run: async () => { throw new Error("deploy failed"); }, }, { name: "teardown", always: true, run: async () => { teardownRan = true; }, }, ok("after"), ]; const steps = await runStagePlan(stages, makeEnv()); expect(teardownRan).toBe(true); expect(steps.find((s) => s.step === "teardown")?.status).toBe("pass"); expect(steps.find((s) => s.step === "after")?.status).toBe("skipped"); }); it("requiresMission stages are skipped when register produced no mission", async () => { const stages: ColdStage[] = [ok("register-site"), { ...ok("compose"), requiresMission: true }]; const steps = await runStagePlan(stages, makeEnv({ missionId: null })); expect(steps.find((s) => s.step === "compose")?.status).toBe("skipped"); expect(steps.find((s) => s.step === "compose")?.skippedReason).toContain("no mission"); }); }); describe("runStagePlan (RFC-1324 AC-6)", () => { it("deploy stages report skipped, never pass, when secrets are absent", async () => { const stages: ColdStage[] = [ ok("ship-closed"), { name: "dev-deploy", requiresMission: true, deployOnly: true, skipWhen: (env) => !env.secrets.accountId || !env.secrets.apiToken ? "secrets absent" : null, run: async () => {}, }, { name: "access-probe", requiresMission: true, deployOnly: true, skipWhen: (env) => !env.secrets.accountId || !env.secrets.apiToken ? "secrets absent" : null, run: async () => {}, }, ]; const steps = await runStagePlan(stages, makeEnv()); const deploy = steps.filter((s) => s.step === "dev-deploy" || s.step === "access-probe"); expect(deploy).toHaveLength(2); for (const s of deploy) { expect(s.status).toBe("skipped"); expect(s.skippedReason).toBe("secrets absent"); } expect(steps.find((s) => s.step === "ship-closed")?.status).toBe("pass"); }); it("deploy stages run when secrets are present", async () => { let ran = false; const stages: ColdStage[] = [ { name: "dev-deploy", skipWhen: (env) => (!env.secrets.apiToken ? "secrets absent" : null), run: async () => { ran = true; }, }, ]; const steps = await runStagePlan( stages, makeEnv({ secrets: { accountId: "a", apiToken: "t" } }), ); expect(ran).toBe(true); expect(steps[0].status).toBe("pass"); }); });