feat(cron): configurable failure alerts for repeated job errors (openclaw#24789) thanks @0xbrak

Verified:
- pnpm install --frozen-lockfile
- pnpm check
- pnpm test -- --run src/cron/service.failure-alert.test.ts src/cli/cron-cli.test.ts src/gateway/protocol/cron-validators.test.ts

Co-authored-by: 0xbrak <181251288+0xbrak@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
0xbrak
2026-03-01 09:18:15 -05:00
committed by GitHub
parent f902697bd5
commit 4637b90c07
18 changed files with 842 additions and 1 deletions

View File

@@ -551,4 +551,53 @@ describe("cron cli", () => {
it("rejects --exact on edit when existing job is not cron", async () => {
await expectCronEditWithScheduleLookupExit({ kind: "every", everyMs: 60_000 }, ["--exact"]);
});
it("patches failure alert settings on cron edit", async () => {
callGatewayFromCli.mockClear();
const program = buildProgram();
await program.parseAsync(
[
"cron",
"edit",
"job-1",
"--failure-alert-after",
"3",
"--failure-alert-cooldown",
"1h",
"--failure-alert-channel",
"telegram",
"--failure-alert-to",
"19098680",
],
{ from: "user" },
);
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: {
failureAlert?: { after?: number; cooldownMs?: number; channel?: string; to?: string };
};
};
expect(patch?.patch?.failureAlert?.after).toBe(3);
expect(patch?.patch?.failureAlert?.cooldownMs).toBe(3_600_000);
expect(patch?.patch?.failureAlert?.channel).toBe("telegram");
expect(patch?.patch?.failureAlert?.to).toBe("19098680");
});
it("supports --no-failure-alert on cron edit", async () => {
callGatewayFromCli.mockClear();
const program = buildProgram();
await program.parseAsync(["cron", "edit", "job-1", "--no-failure-alert"], {
from: "user",
});
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as { patch?: { failureAlert?: boolean } };
expect(patch?.patch?.failureAlert).toBe(false);
});
});