Cron: guard missing expr in schedule parsing

This commit is contained in:
Vignesh Natarajan
2026-02-21 20:18:11 -08:00
parent eea0a68199
commit 961bde27fe
6 changed files with 45 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Agents/Ollama: preserve unsafe integer tool-call arguments as exact strings during NDJSON parsing, preventing large numeric IDs from being rounded before tool execution. (#23170) Thanks @BestJoester.
- Cron/Gateway: keep `cron.list` and `cron.status` responsive during startup catch-up by avoiding a long-held cron lock while missed jobs execute. (#23106) Thanks @jayleekr.
- Gateway/Config reload: compare array-valued config paths structurally during diffing so unchanged `memory.qmd.paths` and `memory.qmd.scope.rules` no longer trigger false restart-required reloads. (#23185) Thanks @rex05ai.
- Cron/Scheduling: validate runtime cron expressions before schedule/stagger evaluation so malformed persisted jobs report a clear `invalid cron schedule: expr is required` error instead of crashing with `undefined.trim` failures and auto-disable churn. (#23223) Thanks @asimons81.
- TUI/Input: enable multiline-paste burst coalescing on macOS Terminal.app and iTerm so pasted blocks no longer submit line-by-line as separate messages. (#18809) Thanks @fwends.
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.

View File

@@ -13,6 +13,18 @@ describe("cron schedule", () => {
expect(next).toBe(Date.parse("2025-12-17T17:00:00.000Z"));
});
it("throws a clear error when cron expr is missing at runtime", () => {
const nowMs = Date.parse("2025-12-13T00:00:00.000Z");
expect(() =>
computeNextRunAtMs(
{
kind: "cron",
} as unknown as { kind: "cron"; expr: string; tz?: string },
nowMs,
),
).toThrow("invalid cron schedule: expr is required");
});
it("computes next run for every schedule", () => {
const anchor = Date.parse("2025-12-13T00:00:00.000Z");
const now = anchor + 10_000;

View File

@@ -41,7 +41,11 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
return anchor + steps * everyMs;
}
const expr = schedule.expr.trim();
const exprSource = (schedule as { expr?: unknown }).expr;
if (typeof exprSource !== "string") {
throw new Error("invalid cron schedule: expr is required");
}
const expr = exprSource.trim();
if (!expr) {
return undefined;
}

View File

@@ -186,4 +186,19 @@ describe("cron schedule error isolation", () => {
expect(badJob.state.lastError).toMatch(/^schedule error:/);
expect(badJob.state.lastError).toBeTruthy();
});
it("records a clear schedule error when cron expr is missing", () => {
const badJob = createJob({
id: "missing-expr",
name: "Missing Expr",
schedule: { kind: "cron" } as unknown as CronJob["schedule"],
});
const state = createMockState([badJob]);
recomputeNextRuns(state);
expect(badJob.state.lastError).toContain("invalid cron schedule: expr is required");
expect(badJob.state.lastError).not.toContain("Cannot read properties of undefined");
expect(badJob.state.scheduleErrorCount).toBe(1);
});
});

View File

@@ -33,4 +33,13 @@ describe("cron stagger helpers", () => {
expect(resolveCronStaggerMs({ kind: "cron", expr: "0 * * * *", staggerMs: 0 })).toBe(0);
expect(resolveCronStaggerMs({ kind: "cron", expr: "15 * * * *" })).toBe(0);
});
it("handles missing runtime expr values without throwing", () => {
expect(() =>
resolveCronStaggerMs({ kind: "cron" } as unknown as { kind: "cron"; expr: string }),
).not.toThrow();
expect(
resolveCronStaggerMs({ kind: "cron" } as unknown as { kind: "cron"; expr: string }),
).toBe(0);
});
});

View File

@@ -41,5 +41,7 @@ export function resolveCronStaggerMs(schedule: Extract<CronSchedule, { kind: "cr
if (explicit !== undefined) {
return explicit;
}
return resolveDefaultCronStaggerMs(schedule.expr) ?? 0;
const expr = (schedule as { expr?: unknown }).expr;
const cronExpr = typeof expr === "string" ? expr : "";
return resolveDefaultCronStaggerMs(cronExpr) ?? 0;
}