mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 01:58:26 +00:00
fix: prevent cron jobs from skipping execution when nextRunAtMs advances (#14068)
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
130
src/cron/service.issue-13992-regression.test.ts
Normal file
130
src/cron/service.issue-13992-regression.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CronServiceState } from "./service/state.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
import { recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
||||
|
||||
describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
function createMockState(jobs: CronJob[]): CronServiceState {
|
||||
return {
|
||||
store: { version: 1, jobs },
|
||||
running: false,
|
||||
timer: null,
|
||||
storeLoadedAtMs: Date.now(),
|
||||
deps: {
|
||||
storePath: "/mock/path",
|
||||
cronEnabled: true,
|
||||
nowMs: () => Date.now(),
|
||||
log: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as never,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("should NOT recompute nextRunAtMs for past-due jobs during maintenance", () => {
|
||||
const now = Date.now();
|
||||
const pastDue = now - 60_000; // 1 minute ago
|
||||
|
||||
const job: CronJob = {
|
||||
id: "test-job",
|
||||
name: "test job",
|
||||
enabled: true,
|
||||
schedule: { kind: "cron", expr: "0 8 * * *", tz: "UTC" },
|
||||
payload: { kind: "systemEvent", text: "test" },
|
||||
sessionTarget: "main",
|
||||
createdAtMs: now - 3600_000,
|
||||
updatedAtMs: now - 3600_000,
|
||||
state: {
|
||||
nextRunAtMs: pastDue, // This is in the past and should NOT be recomputed
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should not have changed the past-due nextRunAtMs
|
||||
expect(job.state.nextRunAtMs).toBe(pastDue);
|
||||
});
|
||||
|
||||
it("should compute missing nextRunAtMs during maintenance", () => {
|
||||
const now = Date.now();
|
||||
|
||||
const job: CronJob = {
|
||||
id: "test-job",
|
||||
name: "test job",
|
||||
enabled: true,
|
||||
schedule: { kind: "cron", expr: "0 8 * * *", tz: "UTC" },
|
||||
payload: { kind: "systemEvent", text: "test" },
|
||||
sessionTarget: "main",
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
state: {
|
||||
// nextRunAtMs is missing
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have computed a nextRunAtMs
|
||||
expect(typeof job.state.nextRunAtMs).toBe("number");
|
||||
expect(job.state.nextRunAtMs).toBeGreaterThan(now);
|
||||
});
|
||||
|
||||
it("should clear nextRunAtMs for disabled jobs during maintenance", () => {
|
||||
const now = Date.now();
|
||||
const futureTime = now + 3600_000;
|
||||
|
||||
const job: CronJob = {
|
||||
id: "test-job",
|
||||
name: "test job",
|
||||
enabled: false, // Disabled
|
||||
schedule: { kind: "cron", expr: "0 8 * * *", tz: "UTC" },
|
||||
payload: { kind: "systemEvent", text: "test" },
|
||||
sessionTarget: "main",
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
state: {
|
||||
nextRunAtMs: futureTime,
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have cleared nextRunAtMs for disabled job
|
||||
expect(job.state.nextRunAtMs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should clear stuck running markers during maintenance", () => {
|
||||
const now = Date.now();
|
||||
const stuckTime = now - 3 * 60 * 60_000; // 3 hours ago (> 2 hour threshold)
|
||||
const futureTime = now + 3600_000;
|
||||
|
||||
const job: CronJob = {
|
||||
id: "test-job",
|
||||
name: "test job",
|
||||
enabled: true,
|
||||
schedule: { kind: "cron", expr: "0 8 * * *", tz: "UTC" },
|
||||
payload: { kind: "systemEvent", text: "test" },
|
||||
sessionTarget: "main",
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
state: {
|
||||
nextRunAtMs: futureTime,
|
||||
runningAtMs: stuckTime, // Stuck running marker
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have cleared stuck running marker
|
||||
expect(job.state.runningAtMs).toBeUndefined();
|
||||
// But should NOT have changed nextRunAtMs (it's still future)
|
||||
expect(job.state.nextRunAtMs).toBe(futureTime);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user