mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:58:26 +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:
87
src/cron/service.get-job.test.ts
Normal file
87
src/cron/service.get-job.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CronService } from "./service.js";
|
||||
import {
|
||||
createCronStoreHarness,
|
||||
createNoopLogger,
|
||||
installCronTestHooks,
|
||||
} from "./service.test-harness.js";
|
||||
|
||||
const logger = createNoopLogger();
|
||||
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-get-job-" });
|
||||
installCronTestHooks({ logger });
|
||||
|
||||
function createCronService(storePath: string) {
|
||||
return new CronService({
|
||||
storePath,
|
||||
cronEnabled: true,
|
||||
log: logger,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })),
|
||||
});
|
||||
}
|
||||
|
||||
describe("CronService.getJob", () => {
|
||||
it("returns added jobs and undefined for missing ids", async () => {
|
||||
const { storePath } = await makeStorePath();
|
||||
const cron = createCronService(storePath);
|
||||
await cron.start();
|
||||
|
||||
try {
|
||||
const added = await cron.add({
|
||||
name: "lookup-test",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "ping" },
|
||||
});
|
||||
|
||||
expect(cron.getJob(added.id)?.id).toBe(added.id);
|
||||
expect(cron.getJob("missing-job-id")).toBeUndefined();
|
||||
} finally {
|
||||
cron.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves notify on create for true, false, and omitted", async () => {
|
||||
const { storePath } = await makeStorePath();
|
||||
const cron = createCronService(storePath);
|
||||
await cron.start();
|
||||
|
||||
try {
|
||||
const notifyTrue = await cron.add({
|
||||
name: "notify-true",
|
||||
enabled: true,
|
||||
notify: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "ping" },
|
||||
});
|
||||
const notifyFalse = await cron.add({
|
||||
name: "notify-false",
|
||||
enabled: true,
|
||||
notify: false,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "ping" },
|
||||
});
|
||||
const notifyOmitted = await cron.add({
|
||||
name: "notify-omitted",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "ping" },
|
||||
});
|
||||
|
||||
expect(cron.getJob(notifyTrue.id)?.notify).toBe(true);
|
||||
expect(cron.getJob(notifyFalse.id)?.notify).toBe(false);
|
||||
expect(cron.getJob(notifyOmitted.id)?.notify).toBeUndefined();
|
||||
} finally {
|
||||
cron.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -100,4 +100,24 @@ describe("applyJobPatch", () => {
|
||||
bestEffort: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates notify via patch", () => {
|
||||
const now = Date.now();
|
||||
const job: CronJob = {
|
||||
id: "job-4",
|
||||
name: "job-4",
|
||||
enabled: true,
|
||||
notify: false,
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "do it" },
|
||||
state: {},
|
||||
};
|
||||
|
||||
expect(() => applyJobPatch(job, { notify: true })).not.toThrow();
|
||||
expect(job.notify).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
||||
import type { CronJob, CronJobCreate, CronJobPatch } from "./types.js";
|
||||
import * as ops from "./service/ops.js";
|
||||
import { type CronServiceDeps, createCronServiceState } from "./service/state.js";
|
||||
|
||||
@@ -42,6 +42,10 @@ export class CronService {
|
||||
return await ops.run(this.state, id, mode);
|
||||
}
|
||||
|
||||
getJob(id: string): CronJob | undefined {
|
||||
return this.state.store?.jobs.find((job) => job.id === id);
|
||||
}
|
||||
|
||||
wake(opts: { mode: "now" | "next-heartbeat"; text: string }) {
|
||||
return ops.wakeNow(this.state, opts);
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
|
||||
name: normalizeRequiredName(input.name),
|
||||
description: normalizeOptionalText(input.description),
|
||||
enabled,
|
||||
notify: typeof input.notify === "boolean" ? input.notify : undefined,
|
||||
deleteAfterRun,
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
@@ -284,6 +285,9 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
|
||||
if (typeof patch.enabled === "boolean") {
|
||||
job.enabled = patch.enabled;
|
||||
}
|
||||
if (typeof patch.notify === "boolean") {
|
||||
job.notify = patch.notify;
|
||||
}
|
||||
if (typeof patch.deleteAfterRun === "boolean") {
|
||||
job.deleteAfterRun = patch.deleteAfterRun;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export type CronJob = {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
notify?: boolean;
|
||||
deleteAfterRun?: boolean;
|
||||
createdAtMs: number;
|
||||
updatedAtMs: number;
|
||||
|
||||
Reference in New Issue
Block a user