From 0e3cc2e5adaf02f1e4473968db2c8f33bd32b29e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 17:33:11 -0400 Subject: [PATCH] fix(cli): clamp cron wait poll timers --- src/cli/cron-cli.test.ts | 42 ++++++++++++++++++++++++ src/cli/cron-cli/register.cron-simple.ts | 13 +++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 7726de99a5a..8dbddf3aeee 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -354,6 +354,48 @@ describe("cron cli", () => { expect(callGatewayFromCli.mock.calls.some((call) => call[0] === "cron.run")).toBe(false); }); + it("bounds oversized cron run wait poll intervals by the wait timeout", async () => { + vi.useFakeTimers(); + resetGatewayMock(); + callGatewayFromCli.mockImplementation( + async (method: string, _opts: unknown, params?: unknown) => { + if (method === "cron.status") { + return { enabled: true }; + } + if (method === "cron.run") { + return { ok: true, enqueued: true, runId: "manual:job-1:123:0", params }; + } + if (method === "cron.runs") { + return { entries: [] }; + } + return { ok: true, params }; + }, + ); + const program = buildProgram(); + const run = program.parseAsync( + [ + "cron", + "run", + "job-1", + "--wait", + "--wait-timeout", + "10ms", + "--poll-interval", + "999999999999999ms", + ], + { from: "user" }, + ); + + await vi.waitFor(() => { + expect(callGatewayFromCli.mock.calls.some((call) => call[0] === "cron.runs")).toBe(true); + }); + + const rejection = expect(run).rejects.toThrow("__exit__:1"); + await vi.advanceTimersByTimeAsync(10); + await rejection; + expectRuntimeErrorContaining("timed out waiting for cron run"); + }); + it("trims model and thinking on cron add", { timeout: CRON_CLI_TEST_TIMEOUT_MS }, async () => { await runCronCommand([ "cron", diff --git a/src/cli/cron-cli/register.cron-simple.ts b/src/cli/cron-cli/register.cron-simple.ts index 979f36a84d8..217fd0ce862 100644 --- a/src/cli/cron-cli/register.cron-simple.ts +++ b/src/cli/cron-cli/register.cron-simple.ts @@ -1,3 +1,7 @@ +import { + resolvePositiveTimerTimeoutMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; import type { CronDeliveryPreview, CronJob } from "../../cron/types.js"; @@ -44,7 +48,7 @@ function parseCronRunWaitDuration(raw: unknown, label: string): number { if (!Number.isFinite(durationMs) || durationMs < 0) { throw new Error(`invalid ${label}`); } - return durationMs; + return resolveTimerTimeoutMs(durationMs, 0, 0); } function parseCronRunPollInterval(raw: unknown): number { @@ -52,7 +56,7 @@ function parseCronRunPollInterval(raw: unknown): number { if (durationMs <= 0) { throw new Error("invalid --poll-interval"); } - return durationMs; + return resolvePositiveTimerTimeoutMs(durationMs, 2_000); } async function waitForCronRunCompletion(params: { @@ -73,10 +77,11 @@ async function waitForCronRunCompletion(params: { if (entry?.status === "ok" || entry?.status === "error" || entry?.status === "skipped") { return entry; } - if (Date.now() - startedAt >= params.timeoutMs) { + const elapsedMs = Date.now() - startedAt; + if (elapsedMs >= params.timeoutMs) { throw new Error(`timed out waiting for cron run ${params.runId}`); } - await sleep(params.pollIntervalMs); + await sleep(Math.min(params.pollIntervalMs, params.timeoutMs - elapsedMs)); } }