mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 04:59:49 +00:00
fix(cron-cli): bound loadCronJobForShow pagination (#83856)
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- fix(mattermost): fail closed on missing channel type [AI]. (#84091) Thanks @pgondhi987.
|
||||
- Recheck rebuilt system.run argv [AI]. (#84090) Thanks @pgondhi987.
|
||||
- CLI/cron: bound `openclaw cron show` job lookup pagination so non-advancing or unbounded `cron.list` responses fail instead of hanging the command. Fixes #83856. (#83989)
|
||||
- Agents/messages: stop message-tool-only turns after a successful source-channel `message` send while keeping transcript mirrors under the session write lock. (#84289)
|
||||
- Agents: filter silent heartbeat response-tool transcript artifacts out of embedded context snapshots so later user turns are not polluted by heartbeat no-op messages. (#83477) Thanks @fuller-stack-dev.
|
||||
- Agents/OpenAI: log repeated strict tool-schema downgrade diagnostics once per provider/model/tool signature, reducing duplicate debug noise while preserving `strict=false` fallback behavior. Fixes #82930. (#82933) Thanks @galiniliev.
|
||||
|
||||
70
src/cli/cron-cli/register.cron-simple.test.ts
Normal file
70
src/cli/cron-cli/register.cron-simple.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CronJob } from "../../cron/types.js";
|
||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||
|
||||
const callGatewayFromCli = vi.fn();
|
||||
|
||||
vi.mock("../gateway-rpc.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../gateway-rpc.js")>("../gateway-rpc.js");
|
||||
return {
|
||||
...actual,
|
||||
callGatewayFromCli: (...args: Parameters<typeof actual.callGatewayFromCli>) =>
|
||||
callGatewayFromCli(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const { loadCronJobForShow } = await import("./register.cron-simple.js");
|
||||
|
||||
const opts: GatewayRpcOpts = {} as GatewayRpcOpts;
|
||||
|
||||
describe("loadCronJobForShow pagination guard (regression for #83856)", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayFromCli.mockReset();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("throws when nextOffset fails to advance", async () => {
|
||||
callGatewayFromCli.mockResolvedValue({
|
||||
jobs: [],
|
||||
hasMore: true,
|
||||
nextOffset: 0,
|
||||
});
|
||||
await expect(loadCronJobForShow(opts, "missing")).rejects.toThrow(/pagination did not advance/);
|
||||
expect(callGatewayFromCli).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("throws when pagination exceeds the max page count", async () => {
|
||||
let nextOffset = 0;
|
||||
callGatewayFromCli.mockImplementation(async () => {
|
||||
nextOffset += 1;
|
||||
return { jobs: [], hasMore: true, nextOffset };
|
||||
});
|
||||
await expect(loadCronJobForShow(opts, "missing")).rejects.toThrow(
|
||||
/pagination exceeded maximum pages/,
|
||||
);
|
||||
expect(callGatewayFromCli.mock.calls.length).toBeGreaterThan(1);
|
||||
expect(callGatewayFromCli.mock.calls.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it("returns the job when found on a later page", async () => {
|
||||
const job: CronJob = { id: "abc", name: "wanted" } as unknown as CronJob;
|
||||
callGatewayFromCli
|
||||
.mockResolvedValueOnce({ jobs: [], hasMore: true, nextOffset: 200 })
|
||||
.mockResolvedValueOnce({ jobs: [job], hasMore: false, nextOffset: null });
|
||||
const result = await loadCronJobForShow(opts, "wanted");
|
||||
expect(result.job?.id).toBe("abc");
|
||||
expect(callGatewayFromCli).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns empty result when pagination terminates without a match", async () => {
|
||||
callGatewayFromCli.mockResolvedValueOnce({
|
||||
jobs: [],
|
||||
hasMore: false,
|
||||
nextOffset: null,
|
||||
});
|
||||
const result = await loadCronJobForShow(opts, "missing");
|
||||
expect(result.job).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "./shared.js";
|
||||
|
||||
const CRON_SHOW_PAGE_SIZE = 200;
|
||||
const CRON_SHOW_LOOKUP_MAX_PAGES = 50;
|
||||
const CRON_RUN_WAIT_TIMEOUT_DEFAULT = "10m";
|
||||
const CRON_RUN_WAIT_POLL_INTERVAL_DEFAULT = "2s";
|
||||
|
||||
@@ -87,32 +88,36 @@ function findCronJobInPage(jobs: CronJob[], idOrName: string): CronJob | undefin
|
||||
);
|
||||
}
|
||||
|
||||
async function loadCronJobForShow(
|
||||
export async function loadCronJobForShow(
|
||||
opts: GatewayRpcOpts,
|
||||
idOrName: string,
|
||||
): Promise<{ job?: CronJob; deliveryPreview?: CronDeliveryPreview }> {
|
||||
let offset = 0;
|
||||
for (;;) {
|
||||
for (let page = 0; page < CRON_SHOW_LOOKUP_MAX_PAGES; page += 1) {
|
||||
const res = await callGatewayFromCli("cron.list", opts, {
|
||||
includeDisabled: true,
|
||||
limit: CRON_SHOW_PAGE_SIZE,
|
||||
offset,
|
||||
});
|
||||
const page = res as {
|
||||
const listed = res as {
|
||||
jobs?: CronJob[];
|
||||
hasMore?: boolean;
|
||||
nextOffset?: number | null;
|
||||
};
|
||||
const jobs = page.jobs ?? [];
|
||||
const jobs = listed.jobs ?? [];
|
||||
const job = findCronJobInPage(jobs, idOrName);
|
||||
if (job) {
|
||||
return { job, deliveryPreview: coerceCronDeliveryPreviews(res).get(job.id) };
|
||||
}
|
||||
if (!page.hasMore || typeof page.nextOffset !== "number") {
|
||||
if (!listed.hasMore || typeof listed.nextOffset !== "number") {
|
||||
return {};
|
||||
}
|
||||
offset = page.nextOffset;
|
||||
if (listed.nextOffset <= offset) {
|
||||
throw new Error("cron.list pagination did not advance while looking up cron job");
|
||||
}
|
||||
offset = listed.nextOffset;
|
||||
}
|
||||
throw new Error("cron.list pagination exceeded maximum pages while looking up cron job");
|
||||
}
|
||||
|
||||
function registerCronToggleCommand(params: {
|
||||
|
||||
Reference in New Issue
Block a user