From 5bb7f77b80934d49432c6acc96644f33c1c7b956 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sat, 11 Apr 2026 08:43:25 +0000 Subject: [PATCH] feat(tests): add regression test for timer execution to prevent store rollback during job execution --- tests/cron/test_cron_service.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/cron/test_cron_service.py b/tests/cron/test_cron_service.py index 4aa7fc06d..747f8ec81 100644 --- a/tests/cron/test_cron_service.py +++ b/tests/cron/test_cron_service.py @@ -329,6 +329,45 @@ async def test_external_update_preserves_run_history_records(tmp_path): fresh._save_store() +# ── timer race regression tests ── + + +@pytest.mark.asyncio +async def test_timer_execution_is_not_rolled_back_by_list_jobs_reload(tmp_path): + """list_jobs() during _on_timer should not replace the active store and re-run the same due job.""" + store_path = tmp_path / "cron" / "jobs.json" + calls: list[str] = [] + + async def on_job(job): + calls.append(job.id) + # Simulate frontend polling list_jobs while the timer callback is mid-execution. + service.list_jobs(include_disabled=True) + await asyncio.sleep(0) + + service = CronService(store_path, on_job=on_job) + service._running = True + service._load_store() + service._arm_timer = lambda: None + + job = service.add_job( + name="race", + schedule=CronSchedule(kind="every", every_ms=60_000), + message="hello", + ) + job.state.next_run_at_ms = max(1, int(time.time() * 1000) - 1_000) + service._save_store() + + await service._on_timer() + await service._on_timer() + + assert calls == [job.id] + loaded = service.get_job(job.id) + assert loaded is not None + assert loaded.state.last_run_at_ms is not None + assert loaded.state.next_run_at_ms is not None + assert loaded.state.next_run_at_ms > loaded.state.last_run_at_ms + + # ── update_job tests ──