fix(cron): keep repaired schedule error state after force-run reload

This commit is contained in:
create 2026-03-20 18:23:34 +08:00
parent dbc2925451
commit 44e912eccd
2 changed files with 90 additions and 1 deletions

View File

@ -397,6 +397,95 @@ describe("forceReload repairs externally changed cron schedules", () => {
expect(persistedJob?.state?.lastStatus).toBe("ok");
});
it("keeps scheduleErrorCount cleared when external reload fixes schedule during force-run", async () => {
const store = await makeStorePath();
let nowMs = Date.parse("2026-03-19T01:44:00.000Z");
const jobId = "manual-run-reload-clears-schedule-error-count";
const staleNextRunAtMs = Date.parse("2026-03-19T23:30:00.000Z");
const createJob = (params: {
expr: string;
scheduleErrorCount?: number;
lastError?: string;
nextRunAtMs?: number;
}): CronJob => ({
id: jobId,
name: "manual run reload clears schedule error count",
enabled: true,
createdAtMs: Date.parse("2026-03-18T00:30:00.000Z"),
updatedAtMs: Date.parse("2026-03-19T01:44:00.000Z"),
schedule: { kind: "cron", expr: params.expr, tz: "Asia/Shanghai", staggerMs: 0 },
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payload: { kind: "agentTurn", message: "tick" },
state: {
nextRunAtMs: params.nextRunAtMs,
scheduleErrorCount: params.scheduleErrorCount,
lastError: params.lastError,
},
});
await writeCronStoreSnapshot({
storePath: store.storePath,
jobs: [
createJob({
expr: "30 23 * * *",
nextRunAtMs: staleNextRunAtMs,
scheduleErrorCount: 2,
lastError: "cron: invalid expression",
}),
],
});
const runIsolatedAgentJob = vi.fn(async () => {
await writeCronStoreSnapshot({
storePath: store.storePath,
jobs: [
createJob({
expr: "30 8 * * *",
nextRunAtMs: staleNextRunAtMs,
scheduleErrorCount: 2,
lastError: "cron: invalid expression",
}),
],
});
nowMs += 500;
return { status: "ok" as const, summary: "done" };
});
const state = createCronServiceState({
cronEnabled: true,
storePath: store.storePath,
log: noopLogger,
nowMs: () => nowMs,
enqueueSystemEvent: vi.fn(),
requestHeartbeatNow: vi.fn(),
runIsolatedAgentJob,
});
const result = await run(state, jobId, "force");
expect(result).toEqual({ ok: true, ran: true });
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1);
const merged = state.store?.jobs.find((job) => job.id === jobId);
expect(merged?.schedule).toEqual({
kind: "cron",
expr: "30 8 * * *",
tz: "Asia/Shanghai",
staggerMs: 0,
});
expect(merged?.state.scheduleErrorCount).toBeUndefined();
const persisted = JSON.parse(await fs.readFile(store.storePath, "utf8")) as {
jobs?: Array<{
id: string;
state?: { scheduleErrorCount?: number };
}>;
};
const persistedJob = persisted.jobs?.find((job) => job.id === jobId);
expect(persistedJob?.state?.scheduleErrorCount).toBeUndefined();
});
it("keeps one-shot terminal disable state when manual force-run reloads unchanged store", async () => {
const store = await makeStorePath();
let nowMs = Date.parse("2026-03-19T01:44:00.000Z");

View File

@ -99,8 +99,8 @@ function mergeManualRunSnapshotAfterReload(params: {
if (externalScheduleOrEnabledChanged) {
reloaded.enabled = preservedEnabled;
reloaded.state.nextRunAtMs = preservedNextRunAtMs;
reloaded.state.scheduleErrorCount = preservedScheduleErrorCount;
if (preservedScheduleErrorCount !== undefined) {
reloaded.state.scheduleErrorCount = preservedScheduleErrorCount;
reloaded.state.lastError = preservedScheduleErrorText;
}
} else {