Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions packages/loopover-miner/lib/governor-pause-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <text>] [--dry-run] [--json]";
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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));
}
}
94 changes: 94 additions & 0 deletions test/unit/miner-governor-pause-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]);
});
});