| """Integration tests — verify subsystems work together. |
| |
| These test the wiring between components using mocks for external |
| dependencies (hardware, APIs) but real internal logic. |
| """ |
|
|
| import time |
| import numpy as np |
| from unittest.mock import MagicMock, patch |
|
|
| from jarvis.presence import PresenceLoop, State |
| from jarvis.robot.controller import RobotController |
|
|
|
|
| class TestPresenceWithFaceTracker: |
| """Test that face tracker feeds into presence loop correctly.""" |
|
|
| @patch("jarvis.vision.face_tracker.YOLO") |
| def test_face_tracker_drives_presence(self, mock_yolo_cls): |
| import torch |
|
|
| robot = RobotController(sim=True) |
| robot.connect() |
| presence = PresenceLoop(robot) |
|
|
| |
| mock_model = MagicMock() |
| mock_yolo_cls.return_value = mock_model |
|
|
| mock_box = MagicMock() |
| mock_box.xyxy = [torch.tensor([50.0, 200.0, 150.0, 300.0])] |
| mock_box.conf = [torch.tensor(0.9)] |
| mock_result = MagicMock() |
| mock_result.boxes = [mock_box] |
| mock_model.return_value = [mock_result] |
|
|
| frame = np.zeros((480, 640, 3), dtype=np.uint8) |
|
|
| from jarvis.vision.face_tracker import FaceTracker |
| tracker = FaceTracker( |
| presence=presence, |
| get_frame=lambda: frame, |
| fps=50, |
| ) |
|
|
| presence.signals.state = State.LISTENING |
| presence.start() |
| tracker.start() |
| time.sleep(0.3) |
| tracker.stop() |
| presence.stop() |
|
|
| |
| assert presence.signals.face_last_seen is not None |
|
|
|
|
| class TestPresenceStateTransitions: |
| """Test that presence loop handles rapid state changes.""" |
|
|
| def test_rapid_state_cycling(self, mock_robot): |
| presence = PresenceLoop(mock_robot) |
| presence.start() |
|
|
| |
| for _ in range(5): |
| for state in State: |
| presence.signals.state = state |
| time.sleep(0.02) |
|
|
| presence.stop() |
| |
|
|
|
|
| class TestEmbodyIntegration: |
| """Test that embody tool properly affects presence loop behavior.""" |
|
|
| def test_nod_affects_speaking_state(self, mock_robot): |
| from jarvis.tools.robot import bind |
|
|
| presence = PresenceLoop(mock_robot) |
| bind(mock_robot, presence) |
|
|
| |
| presence.signals.state = State.SPEAKING |
| presence.signals.intent_nod = 0.8 |
|
|
| |
| for i in range(50): |
| presence._do_speaking(float(i) * 0.033, presence.signals) |
|
|
| |
| |
| assert presence._pitch != 0.0 or presence._roll != 0.0 |
|
|
|
|
| class TestAudioPipelineFlow: |
| """Test audio pipeline data flow with mocked components.""" |
|
|
| @patch("jarvis.audio.vad.load_silero_vad") |
| @patch("jarvis.audio.vad.VADIterator") |
| @patch("jarvis.audio.stt.WhisperModel") |
| def test_vad_to_stt_flow(self, mock_whisper_cls, mock_vad_iter, mock_load_vad): |
| import torch |
|
|
| |
| mock_model = MagicMock() |
| mock_model.return_value = torch.tensor(0.9) |
| mock_load_vad.return_value = mock_model |
|
|
| |
| mock_whisper = MagicMock() |
| seg = MagicMock() |
| seg.text = "hello jarvis" |
| mock_whisper.transcribe.return_value = (iter([seg]), MagicMock()) |
| mock_whisper_cls.return_value = mock_whisper |
|
|
| from jarvis.audio.vad import VoiceActivityDetector |
| from jarvis.audio.stt import SpeechToText |
|
|
| vad = VoiceActivityDetector() |
| stt = SpeechToText() |
|
|
| |
| chunks = [] |
| for _ in range(10): |
| chunk = np.random.randn(512).astype(np.float32) * 0.1 |
| if vad.is_speech(chunk): |
| chunks.append(chunk) |
|
|
| |
| if chunks: |
| audio = np.concatenate(chunks) |
| text = stt.transcribe(audio) |
| assert text == "hello jarvis" |
|
|