diff --git a/src/cron/service.store-load-invalid-main-job.test.ts b/src/cron/service.store-load-invalid-main-job.test.ts new file mode 100644 index 00000000000..39bc3588e44 --- /dev/null +++ b/src/cron/service.store-load-invalid-main-job.test.ts @@ -0,0 +1,78 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CronService } from "./service.js"; +import { + createNoopLogger, + installCronTestHooks, + writeCronStoreSnapshot, +} from "./service.test-harness.js"; +import type { CronJob } from "./types.js"; + +const noopLogger = createNoopLogger(); +installCronTestHooks({ logger: noopLogger }); + +async function makeStorePath() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-store-load-")); + return { + dir, + storePath: path.join(dir, "cron", "jobs.json"), + }; +} + +describe("CronService store load", () => { + let tempDir: string | null = null; + + afterEach(async () => { + if (!tempDir) { + return; + } + await fs.rm(tempDir, { recursive: true, force: true }); + tempDir = null; + }); + + it("skips invalid main jobs with agentTurn payloads loaded from disk", async () => { + const { dir, storePath } = await makeStorePath(); + tempDir = dir; + const enqueueSystemEvent = vi.fn(); + const requestHeartbeatNow = vi.fn(); + + const job = { + id: "job-1", + enabled: true, + createdAtMs: Date.parse("2025-12-13T00:00:00.000Z"), + updatedAtMs: Date.parse("2025-12-13T00:00:00.000Z"), + schedule: { kind: "at", at: "2025-12-13T00:00:01.000Z" }, + sessionTarget: "main", + wakeMode: "now", + payload: { kind: "agentTurn", message: "bad" }, + state: {}, + name: "bad", + } satisfies CronJob; + + await writeCronStoreSnapshot({ storePath, jobs: [job] }); + + const cron = new CronService({ + storePath, + cronEnabled: true, + log: noopLogger, + enqueueSystemEvent, + requestHeartbeatNow, + runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })), + }); + + await cron.start(); + vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z")); + await cron.run("job-1", "due"); + + expect(enqueueSystemEvent).not.toHaveBeenCalled(); + expect(requestHeartbeatNow).not.toHaveBeenCalled(); + + const jobs = await cron.list({ includeDisabled: true }); + expect(jobs[0]?.state.lastStatus).toBe("skipped"); + expect(jobs[0]?.state.lastError).toMatch(/main job requires/i); + + cron.stop(); + }); +});