fix(cli): clamp cron wait poll timers

This commit is contained in:
Peter Steinberger
2026-05-30 17:33:11 -04:00
parent 1670b970ee
commit 0e3cc2e5ad
2 changed files with 51 additions and 4 deletions

View File

@@ -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",

View File

@@ -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));
}
}