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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,15 @@ export function registerCronEditCommand(cron: Command) {
|
||||
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
|
||||
.option("--best-effort-deliver", "Do not fail job if delivery fails")
|
||||
.option("--no-best-effort-deliver", "Fail job when delivery fails")
|
||||
.option("--failure-alert", "Enable failure alerts for this job")
|
||||
.option("--no-failure-alert", "Disable failure alerts for this job")
|
||||
.option("--failure-alert-after <n>", "Alert after N consecutive job errors")
|
||||
.option(
|
||||
"--failure-alert-channel <channel>",
|
||||
`Failure alert channel (${getCronChannelOptions()})`,
|
||||
)
|
||||
.option("--failure-alert-to <dest>", "Failure alert destination")
|
||||
.option("--failure-alert-cooldown <duration>", "Minimum time between alerts (e.g. 1h, 30m)")
|
||||
.action(async (id, opts) => {
|
||||
try {
|
||||
if (opts.session === "main" && opts.message) {
|
||||
@@ -264,6 +273,49 @@ export function registerCronEditCommand(cron: Command) {
|
||||
patch.delivery = delivery;
|
||||
}
|
||||
|
||||
const hasFailureAlertAfter = typeof opts.failureAlertAfter === "string";
|
||||
const hasFailureAlertChannel = typeof opts.failureAlertChannel === "string";
|
||||
const hasFailureAlertTo = typeof opts.failureAlertTo === "string";
|
||||
const hasFailureAlertCooldown = typeof opts.failureAlertCooldown === "string";
|
||||
const hasFailureAlertFields =
|
||||
hasFailureAlertAfter ||
|
||||
hasFailureAlertChannel ||
|
||||
hasFailureAlertTo ||
|
||||
hasFailureAlertCooldown;
|
||||
const failureAlertFlag =
|
||||
typeof opts.failureAlert === "boolean" ? opts.failureAlert : undefined;
|
||||
if (failureAlertFlag === false && hasFailureAlertFields) {
|
||||
throw new Error("Use --no-failure-alert alone (without failure-alert-* options).");
|
||||
}
|
||||
if (failureAlertFlag === false) {
|
||||
patch.failureAlert = false;
|
||||
} else if (failureAlertFlag === true || hasFailureAlertFields) {
|
||||
const failureAlert: Record<string, unknown> = {};
|
||||
if (hasFailureAlertAfter) {
|
||||
const after = Number.parseInt(String(opts.failureAlertAfter), 10);
|
||||
if (!Number.isFinite(after) || after <= 0) {
|
||||
throw new Error("Invalid --failure-alert-after (must be a positive integer).");
|
||||
}
|
||||
failureAlert.after = after;
|
||||
}
|
||||
if (hasFailureAlertChannel) {
|
||||
const channel = String(opts.failureAlertChannel).trim().toLowerCase();
|
||||
failureAlert.channel = channel ? channel : undefined;
|
||||
}
|
||||
if (hasFailureAlertTo) {
|
||||
const to = String(opts.failureAlertTo).trim();
|
||||
failureAlert.to = to ? to : undefined;
|
||||
}
|
||||
if (hasFailureAlertCooldown) {
|
||||
const cooldownMs = parseDurationMs(String(opts.failureAlertCooldown));
|
||||
if (!cooldownMs && cooldownMs !== 0) {
|
||||
throw new Error("Invalid --failure-alert-cooldown.");
|
||||
}
|
||||
failureAlert.cooldownMs = cooldownMs;
|
||||
}
|
||||
patch.failureAlert = failureAlert;
|
||||
}
|
||||
|
||||
const res = await callGatewayFromCli("cron.update", opts, {
|
||||
id,
|
||||
patch,
|
||||
|
||||
Reference in New Issue
Block a user