






















@@ -600,6 +600,144 @@ describe("maybeRepairLegacyCronStore", () => {
600600expectNoteContaining("1 job still uses legacy", "Cron");
601601});
602602603+it("advises on isolated shell-prompt jobs without a non-actionable --fix repair note (#94655)", async () => {
604+const storePath = await makeTempStorePath();
605+const shellPromptJobs: Array<Record<string, unknown>> = [
606+createCurrentCronJob({
607+id: "shell-prompt-job-1",
608+name: "Shell prompt job 1",
609+schedule: { kind: "cron", expr: "*/30 * * * *", tz: "UTC" },
610+sessionTarget: "isolated",
611+payload: {
612+kind: "agentTurn",
613+message:
614+"Run python3 scripts/check_mail.py and send a compact summary if anything changed.",
615+toolsAllow: ["*"],
616+},
617+delivery: { mode: "announce" },
618+}),
619+createCurrentCronJob({
620+id: "shell-prompt-job-2",
621+name: "Shell prompt job 2",
622+schedule: { kind: "cron", expr: "15 * * * *", tz: "UTC" },
623+sessionTarget: "isolated",
624+payload: {
625+kind: "agentTurn",
626+message: "Run node scripts/check_mail.js and summarize any new messages.",
627+toolsAllow: ["bash"],
628+},
629+delivery: { mode: "announce" },
630+}),
631+createCurrentCronJob({
632+id: "shell-prompt-job-3",
633+name: "Shell prompt job 3",
634+schedule: { kind: "cron", expr: "45 * * * *", tz: "UTC" },
635+sessionTarget: "isolated",
636+payload: {
637+kind: "agentTurn",
638+message: "Execute ./scripts/check_mail.sh and report changed mailbox counts.",
639+toolsAllow: ["shell"],
640+},
641+delivery: { mode: "announce" },
642+}),
643+];
644+const shellPromptJob = requirePersistedJob(shellPromptJobs, 0);
645+await writeCurrentCronStore(storePath, shellPromptJobs);
646+647+const prompter = makePrompter(true);
648+await maybeRepairLegacyCronStore({
649+cfg: createCronConfig(storePath),
650+options: {},
651+ prompter,
652+});
653+654+// The advisory is informational only: doctor --fix cannot rewrite a working
655+// isolated agentTurn job, so the misleading repair note must stay absent.
656+expectNoNoteContaining("Cron store issues detected", "Cron");
657+expectNoteContaining(
658+"3 isolated cron jobs drive shell/process tools from the agent prompt and keep running as-is: `Shell prompt job 1`, `Shell prompt job 2`, `Shell prompt job 3`.",
659+"Cron",
660+);
661+expectNoteContaining("informational only", "Cron");
662+expectNoteContaining("Shell prompt job 1", "Cron");
663+expectNoteContaining("Shell prompt job 2", "Cron");
664+expectNoteContaining("Shell prompt job 3", "Cron");
665+expectNoNoteContaining("openclaw doctor --fix", "Cron");
666+expectNoNoteContaining("jobs.json", "Cron");
667+expect(prompter.confirm).not.toHaveBeenCalled();
668+669+// No churn: the advisory does not rewrite the still-working jobs.
670+const persistedJobs = await readPersistedJobs(storePath);
671+expect(persistedJobs).toEqual(shellPromptJobs);
672+const job = requirePersistedJob(persistedJobs, 0);
673+expect(job).toEqual(shellPromptJob);
674+const reloaded = await loadCronJobsStoreWithConfigJobs(storePath);
675+expect(reloaded.configJobIndexes).toEqual([0, 1, 2]);
676+expect(reloaded.invalidConfigRows).toEqual([]);
677+const configJob = requirePersistedJob(reloaded.configJobs, 0);
678+expect(configJob).toEqual(
679+Object.fromEntries(Object.entries(shellPromptJob).filter(([key]) => key !== "updatedAtMs")),
680+);
681+expect(reloaded.configJobRuntimeEntries[0]).toEqual({
682+updatedAtMs: shellPromptJob.updatedAtMs,
683+state: {},
684+scheduleIdentity: JSON.stringify({
685+version: 1,
686+enabled: shellPromptJob.enabled,
687+schedule: shellPromptJob.schedule,
688+}),
689+});
690+const payload = requireRecord(job.payload, "cron payload");
691+expect(payload.kind).toBe("agentTurn");
692+expect(payload.message).toContain("python3 scripts/check_mail.py");
693+});
694+695+it("keeps restricted command prompts actionable without a --fix repair note", async () => {
696+const storePath = await makeTempStorePath();
697+const commandPromptJob = createCurrentCronJob({
698+id: "restricted-command-prompt",
699+name: "Restricted command prompt",
700+schedule: { kind: "cron", expr: "*/30 * * * *", tz: "UTC" },
701+sessionTarget: "isolated",
702+payload: {
703+kind: "agentTurn",
704+message: [
705+"Command to run:",
706+"- command: python3 scripts/check_mail.py",
707+"- workdir: /home/openclaw/.razor/clawd",
708+].join("\n"),
709+toolsAllow: ["read", "message"],
710+},
711+delivery: { mode: "announce" },
712+});
713+await writeCurrentCronStore(storePath, [commandPromptJob]);
714+715+const prompter = makePrompter(true);
716+await maybeRepairLegacyCronStore({
717+cfg: createCronConfig(storePath),
718+options: {},
719+ prompter,
720+});
721+722+expectNoNoteContaining("Cron store issues detected", "Cron");
723+expectNoteContaining(
724+"1 isolated cron job describes a shell command in the agent prompt but lacks shell/process tool access: `Restricted command prompt`.",
725+"Cron",
726+);
727+expectNoteContaining("not the supported shell-tool prompt shape", "Cron");
728+expectNoteContaining("Recreate the job as a command cron job", "Cron");
729+expectNoNoteContaining("informational only", "Cron");
730+expectNoNoteContaining("keep running as-is", "Cron");
731+expectNoNoteContaining("openclaw doctor --fix", "Cron");
732+expect(prompter.confirm).not.toHaveBeenCalled();
733+734+const job = requirePersistedJob(await readPersistedJobs(storePath), 0);
735+const payload = requireRecord(job.payload, "cron payload");
736+expect(payload.kind).toBe("agentTurn");
737+expect(payload.message).toContain("python3 scripts/check_mail.py");
738+expect(payload.toolsAllow).toEqual(["read", "message"]);
739+});
740+603741it("repairs malformed persisted cron ids before list rendering sees them", async () => {
604742const storePath = await makeTempStorePath();
605743await writeCronStore(storePath, [
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。