From dad9fc882137eed86845f55a4ad5ebb23485bee2 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:43:08 -0700 Subject: [PATCH] test(cron): pin resume keeping a paused-over slot due, and the future/null control Red on origin/main for the kept-slot case (resume re-anchored past it), green with the fix; the control pins that future and null stored slots still recompute from now. --- tests/cron/test_jobs.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index cd81b8a61b..ee4303e1c6 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -622,6 +622,41 @@ class TestPauseResumeJob: with pytest.raises(ValueError, match="in the past"): resume_job("test-resume-past") + def test_resume_keeps_slot_that_elapsed_while_paused_due(self, tmp_cron_dir, monkeypatch): + """A recurring job paused before its slot and resumed after it comes back with that slot + still due — the due scan then fires it (late/catch-up) or logs the skip. Re-anchoring + from now consumed the occurrence with no run, no ledger row and no log line (#113603).""" + now = datetime(2026, 9, 16, 17, 0, 0, tzinfo=timezone.utc) + monkeypatch.setattr("cron.jobs._hermes_now", lambda: now) + job = create_job(prompt="daily pipeline", schedule="30 1 * * *", deliver="local") + stored = load_jobs() + row = next(r for r in stored if r["id"] == job["id"]) + slot = datetime(2026, 9, 16, 1, 30, 0, tzinfo=timezone.utc).isoformat() + row["next_run_at"] = slot + save_jobs(stored) + + pause_job(job["id"], reason="ops audit") + assert job["id"] not in {j["id"] for j in get_due_jobs()} + assert get_job(job["id"])["next_run_at"] == slot + + assert resume_job(job["id"])["next_run_at"] == slot + assert job["id"] in {j["id"] for j in get_due_jobs()} + + def test_resume_recomputes_future_or_missing_slot_from_now(self, tmp_cron_dir, monkeypatch): + """Control: a paused job whose stored slot is still ahead, or created ``--paused`` with no + slot, resumes onto the next future occurrence as before.""" + now = datetime(2026, 9, 16, 17, 0, 0, tzinfo=timezone.utc) + monkeypatch.setattr("cron.jobs._hermes_now", lambda: now) + ahead = create_job(prompt="daily", schedule="30 1 * * *", deliver="local") + pause_job(ahead["id"]) + canary = create_job(prompt="canary", schedule="0 9 * * *", deliver="local", paused=True) + assert get_job(canary["id"])["next_run_at"] is None + + for jid in (ahead["id"], canary["id"]): + resumed = resume_job(jid) + assert datetime.fromisoformat(resumed["next_run_at"]) > now + assert jid not in {j["id"] for j in get_due_jobs()} + class TestResolveJobRef: """Name-based job lookup for CLI/tool callers (PR #2627, @buntingszn)."""