
























@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
66import type { OpenClawConfig } from "../../../config/config.js";
77import { readCronRunLogEntriesSync } from "../../../cron/run-log.js";
88import {
9+loadCronJobsStoreWithConfigJobs,
910loadCronQuarantineFile,
1011loadCronStore,
1112resolveCronQuarantinePath,
@@ -841,12 +842,80 @@ describe("maybeRepairLegacyCronStore", () => {
841842const job = requirePersistedJob(jobs, 0);
842843expect(job.notify).toBeUndefined();
843844expect(job.delivery).toBeUndefined();
845+const reloaded = await loadCronJobsStoreWithConfigJobs(storePath);
846+const persisted = reloaded.configJobs as unknown as Array<Record<string, unknown>>;
847+expect(persisted[0]?.notify).toBe(true);
844848expectNoteContaining(
845849"cron.webhook is not a valid HTTP(S) URL so doctor cannot migrate it automatically",
846850"Doctor warnings",
847851);
848852});
849853854+it("removes inert legacy notify:true for delivery.mode none when cron.webhook is unset and stops looping (#44460)", async () => {
855+const storePath = await makeTempStorePath();
856+await writeCronStore(storePath, [
857+createCurrentCronJob({
858+id: "notify-none-unset",
859+name: "Notify none unset",
860+notify: true,
861+delivery: { mode: "none" },
862+}),
863+]);
864+865+const cfg = { cron: { store: storePath } } as OpenClawConfig;
866+await maybeRepairLegacyCronStore({
867+ cfg,
868+options: {},
869+prompter: makePrompter(true),
870+});
871+872+const reloaded = await loadCronJobsStoreWithConfigJobs(storePath);
873+const persisted = reloaded.configJobs as unknown as Array<Record<string, unknown>>;
874+expect(persisted).toHaveLength(1);
875+expect(persisted[0]?.notify).toBeUndefined();
876+expect(requireRecord(persisted[0]?.delivery, "cron delivery").mode).toBe("none");
877+expectNoNoteContaining(
878+"cron.webhook is unset so doctor cannot migrate it automatically",
879+"Doctor warnings",
880+);
881+882+noteMock.mockClear();
883+await maybeRepairLegacyCronStore({
884+ cfg,
885+options: {},
886+prompter: makePrompter(true),
887+});
888+expectNoNoteContaining("still uses legacy `notify: true`", "Cron");
889+});
890+891+it("drops inert legacy notify alongside existing announce delivery without changing it when cron.webhook is unset (#44460)", async () => {
892+const storePath = await makeTempStorePath();
893+await writeCronStore(storePath, [
894+createCurrentCronJob({
895+id: "notify-announce-unset",
896+name: "Notify announce unset",
897+notify: true,
898+payload: { kind: "agentTurn", message: "Status" },
899+delivery: { mode: "announce", to: "telegram:123" },
900+}),
901+]);
902+903+const cfg = { cron: { store: storePath } } as OpenClawConfig;
904+await maybeRepairLegacyCronStore({
905+ cfg,
906+options: {},
907+prompter: makePrompter(true),
908+});
909+910+const reloaded = await loadCronJobsStoreWithConfigJobs(storePath);
911+const persisted = reloaded.configJobs as unknown as Array<Record<string, unknown>>;
912+expect(persisted).toHaveLength(1);
913+expect(persisted[0]?.notify).toBeUndefined();
914+const delivery = requireRecord(persisted[0]?.delivery, "cron delivery");
915+expect(delivery.mode).toBe("announce");
916+expect(delivery.to).toBe("telegram:123");
917+});
918+850919it("quarantines invalid legacy rows before saving the repaired store", async () => {
851920const storePath = await makeTempStorePath();
852921await writeCronStore(storePath, [
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。