Cron: persist manual run marker before unlock (#23993)

* Cron: persist manual run marker before unlock

* Cron tests: relax wakeMode now microtask wait after run lock persist
This commit is contained in:
Tak Hoffman
2026-02-22 18:39:37 -06:00
committed by GitHub
parent 9c87b53c8e
commit 211ab9e4f6
4 changed files with 65 additions and 1 deletions

View File

@@ -507,6 +507,64 @@ describe("Cron issue regressions", () => {
cron.stop();
});
it("does not double-run a job when cron.run overlaps a due timer tick", async () => {
const store = await makeStorePath();
const runStarted = createDeferred<void>();
const runFinished = createDeferred<void>();
const runResolvers: Array<
(value: { status: "ok" | "error" | "skipped"; summary?: string; error?: string }) => void
> = [];
const runIsolatedAgentJob = vi.fn(async () => {
if (runIsolatedAgentJob.mock.calls.length === 1) {
runStarted.resolve();
}
return await new Promise<{ status: "ok" | "error" | "skipped"; summary?: string }>(
(resolve) => {
runResolvers.push(resolve);
},
);
});
let targetJobId = "";
const cron = await startCronForStore({
storePath: store.storePath,
runIsolatedAgentJob,
onEvent: (evt: CronEvent) => {
if (evt.jobId === targetJobId && evt.action === "finished") {
runFinished.resolve();
}
},
});
const dueAt = Date.now() + 100;
const job = await cron.add({
name: "manual-overlap-no-double-run",
enabled: true,
schedule: { kind: "at", at: new Date(dueAt).toISOString() },
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payload: { kind: "agentTurn", message: "overlap" },
delivery: { mode: "none" },
});
targetJobId = job.id;
const manualRun = cron.run(job.id, "force");
await runStarted.promise;
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(120);
await Promise.resolve();
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1);
runResolvers[0]?.({ status: "ok", summary: "done" });
await manualRun;
await runFinished.promise;
// Barrier for final persistence before cleanup.
await cron.list({ includeDisabled: true });
cron.stop();
});
it("#13845: one-shot jobs with terminal statuses do not re-fire on restart", async () => {
const store = await makeStorePath();
const pastAt = Date.parse("2026-02-06T09:00:00.000Z");