From 36b1a6e62c9e07cff9b84b6e2b496285948f347a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:32:21 -0700 Subject: [PATCH] test(cron): crash-safety tests claim the fire before asserting not-due MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit advance_next_run alone no longer commits an occurrence — a restart before the fire claim restores it (#107485). The at-most-once assertions model the real tick sequence (advance → claim) so they keep guarding the mid-run crash case. --- tests/cron/test_cron_timezone_migration_catchup.py | 4 +++- tests/cron/test_jobs.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/cron/test_cron_timezone_migration_catchup.py b/tests/cron/test_cron_timezone_migration_catchup.py index ef4cf4a108..180b473fe0 100644 --- a/tests/cron/test_cron_timezone_migration_catchup.py +++ b/tests/cron/test_cron_timezone_migration_catchup.py @@ -85,13 +85,15 @@ def test_legacy_utc_offset_next_run_still_fires(temp_home, monkeypatch): def test_legacy_offset_catchup_fires_at_most_once(temp_home, monkeypatch): """The catch-up run is a single fire: once the scheduler advances the job, the legacy instant is gone and a second scan finds nothing due.""" - from cron.jobs import advance_next_run, get_due_jobs, get_job + from cron.jobs import advance_next_run, claim_job_for_fire, get_due_jobs, get_job monkeypatch.setattr("cron.jobs._hermes_now", lambda: _BRUSSELS_NOW) jid = _write_cron_job(_DAILY_0400, _LEGACY_UTC_NEXT_RUN) assert jid in [j["id"] for j in get_due_jobs()] assert advance_next_run(jid) is True + # The fire claim is what commits the occurrence; without it a restart restores the slot. + assert claim_job_for_fire(jid) # Re-anchored to tomorrow's occurrence, expressed in the configured zone. assert get_job(jid)["next_run_at"] == "2026-09-03T04:00:00+02:00" diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index 4bfdde1e99..cd81b8a61b 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -860,12 +860,15 @@ class TestAdvanceNextRun: due_before = get_due_jobs() assert len(due_before) == 1 - # Advance (simulating what tick() does before run_job) + # Advance + claim (what tick() does before run_job); the claim is the point after which + # side effects may exist, so a restart after it must not re-fire (#3396). A restart + # BEFORE the claim restores the occurrence instead (#107485, test_missed_window_catchup). advance_next_run(job["id"]) + assert claim_job_for_fire(job["id"]) - # Now the job should NOT be due (simulates restart after crash) + # Now the job should NOT be due (simulates restart after a mid-run crash) due_after = get_due_jobs() - assert len(due_after) == 0, "Job should not be due after advance_next_run" + assert len(due_after) == 0, "Job should not be due after advance + claim" class TestGetDueJobs: