Skip to content
Closed
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
7 changes: 7 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { runManagePoll } from "../lib/manage-poll.js";
import { runManageStatus } from "../lib/manage-status.js";
import { runPlanCli } from "../lib/plan-store-cli.js";
import { runQueueCli } from "../lib/portfolio-queue-cli.js";
import { runRejectCli } from "../lib/rejection-render.js";
import { runStateCli } from "../lib/run-state-cli.js";
import { runDoctor, runStatus } from "../lib/status.js";
import {
Expand Down Expand Up @@ -88,6 +89,12 @@ if (cliArgs[0] === "hooks" && cliArgs[1] === "check") {
process.exit(exitCode);
}

if (cliArgs[0] === "reject") {
const exitCode = runRejectCli(cliArgs[1], cliArgs.slice(2));
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
}

if (cliArgs[0] === "state") {
const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2));
await awaitOpportunisticUpdateCheck(updateCheck);
Expand Down
2 changes: 2 additions & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function printHelp(input) {
" gittensory-miner plan show <planId> [--json]",
" gittensory-miner governor list [--repo <owner/repo>] [--type allowed|denied|throttled|kill_switch] [--json]",
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
" gittensory-miner reject render --reason <reason> --repo <owner/repo> --pr <number> [--json]",
" gittensory-miner reject reasons [--json]",
" gittensory-miner state get <owner/repo> [--json]",
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--json]",
"",
Expand Down
20 changes: 20 additions & 0 deletions packages/gittensory-miner/lib/rejection-render.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type ParsedRejectRenderArgs =
| {
reason: string;
repo: string;
prNumber: number;
json: boolean;
}
| { error: string };

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

export function parseRejectRenderArgs(args: string[]): ParsedRejectRenderArgs;

export function parseRejectReasonsArgs(args: string[]): ParsedRejectReasonsArgs;

export function runRejectRender(args: string[]): number;

export function runRejectReasons(args: string[]): number;

export function runRejectCli(subcommand: string | undefined, args: string[]): number;
128 changes: 128 additions & 0 deletions packages/gittensory-miner/lib/rejection-render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { REJECTION_REASONS, renderRejectionMessage } from "./rejection-templates.js";

const REJECT_RENDER_USAGE =
"Usage: gittensory-miner reject render --reason <gate_close|maintainer_close_no_reason|superseded_by_duplicate> --repo <owner/repo> --pr <number> [--json]";

function parsePositiveInt(flag, value) {
if (value === undefined) {
return { error: `Missing value for ${flag}.` };
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
return { error: `Invalid value for ${flag}: must be a positive integer.` };
}
return { value: parsed };
}

export function parseRejectRenderArgs(args) {
const options = {
json: false,
reason: undefined,
repo: undefined,
prNumber: undefined,
};

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--reason") {
const reason = args[++index];
if (!reason) return { error: "Missing value for --reason." };
options.reason = reason;
continue;
}
if (token === "--repo") {
const repo = args[++index];
if (!repo) return { error: "Missing value for --repo." };
options.repo = repo;
continue;
}
if (token === "--pr") {
const parsed = parsePositiveInt("--pr", args[++index]);
if ("error" in parsed) return { error: parsed.error };
options.prNumber = parsed.value;
continue;
}
if (token.startsWith("-")) {
return { error: `Unknown option: ${token}` };
}
return { error: REJECT_RENDER_USAGE };
}

if (!options.reason || !options.repo || options.prNumber === undefined) {
return { error: REJECT_RENDER_USAGE };
}

return options;
}

export function parseRejectReasonsArgs(args) {
if (args.length === 1 && args[0] === "--json") {
return { json: true };
}
if (args.length === 0) {
return { json: false };
}
if (args.length === 1 && args[0].startsWith("-")) {
return { error: `Unknown option: ${args[0]}` };
}
return { error: "Usage: gittensory-miner reject reasons [--json]" };
}

export function runRejectRender(args) {
const parsed = parseRejectRenderArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

try {
const message = renderRejectionMessage(parsed.reason, {
repoFullName: parsed.repo,
prNumber: parsed.prNumber,
});
if (parsed.json) {
console.log(
JSON.stringify({
reason: parsed.reason,
repoFullName: parsed.repo,
prNumber: parsed.prNumber,
message,
}),
);
} else {
console.log(message);
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

export function runRejectReasons(args) {
const parsed = parseRejectReasonsArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

if (parsed.json) {
console.log(JSON.stringify({ reasons: [...REJECTION_REASONS] }));
} else {
for (const reason of REJECTION_REASONS) {
console.log(reason);
}
}
return 0;
}

export function runRejectCli(subcommand, args) {
if (subcommand === "render") return runRejectRender(args);
if (subcommand === "reasons") return runRejectReasons(args);
console.error(`Unknown reject subcommand: ${subcommand ?? ""}. ${REJECT_RENDER_USAGE}`);
return 2;
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"lib"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/rejection-render.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "0.1.0"
Expand Down
108 changes: 108 additions & 0 deletions test/unit/miner-cli-rejection-render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
parseRejectReasonsArgs,
parseRejectRenderArgs,
runRejectCli,
runRejectReasons,
runRejectRender,
} from "../../packages/gittensory-miner/lib/rejection-render.js";

afterEach(() => {
vi.restoreAllMocks();
});

describe("gittensory-miner reject render command", () => {
it("parseRejectRenderArgs validates required flags", () => {
expect(parseRejectRenderArgs([])).toEqual({
error: expect.stringContaining("Usage: gittensory-miner reject render"),
});
expect(parseRejectRenderArgs(["--reason", "gate_close"])).toEqual({
error: expect.stringContaining("Usage: gittensory-miner reject render"),
});
expect(parseRejectRenderArgs(["--reason", "gate_close", "--repo", "acme/widgets", "--pr", "0"])).toEqual({
error: "Invalid value for --pr: must be a positive integer.",
});
expect(
parseRejectRenderArgs([
"--reason",
"gate_close",
"--repo",
"acme/widgets",
"--pr",
"42",
"--json",
]),
).toEqual({
reason: "gate_close",
repo: "acme/widgets",
prNumber: 42,
json: true,
});
});

it("parseRejectReasonsArgs accepts optional --json", () => {
expect(parseRejectReasonsArgs([])).toEqual({ json: false });
expect(parseRejectReasonsArgs(["--json"])).toEqual({ json: true });
expect(parseRejectReasonsArgs(["--wat"])).toEqual({ error: "Unknown option: --wat" });
});

it("runRejectRender prints a courtesy note and exits 0", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
expect(
runRejectRender([
"--reason",
"gate_close",
"--repo",
"JSONbored/gittensory",
"--pr",
"42",
]),
).toBe(0);
expect(log).toHaveBeenCalledWith(expect.stringContaining("#42"));
expect(log).toHaveBeenCalledWith(expect.stringContaining("JSONbored/gittensory"));
});

it("runRejectRender prints JSON when requested", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
expect(
runRejectRender([
"--reason",
"superseded_by_duplicate",
"--repo",
"JSONbored/gittensory",
"--pr",
"7",
"--json",
]),
).toBe(0);
expect(log).toHaveBeenCalledWith(expect.stringContaining('"reason":"superseded_by_duplicate"'));
});

it("runRejectRender returns exit code 2 for invalid reasons or flags", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
expect(runRejectRender(["--reason", "bogus", "--repo", "acme/widgets", "--pr", "1"])).toBe(2);
expect(error).toHaveBeenCalledWith("invalid_rejection_reason");
expect(runRejectRender(["--reason", "gate_close", "--repo", "bad", "--pr", "1"])).toBe(2);
expect(runRejectRender(["--reason", "gate_close", "--repo", "acme/widgets", "--pr", "nope"])).toBe(2);
});

it("runRejectReasons lists reason buckets", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
expect(runRejectReasons([])).toBe(0);
expect(log).toHaveBeenCalledWith("gate_close");
expect(runRejectReasons(["--json"])).toBe(0);
expect(log).toHaveBeenCalledWith(expect.stringContaining('"gate_close"'));
});

it("runRejectCli routes render and reasons subcommands", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
expect(
runRejectCli("render", ["--reason", "gate_close", "--repo", "JSONbored/gittensory", "--pr", "1"]),
).toBe(0);
expect(runRejectCli("reasons", [])).toBe(0);
expect(log).toHaveBeenCalled();
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
expect(runRejectCli("wat", [])).toBe(2);
expect(error).toHaveBeenCalledWith(expect.stringContaining("Unknown reject subcommand"));
});
});
Loading