
























@@ -3,6 +3,7 @@ import path from "node:path";
33import { describe, expect, it, vi } from "vitest";
44import { CronService } from "./service.js";
55import { setupCronServiceSuite } from "./service.test-harness.js";
6+import type { CronEvent } from "./service/state.js";
67import { createCronServiceState } from "./service/state.js";
78import { runMissedJobs } from "./service/timer.js";
89@@ -21,6 +22,7 @@ describe("CronService restart catch-up", () => {
2122storePath: string;
2223enqueueSystemEvent: ReturnType<typeof vi.fn>;
2324requestHeartbeatNow: ReturnType<typeof vi.fn>;
25+onEvent?: ReturnType<typeof vi.fn>;
2426}) {
2527return new CronService({
2628storePath: params.storePath,
@@ -29,6 +31,7 @@ describe("CronService restart catch-up", () => {
2931enqueueSystemEvent: params.enqueueSystemEvent as never,
3032requestHeartbeatNow: params.requestHeartbeatNow as never,
3133runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })) as never,
34+onEvent: params.onEvent as ((evt: CronEvent) => void) | undefined,
3235});
3336}
3437@@ -53,23 +56,26 @@ describe("CronService restart catch-up", () => {
5356cron: CronService;
5457enqueueSystemEvent: ReturnType<typeof vi.fn>;
5558requestHeartbeatNow: ReturnType<typeof vi.fn>;
59+onEvent: ReturnType<typeof vi.fn>;
5660}) => Promise<void>,
5761) {
5862const store = await makeStorePath();
5963const enqueueSystemEvent = vi.fn();
6064const requestHeartbeatNow = vi.fn();
65+const onEvent = vi.fn();
61666267await writeStoreJobs(store.storePath, jobs);
63686469const cron = createRestartCronService({
6570storePath: store.storePath,
6671 enqueueSystemEvent,
6772 requestHeartbeatNow,
73+ onEvent,
6874});
69757076try {
7177await cron.start();
72-await run({ cron, enqueueSystemEvent, requestHeartbeatNow });
78+await run({ cron, enqueueSystemEvent, requestHeartbeatNow, onEvent });
7379} finally {
7480cron.stop();
7581await store.cleanup();
@@ -115,7 +121,7 @@ describe("CronService restart catch-up", () => {
115121);
116122});
117123118-it("replays interrupted recurring job on first restart (#60495)", async () => {
124+it("marks interrupted recurring jobs failed instead of replaying them on startup", async () => {
119125const dueAt = Date.parse("2025-12-13T16:00:00.000Z");
120126const staleRunningAt = Date.parse("2025-12-13T16:30:00.000Z");
121127@@ -137,23 +143,32 @@ describe("CronService restart catch-up", () => {
137143},
138144},
139145],
140-async ({ cron, enqueueSystemEvent, requestHeartbeatNow }) => {
146+async ({ cron, enqueueSystemEvent, requestHeartbeatNow, onEvent }) => {
141147expect(noopLogger.warn).toHaveBeenCalledWith(
142148expect.objectContaining({ jobId: "restart-stale-running" }),
143-"cron: clearing stale running marker on startup",
149+"cron: marking interrupted running job failed on startup",
144150);
145151146-expect(enqueueSystemEvent).toHaveBeenCalledWith(
147-"resume stale marker",
148-expect.objectContaining({ agentId: undefined }),
149-);
150-expect(requestHeartbeatNow).toHaveBeenCalled();
152+expect(enqueueSystemEvent).not.toHaveBeenCalled();
153+expect(requestHeartbeatNow).not.toHaveBeenCalled();
151154152155const listedJobs = await cron.list({ includeDisabled: true });
153156const updated = listedJobs.find((job) => job.id === "restart-stale-running");
154157expect(updated?.state.runningAtMs).toBeUndefined();
155-expect(updated?.state.lastStatus).toBe("ok");
156-expect(updated?.state.lastRunAtMs).toBe(Date.parse("2025-12-13T17:00:00.000Z"));
158+expect(updated?.state.lastStatus).toBe("error");
159+expect(updated?.state.lastRunStatus).toBe("error");
160+expect(updated?.state.lastRunAtMs).toBe(staleRunningAt);
161+expect(updated?.state.lastError).toBe("cron: job interrupted by gateway restart");
162+expect(updated?.state.nextRunAtMs).toBeGreaterThan(Date.parse("2025-12-13T17:00:00.000Z"));
163+expect(onEvent).toHaveBeenCalledWith(
164+expect.objectContaining({
165+action: "finished",
166+jobId: "restart-stale-running",
167+status: "error",
168+error: "cron: job interrupted by gateway restart",
169+runAtMs: staleRunningAt,
170+}),
171+);
157172},
158173);
159174});
@@ -194,7 +209,7 @@ describe("CronService restart catch-up", () => {
194209);
195210});
196211197-it("does not replay interrupted one-shot jobs on startup", async () => {
212+it("marks interrupted one-shot jobs failed and disabled on startup", async () => {
198213const dueAt = Date.parse("2025-12-13T16:00:00.000Z");
199214const staleRunningAt = Date.parse("2025-12-13T16:30:00.000Z");
200215@@ -216,13 +231,28 @@ describe("CronService restart catch-up", () => {
216231},
217232},
218233],
219-async ({ cron, enqueueSystemEvent, requestHeartbeatNow }) => {
234+async ({ cron, enqueueSystemEvent, requestHeartbeatNow, onEvent }) => {
220235expect(enqueueSystemEvent).not.toHaveBeenCalled();
221236expect(requestHeartbeatNow).not.toHaveBeenCalled();
222237223238const listedJobs = await cron.list({ includeDisabled: true });
224239const updated = listedJobs.find((job) => job.id === "restart-stale-one-shot");
240+expect(updated?.enabled).toBe(false);
225241expect(updated?.state.runningAtMs).toBeUndefined();
242+expect(updated?.state.lastStatus).toBe("error");
243+expect(updated?.state.lastRunStatus).toBe("error");
244+expect(updated?.state.lastRunAtMs).toBe(staleRunningAt);
245+expect(updated?.state.nextRunAtMs).toBeUndefined();
246+expect(updated?.state.lastError).toBe("cron: job interrupted by gateway restart");
247+expect(onEvent).toHaveBeenCalledWith(
248+expect.objectContaining({
249+action: "finished",
250+jobId: "restart-stale-one-shot",
251+status: "error",
252+error: "cron: job interrupted by gateway restart",
253+runAtMs: staleRunningAt,
254+}),
255+);
226256},
227257);
228258});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。