diff --git a/nanobot/heartbeat/service.py b/nanobot/heartbeat/service.py index 7dbdc03..cb1a1c7 100644 --- a/nanobot/heartbeat/service.py +++ b/nanobot/heartbeat/service.py @@ -80,6 +80,9 @@ class HeartbeatService: if not self.enabled: logger.info("Heartbeat disabled") return + if self._running: + logger.warning("Heartbeat already running") + return self._running = True self._task = asyncio.create_task(self._run_loop()) diff --git a/tests/test_heartbeat_service.py b/tests/test_heartbeat_service.py new file mode 100644 index 0000000..ec91c6b --- /dev/null +++ b/tests/test_heartbeat_service.py @@ -0,0 +1,44 @@ +import asyncio + +import pytest + +from nanobot.heartbeat.service import ( + HEARTBEAT_OK_TOKEN, + HeartbeatService, +) + + +def test_heartbeat_ok_detection() -> None: + def is_ok(response: str) -> bool: + return HEARTBEAT_OK_TOKEN in response.upper() + + assert is_ok("HEARTBEAT_OK") + assert is_ok("`HEARTBEAT_OK`") + assert is_ok("**HEARTBEAT_OK**") + assert is_ok("heartbeat_ok") + assert is_ok("HEARTBEAT_OK.") + + assert not is_ok("HEARTBEAT_NOT_OK") + assert not is_ok("all good") + + +@pytest.mark.asyncio +async def test_start_is_idempotent(tmp_path) -> None: + async def _on_heartbeat(_: str) -> str: + return "HEARTBEAT_OK" + + service = HeartbeatService( + workspace=tmp_path, + on_heartbeat=_on_heartbeat, + interval_s=9999, + enabled=True, + ) + + await service.start() + first_task = service._task + await service.start() + + assert service._task is first_task + + service.stop() + await asyncio.sleep(0)