diff --git a/src/cron/service/timer.apply-job-result.test.ts b/src/cron/service/timer.apply-job-result.test.ts index e64714a1560..4c3e02d40d5 100644 --- a/src/cron/service/timer.apply-job-result.test.ts +++ b/src/cron/service/timer.apply-job-result.test.ts @@ -137,4 +137,31 @@ describe("applyJobResult – delivered=true overrides error status (#50170)", () expect(job.state.lastRunStatus).toBe("ok"); expect(job.state.consecutiveErrors).toBe(0); }); + + it("does not apply error backoff to nextRunAtMs for recurring job when delivered=true overrides error", () => { + // Use a high-frequency schedule so the backoff (30 s minimum) would + // exceed the natural interval and push nextRunAtMs out if applied. + const schedule = { kind: "every" as const, intervalMs: 5_000 }; + + const jobError = createJob({ schedule }); + const jobOk = createJob({ id: "test-job-2", schedule }); + const stateError = createMockState([jobError]); + const stateOk = createMockState([jobOk]); + + applyJobResult(stateError, jobError, { + ...BASE_RESULT, + status: "error", + error: "Warning: Canvas failed", + delivered: true, + }); + + applyJobResult(stateOk, jobOk, { + ...BASE_RESULT, + status: "ok", + delivered: true, + }); + + // Both should schedule the same natural next run — no backoff on delivered=true. + expect(jobError.state.nextRunAtMs).toBe(jobOk.state.nextRunAtMs); + }); }); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index b15f1e96673..d18f14060d4 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -417,7 +417,7 @@ export function applyJobResult( ); } } - } else if (result.status === "error" && job.enabled) { + } else if (effectiveStatus === "error" && job.enabled) { // Apply exponential backoff for errored jobs to prevent retry storms. const backoff = errorBackoffMs(job.state.consecutiveErrors ?? 1); let normalNext: number | undefined;