from __future__ import annotations import asyncio import threading from types import SimpleNamespace from unittest.mock import MagicMock import pytest from jarvis.presence import State from jarvis.runtime_watchdog import watchdog_loop @pytest.mark.asyncio async def test_watchdog_loop_resets_stuck_state_and_records_event() -> None: observability = SimpleNamespace(record_event=MagicMock()) runtime = SimpleNamespace( config=SimpleNamespace( watchdog_listening_timeout_sec=0.05, watchdog_thinking_timeout_sec=0.05, watchdog_speaking_timeout_sec=0.05, ), presence=SimpleNamespace(signals=SimpleNamespace(state=State.THINKING)), _runtime_invariant_checked_monotonic=0.0, _check_runtime_invariants=MagicMock(), _barge_in=threading.Event(), _flush_output=MagicMock(), _clear_tts_queue=MagicMock(), _telemetry={"fallback_responses": 0.0}, _observability=observability, ) task = asyncio.create_task( watchdog_loop( runtime, state_idle=State.IDLE, state_listening=State.LISTENING, state_thinking=State.THINKING, state_speaking=State.SPEAKING, poll_sec=0.01, logger=SimpleNamespace(warning=MagicMock()), ) ) await asyncio.sleep(0.09) task.cancel() with pytest.raises(asyncio.CancelledError): await task assert runtime.presence.signals.state == State.IDLE assert runtime._flush_output.called assert runtime._clear_tts_queue.called assert runtime._telemetry["fallback_responses"] >= 1.0 observability.record_event.assert_called() runtime._check_runtime_invariants.assert_called()