mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
gateway: add cron finished-run webhook (#14535)
* gateway: add cron finished webhook delivery * config: allow cron webhook in runtime schema * cron: require notify flag for webhook posts * ui/docs: add cron notify toggle and webhook docs * fix: harden cron webhook auth and fill notify coverage (#14535) (thanks @advaitpaliwal) --------- Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
This commit is contained in:
@@ -15,6 +15,7 @@ export const DEFAULT_CRON_FORM: CronFormState = {
|
||||
description: "",
|
||||
agentId: "",
|
||||
enabled: true,
|
||||
notify: false,
|
||||
scheduleKind: "every",
|
||||
scheduleAt: "",
|
||||
everyAmount: "30",
|
||||
|
||||
63
ui/src/ui/controllers/cron.test.ts
Normal file
63
ui/src/ui/controllers/cron.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_CRON_FORM } from "../app-defaults.ts";
|
||||
import { addCronJob, type CronState } from "./cron.ts";
|
||||
|
||||
function createState(overrides: Partial<CronState> = {}): CronState {
|
||||
return {
|
||||
client: null,
|
||||
connected: true,
|
||||
cronLoading: false,
|
||||
cronJobs: [],
|
||||
cronStatus: null,
|
||||
cronError: null,
|
||||
cronForm: { ...DEFAULT_CRON_FORM },
|
||||
cronRunsJobId: null,
|
||||
cronRuns: [],
|
||||
cronBusy: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("cron controller", () => {
|
||||
it("forwards notify in cron.add payload", async () => {
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "cron.add") {
|
||||
return { id: "job-1" };
|
||||
}
|
||||
if (method === "cron.list") {
|
||||
return { jobs: [] };
|
||||
}
|
||||
if (method === "cron.status") {
|
||||
return { enabled: true, jobs: 0, nextWakeAtMs: null };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const state = createState({
|
||||
client: {
|
||||
request,
|
||||
} as unknown as CronState["client"],
|
||||
cronForm: {
|
||||
...DEFAULT_CRON_FORM,
|
||||
name: "notify job",
|
||||
notify: true,
|
||||
scheduleKind: "every",
|
||||
everyAmount: "1",
|
||||
everyUnit: "minutes",
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payloadKind: "systemEvent",
|
||||
payloadText: "ping",
|
||||
},
|
||||
});
|
||||
|
||||
await addCronJob(state);
|
||||
|
||||
const addCall = request.mock.calls.find(([method]) => method === "cron.add");
|
||||
expect(addCall).toBeDefined();
|
||||
expect(addCall?.[1]).toMatchObject({
|
||||
notify: true,
|
||||
name: "notify job",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,7 @@ export async function addCronJob(state: CronState) {
|
||||
description: state.cronForm.description.trim() || undefined,
|
||||
agentId: agentId || undefined,
|
||||
enabled: state.cronForm.enabled,
|
||||
notify: state.cronForm.notify,
|
||||
schedule,
|
||||
sessionTarget: state.cronForm.sessionTarget,
|
||||
wakeMode: state.cronForm.wakeMode,
|
||||
|
||||
@@ -473,6 +473,7 @@ export type CronJob = {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
notify?: boolean;
|
||||
deleteAfterRun?: boolean;
|
||||
createdAtMs: number;
|
||||
updatedAtMs: number;
|
||||
|
||||
@@ -19,6 +19,7 @@ export type CronFormState = {
|
||||
description: string;
|
||||
agentId: string;
|
||||
enabled: boolean;
|
||||
notify: boolean;
|
||||
scheduleKind: "at" | "every" | "cron";
|
||||
scheduleAt: string;
|
||||
everyAmount: string;
|
||||
|
||||
@@ -158,4 +158,50 @@ describe("cron view", () => {
|
||||
expect(summaries[0]).toBe("newer run");
|
||||
expect(summaries[1]).toBe("older run");
|
||||
});
|
||||
|
||||
it("forwards notify checkbox updates from the form", () => {
|
||||
const container = document.createElement("div");
|
||||
const onFormChange = vi.fn();
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
onFormChange,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const notifyLabel = Array.from(container.querySelectorAll("label.field.checkbox")).find(
|
||||
(label) => label.querySelector("span")?.textContent?.trim() === "Notify webhook",
|
||||
);
|
||||
const notifyInput =
|
||||
notifyLabel?.querySelector<HTMLInputElement>('input[type="checkbox"]') ?? null;
|
||||
expect(notifyInput).not.toBeNull();
|
||||
|
||||
if (!notifyInput) {
|
||||
return;
|
||||
}
|
||||
notifyInput.checked = true;
|
||||
notifyInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
expect(onFormChange).toHaveBeenCalledWith({ notify: true });
|
||||
});
|
||||
|
||||
it("shows notify chip for webhook-enabled jobs", () => {
|
||||
const container = document.createElement("div");
|
||||
const job = { ...createJob("job-2"), notify: true };
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
jobs: [job],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const chips = Array.from(container.querySelectorAll(".chip")).map((el) =>
|
||||
(el.textContent ?? "").trim(),
|
||||
);
|
||||
expect(chips).toContain("notify");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,15 @@ export function renderCron(props: CronProps) {
|
||||
props.onFormChange({ enabled: (e.target as HTMLInputElement).checked })}
|
||||
/>
|
||||
</label>
|
||||
<label class="field checkbox">
|
||||
<span>Notify webhook</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${props.form.notify}
|
||||
@change=${(e: Event) =>
|
||||
props.onFormChange({ notify: (e.target as HTMLInputElement).checked })}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Schedule</span>
|
||||
<select
|
||||
@@ -398,6 +407,13 @@ function renderJob(job: CronJob, props: CronProps) {
|
||||
<span class=${`chip ${job.enabled ? "chip-ok" : "chip-danger"}`}>
|
||||
${job.enabled ? "enabled" : "disabled"}
|
||||
</span>
|
||||
${
|
||||
job.notify
|
||||
? html`
|
||||
<span class="chip">notify</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span class="chip">${job.sessionTarget}</span>
|
||||
<span class="chip">${job.wakeMode}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user