mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 23:14:31 +00:00
Cron: guard missing expr in schedule parsing
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- 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/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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ describe("cron schedule", () => {
|
|||||||
expect(next).toBe(Date.parse("2025-12-17T17:00:00.000Z"));
|
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", () => {
|
it("computes next run for every schedule", () => {
|
||||||
const anchor = Date.parse("2025-12-13T00:00:00.000Z");
|
const anchor = Date.parse("2025-12-13T00:00:00.000Z");
|
||||||
const now = anchor + 10_000;
|
const now = anchor + 10_000;
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
|
|||||||
return anchor + steps * everyMs;
|
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) {
|
if (!expr) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,4 +186,19 @@ describe("cron schedule error isolation", () => {
|
|||||||
expect(badJob.state.lastError).toMatch(/^schedule error:/);
|
expect(badJob.state.lastError).toMatch(/^schedule error:/);
|
||||||
expect(badJob.state.lastError).toBeTruthy();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,4 +33,13 @@ describe("cron stagger helpers", () => {
|
|||||||
expect(resolveCronStaggerMs({ kind: "cron", expr: "0 * * * *", staggerMs: 0 })).toBe(0);
|
expect(resolveCronStaggerMs({ kind: "cron", expr: "0 * * * *", staggerMs: 0 })).toBe(0);
|
||||||
expect(resolveCronStaggerMs({ kind: "cron", expr: "15 * * * *" })).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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,5 +41,7 @@ export function resolveCronStaggerMs(schedule: Extract<CronSchedule, { kind: "cr
|
|||||||
if (explicit !== undefined) {
|
if (explicit !== undefined) {
|
||||||
return explicit;
|
return explicit;
|
||||||
}
|
}
|
||||||
return resolveDefaultCronStaggerMs(schedule.expr) ?? 0;
|
const expr = (schedule as { expr?: unknown }).expr;
|
||||||
|
const cronExpr = typeof expr === "string" ? expr : "";
|
||||||
|
return resolveDefaultCronStaggerMs(cronExpr) ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user