Spaces:
Paused
Paused
| import fs from "node:fs/promises"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { CronService } from "./service.js"; | |
| const noopLogger = { | |
| debug: vi.fn(), | |
| info: vi.fn(), | |
| warn: vi.fn(), | |
| error: vi.fn(), | |
| }; | |
| async function makeStorePath() { | |
| const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-")); | |
| return { | |
| storePath: path.join(dir, "cron", "jobs.json"), | |
| cleanup: async () => { | |
| await fs.rm(dir, { recursive: true, force: true }); | |
| }, | |
| }; | |
| } | |
| describe("CronService", () => { | |
| beforeEach(() => { | |
| vi.useFakeTimers(); | |
| vi.setSystemTime(new Date("2025-12-13T00:00:00.000Z")); | |
| noopLogger.debug.mockClear(); | |
| noopLogger.info.mockClear(); | |
| noopLogger.warn.mockClear(); | |
| noopLogger.error.mockClear(); | |
| }); | |
| afterEach(() => { | |
| vi.useRealTimers(); | |
| }); | |
| it("skips main jobs with empty systemEvent text", async () => { | |
| const store = await makeStorePath(); | |
| const enqueueSystemEvent = vi.fn(); | |
| const requestHeartbeatNow = vi.fn(); | |
| const cron = new CronService({ | |
| storePath: store.storePath, | |
| cronEnabled: true, | |
| log: noopLogger, | |
| enqueueSystemEvent, | |
| requestHeartbeatNow, | |
| runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })), | |
| }); | |
| await cron.start(); | |
| const atMs = Date.parse("2025-12-13T00:00:01.000Z"); | |
| await cron.add({ | |
| name: "empty systemEvent test", | |
| enabled: true, | |
| schedule: { kind: "at", atMs }, | |
| sessionTarget: "main", | |
| wakeMode: "now", | |
| payload: { kind: "systemEvent", text: " " }, | |
| }); | |
| vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z")); | |
| await vi.runOnlyPendingTimersAsync(); | |
| 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(/non-empty/i); | |
| cron.stop(); | |
| await store.cleanup(); | |
| }); | |
| it("does not schedule timers when cron is disabled", async () => { | |
| const store = await makeStorePath(); | |
| const enqueueSystemEvent = vi.fn(); | |
| const requestHeartbeatNow = vi.fn(); | |
| const cron = new CronService({ | |
| storePath: store.storePath, | |
| cronEnabled: false, | |
| log: noopLogger, | |
| enqueueSystemEvent, | |
| requestHeartbeatNow, | |
| runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })), | |
| }); | |
| await cron.start(); | |
| const atMs = Date.parse("2025-12-13T00:00:01.000Z"); | |
| await cron.add({ | |
| name: "disabled cron job", | |
| enabled: true, | |
| schedule: { kind: "at", atMs }, | |
| sessionTarget: "main", | |
| wakeMode: "now", | |
| payload: { kind: "systemEvent", text: "hello" }, | |
| }); | |
| const status = await cron.status(); | |
| expect(status.enabled).toBe(false); | |
| expect(status.nextWakeAtMs).toBeNull(); | |
| vi.setSystemTime(new Date("2025-12-13T00:00:01.000Z")); | |
| await vi.runOnlyPendingTimersAsync(); | |
| expect(enqueueSystemEvent).not.toHaveBeenCalled(); | |
| expect(requestHeartbeatNow).not.toHaveBeenCalled(); | |
| expect(noopLogger.warn).toHaveBeenCalled(); | |
| cron.stop(); | |
| await store.cleanup(); | |
| }); | |
| it("status reports next wake when enabled", async () => { | |
| const store = await makeStorePath(); | |
| const enqueueSystemEvent = vi.fn(); | |
| const requestHeartbeatNow = vi.fn(); | |
| const cron = new CronService({ | |
| storePath: store.storePath, | |
| cronEnabled: true, | |
| log: noopLogger, | |
| enqueueSystemEvent, | |
| requestHeartbeatNow, | |
| runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })), | |
| }); | |
| await cron.start(); | |
| const atMs = Date.parse("2025-12-13T00:00:05.000Z"); | |
| await cron.add({ | |
| name: "status next wake", | |
| enabled: true, | |
| schedule: { kind: "at", atMs }, | |
| sessionTarget: "main", | |
| wakeMode: "next-heartbeat", | |
| payload: { kind: "systemEvent", text: "hello" }, | |
| }); | |
| const status = await cron.status(); | |
| expect(status.enabled).toBe(true); | |
| expect(status.jobs).toBe(1); | |
| expect(status.nextWakeAtMs).toBe(atMs); | |
| cron.stop(); | |
| await store.cleanup(); | |
| }); | |
| }); | |