Cron: respect aborts in main wake-now retries (#23967)

* Cron: respect aborts in main wake-now retries

* Changelog: add main-session cron abort retry fix note

* Cron tests: format post-rebase conflict resolution
This commit is contained in:
Tak Hoffman
2026-02-22 17:19:27 -06:00
committed by GitHub
parent 9bc265f379
commit 3efe63d1ad
3 changed files with 91 additions and 6 deletions

View File

@@ -3,12 +3,13 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
import * as schedule from "./schedule.js";
import { CronService } from "./service.js";
import { createDeferred, createRunningCronServiceState } from "./service.test-harness.js";
import { computeJobNextRunAtMs } from "./service/jobs.js";
import { createCronServiceState, type CronEvent } from "./service/state.js";
import { onTimer, runMissedJobs } from "./service/timer.js";
import { executeJobCore, onTimer, runMissedJobs } from "./service/timer.js";
import type { CronJob, CronJobState } from "./types.js";
const noopLogger = {
@@ -859,6 +860,55 @@ describe("Cron issue regressions", () => {
expect(job?.state.lastError).toContain("timed out");
});
it("respects abort signals while retrying main-session wake-now heartbeat runs", async () => {
vi.useRealTimers();
const abortController = new AbortController();
const runHeartbeatOnce = vi.fn(
async (): Promise<HeartbeatRunResult> => ({
status: "skipped",
reason: "requests-in-flight",
}),
);
const enqueueSystemEvent = vi.fn();
const requestHeartbeatNow = vi.fn();
const mainJob: CronJob = {
id: "main-abort",
name: "main abort",
enabled: true,
createdAtMs: Date.now(),
updatedAtMs: Date.now(),
schedule: { kind: "every", everyMs: 60_000, anchorMs: Date.now() },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "systemEvent", text: "tick" },
state: {},
};
const state = createCronServiceState({
cronEnabled: true,
storePath: "/tmp/openclaw-cron-abort-test/jobs.json",
log: noopLogger,
nowMs: () => Date.now(),
enqueueSystemEvent,
requestHeartbeatNow,
runHeartbeatOnce,
wakeNowHeartbeatBusyMaxWaitMs: 30,
wakeNowHeartbeatBusyRetryDelayMs: 5,
runIsolatedAgentJob: createDefaultIsolatedRunner(),
});
setTimeout(() => {
abortController.abort();
}, 10);
const result = await executeJobCore(state, mainJob, abortController.signal);
expect(result.status).toBe("error");
expect(result.error).toContain("timed out");
expect(enqueueSystemEvent).toHaveBeenCalledTimes(1);
expect(runHeartbeatOnce).toHaveBeenCalled();
expect(requestHeartbeatNow).not.toHaveBeenCalled();
});
it("retries cron schedule computation from the next second when the first attempt returns undefined (#17821)", () => {
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({