diff --git a/packages/loopover-miner/lib/governor-pause-cli.js b/packages/loopover-miner/lib/governor-pause-cli.js index 05ee172618..aa55b5cee0 100644 --- a/packages/loopover-miner/lib/governor-pause-cli.js +++ b/packages/loopover-miner/lib/governor-pause-cli.js @@ -6,6 +6,7 @@ // existing single-row scalar-state table, not a new store: a pause flag has no relational key of its own, the // same reasoning that table's other scalar fields (rate-limit buckets, cap usage) already rely on. +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; import { openGovernorState } from "./governor-state.js"; const GOVERNOR_PAUSE_USAGE = "Usage: loopover-miner governor pause [--reason ] [--dry-run] [--json]"; @@ -83,8 +84,7 @@ function renderPauseState(pauseState) { export async function runGovernorPause(args, options = {}) { const parsed = parseGovernorPauseArgs(args); if ("error" in parsed) { - console.error(parsed.error); - return 2; + return reportCliFailure(argsWantJson(args), parsed.error); } if (parsed.dryRun) { @@ -109,16 +109,14 @@ export async function runGovernorPause(args, options = {}) { return 0; }); } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - return 2; + return reportCliFailure(parsed.json, describeCliError(error)); } } export async function runGovernorResume(args, options = {}) { const parsed = parseGovernorResumeArgs(args); if ("error" in parsed) { - console.error(parsed.error); - return 2; + return reportCliFailure(argsWantJson(args), parsed.error); } if (parsed.dryRun) { @@ -142,16 +140,14 @@ export async function runGovernorResume(args, options = {}) { return 0; }); } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - return 2; + return reportCliFailure(parsed.json, describeCliError(error)); } } export async function runGovernorStatus(args, options = {}) { const parsed = parseNoArgsSubcommand(args, GOVERNOR_STATUS_USAGE); if ("error" in parsed) { - console.error(parsed.error); - return 2; + return reportCliFailure(argsWantJson(args), parsed.error); } try { @@ -165,7 +161,6 @@ export async function runGovernorStatus(args, options = {}) { return 0; }); } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - return 2; + return reportCliFailure(parsed.json, describeCliError(error)); } } diff --git a/test/unit/miner-governor-pause-cli.test.ts b/test/unit/miner-governor-pause-cli.test.ts index 5d8ef0d61e..639e1ca80a 100644 --- a/test/unit/miner-governor-pause-cli.test.ts +++ b/test/unit/miner-governor-pause-cli.test.ts @@ -266,3 +266,97 @@ describe("loopover-miner governor pause/resume/status CLI (#4851)", () => { } }); }); + +describe("governor pause/resume/status --json error contract (#5914)", () => { + it("runGovernorPause emits the JSON envelope on a parse error and never opens the store", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + const openGovernorStateFn = vi.fn(); + + expect(await runGovernorPause(["--verbose", "--json"], { openGovernorState: openGovernorStateFn })).toBe(2); + expect(openGovernorStateFn).not.toHaveBeenCalled(); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ ok: false, error: "Unknown option: --verbose" }); + }); + + it("runGovernorPause emits the JSON envelope when the governor state throws", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect( + await runGovernorPause(["--json"], { + openGovernorState: () => { + throw new Error("disk full"); + }, + }), + ).toBe(2); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ ok: false, error: "disk full" }); + }); + + it("runGovernorResume emits the JSON envelope on a parse error rejected before --json is reached", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + // `extra` aborts the parse before the parser ever sees --json, so the envelope can only come from + // argsWantJson(args) reading raw argv -- parsed.json would be unavailable here. + expect(await runGovernorResume(["extra", "--json"])).toBe(2); + const envelope = JSON.parse(String(log.mock.calls[0]?.[0])); + expect(envelope.ok).toBe(false); + expect(envelope.error).toContain("Usage: loopover-miner governor resume"); + }); + + it("runGovernorResume emits the JSON envelope when the governor state throws", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect( + await runGovernorResume(["--json"], { + openGovernorState: () => { + throw new Error("disk full"); + }, + }), + ).toBe(2); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ ok: false, error: "disk full" }); + }); + + it("runGovernorStatus emits the JSON envelope on a parse error", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect(await runGovernorStatus(["extra", "--json"])).toBe(2); + const envelope = JSON.parse(String(log.mock.calls[0]?.[0])); + expect(envelope.ok).toBe(false); + expect(envelope.error).toContain("Usage: loopover-miner governor status"); + }); + + it("runGovernorStatus emits the JSON envelope when the governor state throws", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + expect( + await runGovernorStatus(["--json"], { + openGovernorState: () => { + throw new Error("disk full"); + }, + }), + ).toBe(2); + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ ok: false, error: "disk full" }); + }); + + it("keeps non-JSON error paths on stderr as plain text", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + + expect(await runGovernorPause(["--verbose"])).toBe(2); + expect(await runGovernorResume(["extra"])).toBe(2); + expect(await runGovernorStatus(["extra"])).toBe(2); + expect( + await runGovernorStatus([], { + openGovernorState: () => { + throw new Error("disk full"); + }, + }), + ).toBe(2); + + expect(log).not.toHaveBeenCalled(); + expect(error.mock.calls.map((call) => String(call[0]))).toEqual([ + "Unknown option: --verbose", + expect.stringContaining("Usage: loopover-miner governor resume"), + expect.stringContaining("Usage: loopover-miner governor status"), + "disk full", + ]); + }); +});