diff --git a/packages/loopover-engine/README.md b/packages/loopover-engine/README.md index 5d37e3f05..65c15f22a 100644 --- a/packages/loopover-engine/README.md +++ b/packages/loopover-engine/README.md @@ -680,6 +680,11 @@ These modules compute the *decisions*; the append-only record of what was decide in [Governor ledger](#governor-ledger) below (`allowed` / `denied` / `throttled` / `kill_switch`), which the chokepoint's returned ledger event feeds. +`action-mode.ts`'s dry-run-by-default precedence (`resolveMinerActionMode` and siblings) has a Codecov-visible +root mirror at `test/unit/miner-governor-action-mode.test.ts` (#8345) — `codecov/patch` only reads the root +vitest suite (see [Test](#test)), so this safety-adjacent write-execution gate is gradeable there as well as by +the package's own `node:test` suite, alongside the existing `test/unit/miner-governor-kill-switch.test.ts` mirror. + ## Governor ledger `normalizeGovernorLedgerEvent` validates append-only governor decision rows before the local miner persists them. diff --git a/test/unit/miner-governor-action-mode.test.ts b/test/unit/miner-governor-action-mode.test.ts new file mode 100644 index 000000000..098bfaa9a --- /dev/null +++ b/test/unit/miner-governor-action-mode.test.ts @@ -0,0 +1,128 @@ +// Root-level vitest coverage twin for `packages/loopover-engine/src/governor/action-mode.ts` (#8345). +// +// `action-mode.ts` (#2342) is the miner's dry-run-by-default write-execution gate — live, load-bearing, and +// fully exercised by the engine package's own `node --test` suite (`packages/loopover-engine/test/action-mode.test.ts`). +// But that runner is not part of the root vitest run Codecov reads `codecov/patch` from, so +// `resolveMinerActionMode` and its siblings report as ~0% covered despite being genuinely tested (same blind +// spot as #6250). This twin imports the primitives via the engine barrel and re-exercises every scenario the +// package suite covers — matching the sibling pattern in `test/unit/calibration-dashboard.test.ts` and +// `test/unit/miner-governor-kill-switch.test.ts`. Kill-switch state is expressed purely through the +// `MinerKillSwitchScope` string (`"none"`/`"repo"`/`"global"`), exactly as the package suite does, so no real +// kill-switch IO path is touched. +import { describe, expect, it } from "vitest"; +import { + MINER_LIVE_MODE_ENV_VAR, + MINER_LIVE_MODE_OPT_IN, + buildMinerDryRunGovernorLedgerEvent, + isExplicitMinerLiveModeOptIn, + isGlobalMinerLiveModeOptIn, + minerActionModeExecutes, + resolveMinerActionMode, +} from "../../packages/loopover-engine/src/index"; + +describe("barrel: action-mode primitives are re-exported from the engine entrypoint (#2342)", () => { + it("exposes the functions and opt-in literals", () => { + expect(typeof resolveMinerActionMode).toBe("function"); + expect(typeof minerActionModeExecutes).toBe("function"); + expect(typeof isExplicitMinerLiveModeOptIn).toBe("function"); + expect(typeof isGlobalMinerLiveModeOptIn).toBe("function"); + expect(typeof buildMinerDryRunGovernorLedgerEvent).toBe("function"); + expect(MINER_LIVE_MODE_OPT_IN).toBe("live"); + expect(MINER_LIVE_MODE_ENV_VAR).toBe("LOOPOVER_MINER_LIVE_MODE"); + }); +}); + +describe("isExplicitMinerLiveModeOptIn", () => { + it("only the exact `\"live\"` literal opts in — no truthy coercion, case-folding, or alternate spellings", () => { + expect(isExplicitMinerLiveModeOptIn("live")).toBe(true); + for (const value of [true, 1, "Live", "LIVE", "yes", "on", "1", "true", "", null, undefined, {}]) { + expect(isExplicitMinerLiveModeOptIn(value)).toBe(false); + } + }); +}); + +describe("isGlobalMinerLiveModeOptIn", () => { + it("only the exact env value `\"live\"` opts in", () => { + expect(isGlobalMinerLiveModeOptIn({ LOOPOVER_MINER_LIVE_MODE: "live" })).toBe(true); + for (const value of [undefined, "", "1", "true", "Live", "on"]) { + expect(isGlobalMinerLiveModeOptIn({ LOOPOVER_MINER_LIVE_MODE: value })).toBe(false); + } + }); +}); + +describe("resolveMinerActionMode — 3-step precedence ladder", () => { + it("no config anywhere defaults to dry_run, never live", () => { + expect( + resolveMinerActionMode({ killSwitchScope: "none", repoLiveModeOptIn: undefined, globalLiveModeOptIn: false }), + ).toBe("dry_run"); + }); + + it("malformed/partial repo opt-in values fail closed to dry_run (global false, && short-circuit)", () => { + for (const repoLiveModeOptIn of [true, "yes", "LIVE", "", null, 1]) { + expect( + resolveMinerActionMode({ killSwitchScope: "none", repoLiveModeOptIn, globalLiveModeOptIn: false }), + ).toBe("dry_run"); + } + }); + + it("the exact repo-side opt-in alone stays dry_run without the operator opt-in (global false arm)", () => { + expect( + resolveMinerActionMode({ killSwitchScope: "none", repoLiveModeOptIn: "live", globalLiveModeOptIn: false }), + ).toBe("dry_run"); + }); + + it("the global operator opt-in alone stays dry_run without repo opt-in (global true, repo not explicit)", () => { + expect( + resolveMinerActionMode({ killSwitchScope: "none", repoLiveModeOptIn: undefined, globalLiveModeOptIn: true }), + ).toBe("dry_run"); + }); + + it("both the repo AND operator opt-ins are required for live (both && operands true)", () => { + expect( + resolveMinerActionMode({ killSwitchScope: "none", repoLiveModeOptIn: "live", globalLiveModeOptIn: true }), + ).toBe("live"); + }); + + it("the kill-switch always wins over any live-mode opt-in (repo and global scope both -> paused)", () => { + expect( + resolveMinerActionMode({ killSwitchScope: "repo", repoLiveModeOptIn: "live", globalLiveModeOptIn: true }), + ).toBe("paused"); + expect( + resolveMinerActionMode({ killSwitchScope: "global", repoLiveModeOptIn: "live", globalLiveModeOptIn: true }), + ).toBe("paused"); + }); +}); + +describe("minerActionModeExecutes", () => { + it("is true only for live", () => { + expect(minerActionModeExecutes("live")).toBe(true); + expect(minerActionModeExecutes("dry_run")).toBe(false); + expect(minerActionModeExecutes("paused")).toBe(false); + }); +}); + +describe("buildMinerDryRunGovernorLedgerEvent", () => { + it("records the would-be action as an `allowed`/`dry_run` shadow row", () => { + const event = buildMinerDryRunGovernorLedgerEvent({ + repoFullName: "acme/widgets", + actionClass: "open_pr", + wouldBeAction: { action: "open_pr", title: "example" }, + }); + expect(event).toEqual({ + eventType: "allowed", + repoFullName: "acme/widgets", + actionClass: "open_pr", + decision: "dry_run", + reason: "dry_run_mode_active", + payload: { wouldBeAction: { action: "open_pr", title: "example" } }, + }); + }); + + it("an omitted repoFullName normalizes to null via the `?? null` fallback", () => { + const event = buildMinerDryRunGovernorLedgerEvent({ + actionClass: "open_pr", + wouldBeAction: { action: "open_pr" }, + }); + expect(event.repoFullName).toBe(null); + }); +});