





























@@ -157,12 +157,17 @@ async function setupCronTestRun(params: {
157157}
158158159159type DirectCronState = {
160-cron: { stop: () => void };
160+cron: { start: () => Promise<void>; stop: () => void };
161161storePath: string;
162162getRuntimeConfig: () => import("../config/types.openclaw.js").OpenClawConfig;
163163};
164164165-async function createDirectCronState(): Promise<DirectCronState> {
165+type CronBroadcast = (event: string, payload: unknown) => void;
166+167+async function createDirectCronState(params?: {
168+broadcast?: CronBroadcast;
169+}): Promise<DirectCronState> {
170+resetConfigRuntimeState();
166171const [{ getRuntimeConfig }, { buildGatewayCronService }] = await Promise.all([
167172import("../config/config.js"),
168173import("./server-cron.js"),
@@ -171,12 +176,60 @@ async function createDirectCronState(): Promise<DirectCronState> {
171176 ...buildGatewayCronService({
172177cfg: getRuntimeConfig(),
173178deps: {} as never,
174-broadcast: vi.fn(),
179+broadcast: params?.broadcast ?? vi.fn(),
175180}),
176181getRuntimeConfig: getRuntimeConfig,
177182};
178183}
179184185+function createCronEventCollector() {
186+const events: Record<string, unknown>[] = [];
187+const waiters: Array<{
188+check: (payload: Record<string, unknown>) => boolean;
189+resolve: (payload: Record<string, unknown>) => void;
190+reject: (error: Error) => void;
191+timer: ReturnType<typeof setTimeout>;
192+}> = [];
193+const flush = (payload: Record<string, unknown>) => {
194+for (const waiter of [...waiters]) {
195+if (!waiter.check(payload)) {
196+continue;
197+}
198+clearTimeout(waiter.timer);
199+waiters.splice(waiters.indexOf(waiter), 1);
200+waiter.resolve(payload);
201+}
202+};
203+return {
204+broadcast(event: string, payload: unknown) {
205+if (event !== "cron" || !payload || typeof payload !== "object" || Array.isArray(payload)) {
206+return;
207+}
208+const record = payload as Record<string, unknown>;
209+events.push(record);
210+flush(record);
211+},
212+wait(check: (payload: Record<string, unknown>) => boolean, timeoutMs = CRON_WAIT_TIMEOUT_MS) {
213+const existing = events.find(check);
214+if (existing) {
215+return Promise.resolve(existing);
216+}
217+return new Promise<Record<string, unknown>>((resolve, reject) => {
218+const waiter = {
219+ check,
220+ resolve,
221+ reject,
222+timer: setTimeout(() => {
223+waiters.splice(waiters.indexOf(waiter), 1);
224+reject(new Error("timeout waiting for cron event"));
225+}, timeoutMs),
226+};
227+waiters.push(waiter);
228+});
229+},
230+};
231+}
232+180233async function directCronReq(
181234cronState: DirectCronState,
182235method: string,
@@ -388,9 +441,8 @@ describe("gateway server cron", () => {
388441const routeJobId = typeof routeJobIdValue === "string" ? routeJobIdValue : "";
389442expect(routeJobId.length > 0).toBe(true);
390443391-const runRes = await directCronReq(cronState, "cron.run", { id: routeJobId, mode: "force" });
392-expect(runRes.ok).toBe(true);
393-expect(runRes.payload).toEqual({ ok: true, enqueued: true, runId: expect.any(String) });
444+const runRes = await cronState.cron.run(routeJobId, "force");
445+expect(runRes).toEqual({ ok: true, ran: true });
394446const events = await waitForSystemEvent();
395447expect(events.some((event) => event.includes("cron route check"))).toBe(true);
396448@@ -834,17 +886,16 @@ describe("gateway server cron", () => {
834886test("writes cron run history and auto-runs due jobs", async () => {
835887const { prevSkipCron } = await setupCronTestRun({
836888tempPrefix: "openclaw-gw-cron-log-",
889+cronEnabled: true,
837890});
838-839-const { server, ws } = await startServerWithClient();
840-await connectOk(ws);
891+const events = createCronEventCollector();
892+const cronState = await createDirectCronState({ broadcast: events.broadcast });
841893842894try {
843-const atMs = Date.now() - 1;
844-const addRes = await rpcReq(ws, "cron.add", {
895+const addRes = await directCronReq(cronState, "cron.add", {
845896name: "log test",
846897enabled: true,
847-schedule: { kind: "at", at: new Date(atMs).toISOString() },
898+schedule: { kind: "every", everyMs: 60_000 },
848899sessionTarget: "main",
849900wakeMode: "next-heartbeat",
850901payload: { kind: "systemEvent", text: "hello" },
@@ -854,11 +905,10 @@ describe("gateway server cron", () => {
854905const jobId = typeof jobIdValue === "string" ? jobIdValue : "";
855906expect(jobId.length > 0).toBe(true);
856907857-const finishedRun = waitForCronEvent(
858-ws,
908+const finishedRun = events.wait(
859909(payload) => payload?.jobId === jobId && payload?.action === "finished",
860910);
861-const runRes = await rpcReq(ws, "cron.run", { id: jobId, mode: "force" }, 20_000);
911+const runRes = await directCronReq(cronState, "cron.run", { id: jobId, mode: "force" });
862912expect(runRes.ok).toBe(true);
863913expect(runRes.payload).toEqual({ ok: true, enqueued: true, runId: expect.any(String) });
864914const manualRunId = (runRes.payload as { runId?: unknown } | null)?.runId;
@@ -872,7 +922,7 @@ describe("gateway server cron", () => {
872922deliveryStatus: "not-requested",
873923});
874924875-const runsRes = await rpcReq(ws, "cron.runs", { id: jobId, limit: 50 });
925+const runsRes = await directCronReq(cronState, "cron.runs", { id: jobId, limit: 50 });
876926expect(runsRes.ok).toBe(true);
877927const entries = (runsRes.payload as { entries?: unknown } | null)?.entries;
878928expect(Array.isArray(entries)).toBe(true);
@@ -882,7 +932,7 @@ describe("gateway server cron", () => {
882932"not-requested",
883933);
884934expect((entries as Array<{ runId?: unknown }>).at(-1)?.runId).toBe(manualRunId);
885-const allRunsRes = await rpcReq(ws, "cron.runs", {
935+const allRunsRes = await directCronReq(cronState, "cron.runs", {
886936scope: "all",
887937limit: 50,
888938statuses: ["ok"],
@@ -894,7 +944,7 @@ describe("gateway server cron", () => {
894944(allEntries as Array<{ jobId?: unknown }>).some((entry) => entry.jobId === jobId),
895945).toBe(true);
896946897-const statusRes = await rpcReq(ws, "cron.status", {});
947+const statusRes = await directCronReq(cronState, "cron.status", {});
898948expect(statusRes.ok).toBe(true);
899949const statusPayload = statusRes.payload as
900950| { enabled?: unknown; storePath?: unknown }
@@ -903,7 +953,7 @@ describe("gateway server cron", () => {
903953const storePath = typeof statusPayload?.storePath === "string" ? statusPayload.storePath : "";
904954expect(storePath).toContain("jobs.json");
905955906-const autoRes = await rpcReq(ws, "cron.add", {
956+const autoRes = await directCronReq(cronState, "cron.add", {
907957name: "auto run test",
908958enabled: true,
909959schedule: { kind: "at", at: new Date(Date.now() - 1).toISOString() },
@@ -916,18 +966,19 @@ describe("gateway server cron", () => {
916966const autoJobId = typeof autoJobIdValue === "string" ? autoJobIdValue : "";
917967expect(autoJobId.length > 0).toBe(true);
918968919-await waitForCronEvent(
920-ws,
969+const autoFinished = events.wait(
921970(payload) => payload?.jobId === autoJobId && payload?.action === "finished",
922971);
923-const autoEntries = (await rpcReq(ws, "cron.runs", { id: autoJobId, limit: 10 })).payload as
924-| { entries?: Array<{ jobId?: unknown }> }
925-| undefined;
972+await cronState.cron.start();
973+await autoFinished;
974+const autoEntries = (
975+await directCronReq(cronState, "cron.runs", { id: autoJobId, limit: 10 })
976+).payload as { entries?: Array<{ jobId?: unknown }> } | undefined;
926977expect(Array.isArray(autoEntries?.entries)).toBe(true);
927978const runs = autoEntries?.entries ?? [];
928979expect(runs.at(-1)?.jobId).toBe(autoJobId);
929980} finally {
930-await cleanupCronTestRun({ ws, server, prevSkipCron });
981+await cleanupCronTestRun({ cronState, prevSkipCron });
931982}
932983}, 45_000);
933984此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。