| from __future__ import annotations |
|
|
| from types import SimpleNamespace |
| from unittest.mock import MagicMock, patch |
|
|
| import pytest |
|
|
| from jarvis.runtime_lifecycle import start, stop |
|
|
|
|
| def _runtime_stub() -> SimpleNamespace: |
| return SimpleNamespace( |
| _started=False, |
| _startup_blockers=lambda: [], |
| _skills=SimpleNamespace(discover=MagicMock()), |
| _publish_skills_status=MagicMock(), |
| _publish_observability_status=MagicMock(), |
| _publish_observability_snapshot=MagicMock(), |
| _publish_voice_status=MagicMock(), |
| _save_runtime_state=MagicMock(), |
| _default_stt_diagnostics=lambda: {"confidence_band": "unknown"}, |
| _output_stream=None, |
| face_tracker=None, |
| hand_tracker=None, |
| _use_robot_audio=False, |
| _observability=None, |
| args=SimpleNamespace(no_vision=True), |
| tts=None, |
| robot=SimpleNamespace( |
| sim=True, |
| connect=MagicMock(), |
| start_audio=MagicMock(), |
| stop_audio=MagicMock(), |
| disconnect=MagicMock(), |
| get_input_audio_samplerate=MagicMock(return_value=None), |
| get_output_audio_samplerate=MagicMock(return_value=None), |
| get_frame=MagicMock(), |
| ), |
| presence=SimpleNamespace(start=MagicMock(), stop=MagicMock()), |
| config=SimpleNamespace( |
| motion_enabled=False, |
| hand_track_enabled=False, |
| sample_rate=16000, |
| yolo_model="model", |
| face_track_fps=15, |
| ), |
| stop=MagicMock(), |
| ) |
|
|
|
|
| def test_start_configures_local_output_stream_when_tts_enabled() -> None: |
| runtime = _runtime_stub() |
| runtime.tts = object() |
|
|
| class _FakeStream: |
| def __init__(self, **kwargs) -> None: |
| self.kwargs = kwargs |
| self.started = False |
|
|
| def start(self) -> None: |
| self.started = True |
|
|
| require_sounddevice = MagicMock() |
| logger = SimpleNamespace(info=MagicMock()) |
| sd_module = SimpleNamespace(OutputStream=_FakeStream) |
|
|
| start( |
| runtime, |
| require_sounddevice_fn=require_sounddevice, |
| sd_module=sd_module, |
| build_face_tracker_fn=lambda: None, |
| build_hand_tracker_fn=lambda: None, |
| sleep_fn=lambda _delay: None, |
| logger=logger, |
| ) |
|
|
| assert runtime._started is True |
| runtime.robot.connect.assert_called_once() |
| require_sounddevice.assert_called_once_with("local audio playback") |
| assert runtime._output_stream is not None |
| assert runtime._output_stream.started is True |
| runtime._publish_voice_status.assert_called_once() |
|
|
|
|
| def test_start_reraises_and_calls_runtime_stop_on_failure() -> None: |
| runtime = _runtime_stub() |
| runtime.tts = object() |
| require_sounddevice = MagicMock(side_effect=RuntimeError("sounddevice unavailable")) |
|
|
| with pytest.raises(RuntimeError, match="sounddevice unavailable"): |
| start( |
| runtime, |
| require_sounddevice_fn=require_sounddevice, |
| sd_module=SimpleNamespace(OutputStream=MagicMock()), |
| build_face_tracker_fn=lambda: None, |
| build_hand_tracker_fn=lambda: None, |
| sleep_fn=lambda _delay: None, |
| logger=SimpleNamespace(info=MagicMock()), |
| ) |
|
|
| runtime.stop.assert_called_once() |
|
|
|
|
| def test_start_builds_trackers_and_configures_robot_audio_on_hardware() -> None: |
| runtime = _runtime_stub() |
| runtime.robot.sim = False |
| runtime.args.no_vision = False |
| runtime.config.motion_enabled = True |
| runtime.config.hand_track_enabled = True |
| face_tracker = SimpleNamespace(start=MagicMock()) |
| hand_tracker = SimpleNamespace(start=MagicMock()) |
| logger = SimpleNamespace(info=MagicMock()) |
| sleep_fn = MagicMock() |
|
|
| start( |
| runtime, |
| require_sounddevice_fn=MagicMock(), |
| sd_module=SimpleNamespace(OutputStream=MagicMock()), |
| build_face_tracker_fn=lambda: face_tracker, |
| build_hand_tracker_fn=lambda: hand_tracker, |
| sleep_fn=sleep_fn, |
| logger=logger, |
| ) |
|
|
| runtime.presence.start.assert_called_once() |
| face_tracker.start.assert_called_once() |
| hand_tracker.start.assert_called_once() |
| runtime.robot.start_audio.assert_called_once_with(recording=True, playing=False) |
| sleep_fn.assert_called_once_with(0.2) |
| assert runtime._use_robot_audio is True |
| assert runtime._robot_input_sr == 16000 |
| assert runtime._robot_output_sr == 16000 |
|
|
|
|
| def test_stop_suppresses_component_errors_and_resets_state() -> None: |
| runtime = _runtime_stub() |
| runtime._started = True |
| runtime._use_robot_audio = True |
| runtime._output_stream = MagicMock() |
| runtime._output_stream.stop.side_effect = RuntimeError("stop failed") |
| runtime._output_stream.close.side_effect = RuntimeError("close failed") |
| runtime.face_tracker = MagicMock() |
| runtime.face_tracker.stop.side_effect = RuntimeError("face stop failed") |
| runtime.hand_tracker = MagicMock() |
| runtime.hand_tracker.stop.side_effect = RuntimeError("hand stop failed") |
| runtime.robot.stop_audio.side_effect = RuntimeError("audio stop failed") |
| runtime.robot.disconnect.side_effect = RuntimeError("disconnect failed") |
| runtime.config.motion_enabled = True |
| runtime.presence.stop.side_effect = RuntimeError("presence stop failed") |
| logger = SimpleNamespace(info=MagicMock()) |
|
|
| with patch("jarvis.runtime_lifecycle.set_runtime_voice_state") as set_voice_state: |
| stop(runtime, logger=logger) |
|
|
| assert runtime._started is False |
| assert runtime._output_stream is None |
| assert runtime.face_tracker is None |
| assert runtime.hand_tracker is None |
| set_voice_state.assert_called_once() |
| runtime._publish_observability_status.assert_called_once() |
| runtime._publish_skills_status.assert_called_once() |
| logger.info.assert_called_once_with("Jarvis offline.") |
|
|