diff --git a/nanobot/cron/service.py b/nanobot/cron/service.py index a75d024dd..ff8882a86 100644 --- a/nanobot/cron/service.py +++ b/nanobot/cron/service.py @@ -448,6 +448,13 @@ class CronService: job.state.last_error = None logger.info("Cron: job '{}' completed", job.name) + except asyncio.CancelledError as e: + current = asyncio.current_task() + if current is not None and current.cancelling(): + raise + job.state.last_status = "error" + job.state.last_error = str(e) or e.__class__.__name__ + logger.exception("Cron: job '{}' was cancelled", job.name) except Exception as e: job.state.last_status = "error" job.state.last_error = str(e) diff --git a/tests/cron/test_cron_service.py b/tests/cron/test_cron_service.py index fa304e06e..195ba9c97 100644 --- a/tests/cron/test_cron_service.py +++ b/tests/cron/test_cron_service.py @@ -137,6 +137,33 @@ async def test_run_history_records_errors(tmp_path) -> None: assert loaded.state.run_history[0].error == "boom" +@pytest.mark.asyncio +async def test_run_history_records_job_cancellation(tmp_path) -> None: + store_path = tmp_path / "cron" / "jobs.json" + + async def cancel(_): + raise asyncio.CancelledError("turn cancelled") + + service = CronService(store_path, on_job=cancel) + job = service.add_job( + name="cancel", + schedule=CronSchedule(kind="every", every_ms=60_000), + message="hello", + session_key="websocket:chat-1", + ) + + assert await service.run_job(job.id) is True + + loaded = service.get_job(job.id) + assert loaded is not None + assert loaded.state.last_status == "error" + assert loaded.state.last_error == "turn cancelled" + assert len(loaded.state.run_history) == 1 + assert loaded.state.run_history[0].status == "error" + assert loaded.state.run_history[0].error == "turn cancelled" + assert loaded.state.next_run_at_ms is not None + + @pytest.mark.asyncio async def test_run_history_trimmed_to_max(tmp_path) -> None: store_path = tmp_path / "cron" / "jobs.json"