fix(cli): paginate cron show lookup

This commit is contained in:
Ayaan Zaidi
2026-04-21 11:45:49 +05:30
parent 5579fef673
commit 19e451dc75
2 changed files with 100 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import type { CronJob } from "../cron/types.js";
import { registerCronCli } from "./cron-cli.js";
const CRON_CLI_TEST_TIMEOUT_MS = 15_000;
@@ -93,6 +94,22 @@ function buildProgram() {
return program;
}
function createCronJob(id: string, name: string): CronJob {
const now = Date.now();
return {
id,
name,
enabled: true,
createdAtMs: now,
updatedAtMs: now,
schedule: { kind: "at", at: new Date(now + 3_600_000).toISOString() },
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payload: { kind: "agentTurn", message: "hello" },
state: {},
};
}
function resetGatewayMock() {
callGatewayFromCli.mockClear();
callGatewayFromCli.mockImplementation(defaultGatewayMock);
@@ -387,6 +404,54 @@ describe("cron cli", () => {
expect(patch.enabled).toBe(expectedEnabled);
});
it("paginates cron show lookups", async () => {
resetGatewayMock();
callGatewayFromCli.mockImplementation(
async (method: string, _opts: unknown, params?: unknown) => {
if (method === "cron.status") {
return { enabled: true };
}
if (method === "cron.list") {
const offset = (params as { offset?: number }).offset ?? 0;
if (offset === 0) {
return {
jobs: [createCronJob("first-page", "First Page")],
hasMore: true,
nextOffset: 200,
};
}
return {
jobs: [createCronJob("target-job", "Target Job")],
hasMore: false,
nextOffset: null,
deliveryPreviews: {
"target-job": {
label: "announce -> telegram:-100",
detail: "resolved from last, main session",
},
},
};
}
return { ok: true, params };
},
);
const program = buildProgram();
await program.parseAsync(["cron", "show", "Target Job"], { from: "user" });
const listParams = callGatewayFromCli.mock.calls
.filter((call) => call[0] === "cron.list")
.map((call) => call[2]);
expect(listParams).toEqual([
{ includeDisabled: true, limit: 200, offset: 0 },
{ includeDisabled: true, limit: 200, offset: 200 },
]);
expect(defaultRuntime.log).toHaveBeenCalledWith("id: target-job");
expect(defaultRuntime.log).toHaveBeenCalledWith(
"delivery: announce -> telegram:-100 (resolved from last, main session)",
);
});
it("sends agent id on cron add", async () => {
await runCronCommand([
"cron",

View File

@@ -1,7 +1,8 @@
import type { Command } from "commander";
import type { CronJob } from "../../cron/types.js";
import type { CronDeliveryPreview, CronJob } from "../../cron/types.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import {
coerceCronDeliveryPreviews,
@@ -11,7 +12,9 @@ import {
warnIfCronSchedulerDisabled,
} from "./shared.js";
function findCronJobForShow(jobs: CronJob[], idOrName: string): CronJob | undefined {
const CRON_SHOW_PAGE_SIZE = 200;
function findCronJobInPage(jobs: CronJob[], idOrName: string): CronJob | undefined {
const needle = normalizeLowercaseStringOrEmpty(idOrName);
return jobs.find(
(job) =>
@@ -20,6 +23,34 @@ function findCronJobForShow(jobs: CronJob[], idOrName: string): CronJob | undefi
);
}
async function loadCronJobForShow(
opts: GatewayRpcOpts,
idOrName: string,
): Promise<{ job?: CronJob; deliveryPreview?: CronDeliveryPreview }> {
let offset = 0;
for (;;) {
const res = await callGatewayFromCli("cron.list", opts, {
includeDisabled: true,
limit: CRON_SHOW_PAGE_SIZE,
offset,
});
const page = res as {
jobs?: CronJob[];
hasMore?: boolean;
nextOffset?: number | null;
};
const jobs = page.jobs ?? [];
const job = findCronJobInPage(jobs, idOrName);
if (job) {
return { job, deliveryPreview: coerceCronDeliveryPreviews(res).get(job.id) };
}
if (!page.hasMore || typeof page.nextOffset !== "number") {
return {};
}
offset = page.nextOffset;
}
}
function registerCronToggleCommand(params: {
cron: Command;
name: "enable" | "disable";
@@ -86,9 +117,7 @@ export function registerCronSimpleCommands(cron: Command) {
.option("--json", "Output JSON", false)
.action(async (id, opts) => {
try {
const res = await callGatewayFromCli("cron.list", opts, { includeDisabled: true });
const jobs = (res as { jobs?: CronJob[] } | null)?.jobs ?? [];
const job = findCronJobForShow(jobs, String(id));
const { job, deliveryPreview } = await loadCronJobForShow(opts, String(id));
if (!job) {
throw new Error(`cron job not found: ${String(id)}`);
}
@@ -96,8 +125,7 @@ export function registerCronSimpleCommands(cron: Command) {
printCronJson(job);
return;
}
const deliveryPreviews = coerceCronDeliveryPreviews(res);
printCronShow(job, defaultRuntime, { deliveryPreview: deliveryPreviews.get(job.id) });
printCronShow(job, defaultRuntime, { deliveryPreview });
} catch (err) {
handleCronCliError(err);
}