





















1+import { describe, expect, it, vi } from "vitest";
2+import { setupCronServiceSuite } from "./service.test-harness.js";
3+import { start } from "./service/ops.js";
4+import { createCronServiceState } from "./service/state.js";
5+import { saveCronStore } from "./store.js";
6+import type { CronJob } from "./types.js";
7+8+const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
9+prefix: "openclaw-cron-overflow-",
10+baseTimeIso: "2025-12-13T17:00:00.000Z",
11+});
12+13+describe("CronService startup catch-up repair scoping", () => {
14+function createHourlyCronJob(id: string, nextRunAtMs: number): CronJob {
15+return {
16+ id,
17+name: `job-${id}`,
18+enabled: true,
19+createdAtMs: nextRunAtMs - 60_000,
20+updatedAtMs: nextRunAtMs - 60_000,
21+schedule: { kind: "cron", expr: "0 * * * *", tz: "UTC" },
22+sessionTarget: "main",
23+wakeMode: "next-heartbeat",
24+payload: { kind: "systemEvent", text: `tick-${id}` },
25+state: { nextRunAtMs },
26+};
27+}
28+29+function createDailyCronJob(id: string, nextRunAtMs: number): CronJob {
30+return {
31+ id,
32+name: `job-${id}`,
33+enabled: true,
34+createdAtMs: nextRunAtMs - 60_000,
35+updatedAtMs: nextRunAtMs - 60_000,
36+schedule: { kind: "cron", expr: "0 9 * * *", tz: "UTC" },
37+sessionTarget: "main",
38+wakeMode: "next-heartbeat",
39+payload: { kind: "systemEvent", text: `tick-${id}` },
40+state: { nextRunAtMs },
41+};
42+}
43+44+it("keeps the overflow daily-cron catch-up deferral after start()'s maintenance pass", async () => {
45+const store = await makeStorePath();
46+const startNow = Date.parse("2025-12-13T17:00:00.000Z");
47+const tomorrowNaturalSlot = Date.parse("2025-12-14T09:00:00.000Z");
48+49+await saveCronStore(store.storePath, {
50+version: 1,
51+jobs: [
52+createHourlyCronJob("hourly-0", Date.parse("2025-12-13T03:00:00.000Z")),
53+createHourlyCronJob("hourly-1", Date.parse("2025-12-13T04:00:00.000Z")),
54+createHourlyCronJob("hourly-2", Date.parse("2025-12-13T05:00:00.000Z")),
55+createHourlyCronJob("hourly-3", Date.parse("2025-12-13T06:00:00.000Z")),
56+createHourlyCronJob("hourly-4", Date.parse("2025-12-13T07:00:00.000Z")),
57+createDailyCronJob("daily-overflow", Date.parse("2025-12-13T09:00:00.000Z")),
58+],
59+});
60+61+const state = createCronServiceState({
62+cronEnabled: true,
63+storePath: store.storePath,
64+log: noopLogger,
65+nowMs: () => startNow,
66+enqueueSystemEvent: vi.fn(),
67+requestHeartbeat: vi.fn(),
68+runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
69+});
70+71+await start(state);
72+73+const deferred = state.store?.jobs.find((job) => job.id === "daily-overflow");
74+75+expect(deferred?.state.nextRunAtMs).toBe(startNow + 5_000);
76+expect(deferred?.state.nextRunAtMs).not.toBe(tomorrowNaturalSlot);
77+78+state.stopped = true;
79+await store.cleanup();
80+});
81+82+it("still repairs a stale future cron slot on start() when no jobs were deferred", async () => {
83+const store = await makeStorePath();
84+const startNow = Date.parse("2025-12-13T17:00:00.000Z");
85+const staleFutureSlot = Date.parse("2025-12-13T18:00:00.000Z");
86+const naturalSlot = Date.parse("2025-12-14T09:00:00.000Z");
87+88+await saveCronStore(store.storePath, {
89+version: 1,
90+jobs: [createDailyCronJob("daily-stale-future", staleFutureSlot)],
91+});
92+93+const state = createCronServiceState({
94+cronEnabled: true,
95+storePath: store.storePath,
96+log: noopLogger,
97+nowMs: () => startNow,
98+enqueueSystemEvent: vi.fn(),
99+requestHeartbeat: vi.fn(),
100+runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
101+});
102+103+await start(state);
104+105+const repaired = state.store?.jobs.find((job) => job.id === "daily-stale-future");
106+107+expect(repaired?.state.nextRunAtMs).toBe(naturalSlot);
108+expect(repaired?.state.nextRunAtMs).not.toBe(staleFutureSlot);
109+110+state.stopped = true;
111+await store.cleanup();
112+});
113+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。