| import { describe, expect, it } from "vitest"; |
| import { computeJobNextRunAtMs } from "./service/jobs.js"; |
| import type { CronJob } from "./types.js"; |
|
|
| const EVERY_30_MIN_MS = 30 * 60_000; |
| const ANCHOR_MS = Date.parse("2026-02-22T09:14:00.000Z"); |
|
|
| function createEveryJob(state: CronJob["state"]): CronJob { |
| return { |
| id: "issue-22895", |
| name: "every-30-min", |
| enabled: true, |
| createdAtMs: ANCHOR_MS, |
| updatedAtMs: ANCHOR_MS, |
| schedule: { kind: "every", everyMs: EVERY_30_MIN_MS, anchorMs: ANCHOR_MS }, |
| sessionTarget: "isolated", |
| wakeMode: "next-heartbeat", |
| payload: { kind: "agentTurn", message: "check cadence" }, |
| delivery: { mode: "none" }, |
| state, |
| }; |
| } |
|
|
| describe("Cron issue #22895 interval scheduling", () => { |
| it("uses lastRunAtMs cadence when the next interval is still in the future", () => { |
| const nowMs = Date.parse("2026-02-22T10:10:00.000Z"); |
| const job = createEveryJob({ |
| lastRunAtMs: Date.parse("2026-02-22T10:04:00.000Z"), |
| }); |
|
|
| const nextFromLast = computeJobNextRunAtMs(job, nowMs); |
| const nextFromAnchor = computeJobNextRunAtMs( |
| { ...job, state: { ...job.state, lastRunAtMs: undefined } }, |
| nowMs, |
| ); |
|
|
| expect(nextFromLast).toBe(job.state.lastRunAtMs! + EVERY_30_MIN_MS); |
| expect(nextFromAnchor).toBe(Date.parse("2026-02-22T10:14:00.000Z")); |
| expect(nextFromLast).toBeGreaterThan(nextFromAnchor!); |
| }); |
|
|
| it("falls back to anchor scheduling when lastRunAtMs cadence is already in the past", () => { |
| const nowMs = Date.parse("2026-02-22T10:40:00.000Z"); |
| const job = createEveryJob({ |
| lastRunAtMs: Date.parse("2026-02-22T10:04:00.000Z"), |
| }); |
|
|
| const next = computeJobNextRunAtMs(job, nowMs); |
| expect(next).toBe(Date.parse("2026-02-22T10:44:00.000Z")); |
| }); |
| }); |
|
|