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
4 changes: 2 additions & 2 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export function printHelp(input) {
" gittensory-miner plan list [--status pending|running|completed|failed] [--json]",
" gittensory-miner plan show <planId> [--json]",
" gittensory-miner governor list [--repo <owner/repo>] [--type allowed|denied|throttled|kill_switch] [--json]",
" gittensory-miner governor pause [--reason <text>] [--json] Stop the loop before its next cycle",
" gittensory-miner governor resume [--json] Let a paused loop continue",
" gittensory-miner governor pause [--reason <text>] [--dry-run] [--json] Stop the loop before its next cycle",
" gittensory-miner governor resume [--dry-run] [--json] Let a paused loop continue",
" gittensory-miner governor status [--json] Show whether the governor is paused",
" gittensory-miner calibration [--json] Report predicted-vs-realized gate accuracy",
" gittensory-miner feasibility <claimStatus> <duplicateClusterRisk> <issueStatus> [--not-found] [--json]",
Expand Down
8 changes: 7 additions & 1 deletion packages/gittensory-miner/lib/governor-pause-cli.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { GovernorState } from "./governor-state.js";

export type ParsedGovernorPauseArgs = { json: boolean; reason: string | null } | { error: string };
export type ParsedGovernorPauseArgs =
| { json: boolean; dryRun: boolean; reason: string | null }
| { error: string };

export type ParsedGovernorResumeArgs = { json: boolean; dryRun: boolean } | { error: string };

export type ParsedGovernorNoArgsSubcommand = { json: boolean } | { error: string };

Expand All @@ -10,6 +14,8 @@ export type GovernorPauseCliOptions = {

export function parseGovernorPauseArgs(args: string[]): ParsedGovernorPauseArgs;

export function parseGovernorResumeArgs(args: string[]): ParsedGovernorResumeArgs;

export function runGovernorPause(args: string[], options?: GovernorPauseCliOptions): Promise<number>;

export function runGovernorResume(args: string[], options?: GovernorPauseCliOptions): Promise<number>;
Expand Down
53 changes: 49 additions & 4 deletions packages/gittensory-miner/lib/governor-pause-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@

import { openGovernorState } from "./governor-state.js";

const GOVERNOR_PAUSE_USAGE = "Usage: gittensory-miner governor pause [--reason <text>] [--json]";
const GOVERNOR_RESUME_USAGE = "Usage: gittensory-miner governor resume [--json]";
const GOVERNOR_PAUSE_USAGE = "Usage: gittensory-miner governor pause [--reason <text>] [--dry-run] [--json]";
const GOVERNOR_RESUME_USAGE = "Usage: gittensory-miner governor resume [--dry-run] [--json]";
const GOVERNOR_STATUS_USAGE = "Usage: gittensory-miner governor status [--json]";

export function parseGovernorPauseArgs(args) {
const options = { json: false, reason: null };
const options = { json: false, dryRun: false, reason: null };

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
// #4847: reports what pausing would do and returns before writing to governor-state.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--reason") {
const value = args[index + 1];
if (!value || value.startsWith("-")) return { error: GOVERNOR_PAUSE_USAGE };
Expand All @@ -34,6 +39,25 @@ export function parseGovernorPauseArgs(args) {
return options;
}

export function parseGovernorResumeArgs(args) {
const options = { json: false, dryRun: false };

for (const token of args) {
if (token === "--json") {
options.json = true;
continue;
}
// #4847: reports what resuming would do and returns before writing to governor-state.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
return { error: GOVERNOR_RESUME_USAGE };
}

return options;
}

function parseNoArgsSubcommand(args, usage) {
if (args.length === 0) return { json: false };
if (args.length === 1 && args[0] === "--json") return { json: true };
Expand Down Expand Up @@ -63,6 +87,17 @@ export async function runGovernorPause(args, options = {}) {
return 2;
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", paused: true, reason: parsed.reason };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult));
} else {
const reason = parsed.reason ? ` (${parsed.reason})` : "";
console.log(`DRY RUN: would pause the governor${reason}. No governor-state write was made.`);
}
return 0;
}

try {
return await withGovernorState(options, (governorState) => {
const pauseState = governorState.savePauseState({ paused: true, reason: parsed.reason });
Expand All @@ -80,12 +115,22 @@ export async function runGovernorPause(args, options = {}) {
}

export async function runGovernorResume(args, options = {}) {
const parsed = parseNoArgsSubcommand(args, GOVERNOR_RESUME_USAGE);
const parsed = parseGovernorResumeArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", paused: false };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult));
} else {
console.log("DRY RUN: would resume the governor. No governor-state write was made.");
}
return 0;
}

try {
return await withGovernorState(options, (governorState) => {
const pauseState = governorState.savePauseState({ paused: false });
Expand Down
68 changes: 65 additions & 3 deletions test/unit/miner-governor-pause-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { closeDefaultGovernorState, openGovernorState } from "../../packages/gittensory-miner/lib/governor-state.js";
import {
parseGovernorPauseArgs,
parseGovernorResumeArgs,
runGovernorPause,
runGovernorResume,
runGovernorStatus,
Expand All @@ -30,12 +31,13 @@ afterEach(() => {

describe("parseGovernorPauseArgs (#4851)", () => {
it("defaults to no reason and non-JSON output", () => {
expect(parseGovernorPauseArgs([])).toEqual({ json: false, reason: null });
expect(parseGovernorPauseArgs([])).toEqual({ json: false, dryRun: false, reason: null });
});

it("parses --reason and --json together", () => {
expect(parseGovernorPauseArgs(["--reason", "investigating a bad PR", "--json"])).toEqual({
it("parses --reason, --dry-run, and --json together", () => {
expect(parseGovernorPauseArgs(["--reason", "investigating a bad PR", "--dry-run", "--json"])).toEqual({
json: true,
dryRun: true,
reason: "investigating a bad PR",
});
});
Expand All @@ -54,6 +56,22 @@ describe("parseGovernorPauseArgs (#4851)", () => {
});
});

describe("parseGovernorResumeArgs (#4847)", () => {
it("defaults to non-dry-run, non-JSON output", () => {
expect(parseGovernorResumeArgs([])).toEqual({ json: false, dryRun: false });
});

it("parses --dry-run and --json together", () => {
expect(parseGovernorResumeArgs(["--dry-run", "--json"])).toEqual({ json: true, dryRun: true });
});

it("rejects an unrecognized token", () => {
expect(parseGovernorResumeArgs(["extra"])).toEqual({
error: expect.stringContaining("Usage: gittensory-miner governor resume"),
});
});
});

describe("gittensory-miner governor pause/resume/status CLI (#4851)", () => {
it("pauses with a reason, then resumes, using an injected governor state", async () => {
const governorState = tempGovernorState();
Expand Down Expand Up @@ -107,6 +125,50 @@ describe("gittensory-miner governor pause/resume/status CLI (#4851)", () => {
expect(String(error.mock.calls[0]?.[0])).toContain("Usage: gittensory-miner governor status");
});

it("#4847: --dry-run reports what pause/resume would do and returns 0 without opening the governor state", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
const openGovernorStateSpy = vi.fn();

expect(
await runGovernorPause(["--reason", "operator requested", "--dry-run", "--json"], {
openGovernorState: openGovernorStateSpy,
}),
).toBe(0);
expect(openGovernorStateSpy).not.toHaveBeenCalled();
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({
outcome: "dry_run",
paused: true,
reason: "operator requested",
});

log.mockClear();
expect(await runGovernorPause(["--dry-run"], { openGovernorState: openGovernorStateSpy })).toBe(0);
expect(openGovernorStateSpy).not.toHaveBeenCalled();
expect(String(log.mock.calls[0]?.[0])).toBe("DRY RUN: would pause the governor. No governor-state write was made.");

log.mockClear();
expect(
await runGovernorPause(["--reason", "operator requested", "--dry-run"], {
openGovernorState: openGovernorStateSpy,
}),
).toBe(0);
expect(String(log.mock.calls[0]?.[0])).toBe(
"DRY RUN: would pause the governor (operator requested). No governor-state write was made.",
);

log.mockClear();
expect(
await runGovernorResume(["--dry-run", "--json"], { openGovernorState: openGovernorStateSpy }),
).toBe(0);
expect(openGovernorStateSpy).not.toHaveBeenCalled();
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({ outcome: "dry_run", paused: false });

log.mockClear();
expect(await runGovernorResume(["--dry-run"], { openGovernorState: openGovernorStateSpy })).toBe(0);
expect(openGovernorStateSpy).not.toHaveBeenCalled();
expect(String(log.mock.calls[0]?.[0])).toBe("DRY RUN: would resume the governor. No governor-state write was made.");
});

it("rejects an unknown pause option before opening any store", async () => {
const openGovernorStateFn = vi.fn();
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
Expand Down