"""Shared fixtures for Jarvis tests. All hardware dependencies (robot, audio, camera, APIs) are mocked. Tests should run without any external services or devices. """ from __future__ import annotations import os from pathlib import Path import pytest import numpy as np from unittest.mock import AsyncMock, MagicMock # Set required env vars before any imports touch Config os.environ.setdefault("OPENAI_API_KEY", "test-key-not-real") os.environ.setdefault("ELEVENLABS_API_KEY", "test-key-not-real") @pytest.fixture def config(tmp_path, monkeypatch): """Config with test defaults.""" monkeypatch.setenv("MEMORY_PATH", str(tmp_path / "memory.sqlite")) monkeypatch.setenv("EXPANSION_STATE_PATH", str(tmp_path / "expansion-state.json")) monkeypatch.setenv("NOTES_CAPTURE_DIR", str(tmp_path / "notes")) monkeypatch.setenv("QUALITY_REPORT_DIR", str(tmp_path / "quality-reports")) project_root = Path(__file__).resolve().parents[1] monkeypatch.setenv("RELEASE_CHANNEL_CONFIG_PATH", str(project_root / "config" / "release-channels.json")) from jarvis.config import Config return Config() @pytest.fixture def mock_robot(): """A RobotController in simulation mode.""" from jarvis.robot.controller import RobotController robot = RobotController(sim=True) robot.connect() return robot @pytest.fixture def presence(mock_robot): """A PresenceLoop bound to a simulated robot (not started).""" from jarvis.presence import PresenceLoop return PresenceLoop(mock_robot) @pytest.fixture def audio_16k_silence(): """512 samples of silence at 16kHz (one VAD chunk).""" return np.zeros(512, dtype=np.float32) @pytest.fixture def audio_16k_noise(): """512 samples of white noise at 16kHz (one VAD chunk).""" rng = np.random.default_rng(42) return rng.standard_normal(512).astype(np.float32) * 0.5 @pytest.fixture def sample_frame(): """A dummy 480x640 RGB camera frame.""" return np.zeros((480, 640, 3), dtype=np.uint8) def _aiohttp_context_manager(response: AsyncMock) -> AsyncMock: ctx = AsyncMock() ctx.__aenter__ = AsyncMock(return_value=response) ctx.__aexit__ = AsyncMock(return_value=False) return ctx @pytest.fixture def aiohttp_response(): """Factory for aiohttp-like response mocks.""" def _build( *, status: int = 200, json_data=None, json_side_effect=None, text_data: str = "", text_side_effect=None, ) -> AsyncMock: response = AsyncMock() response.status = status if json_side_effect is not None: response.json = AsyncMock(side_effect=json_side_effect) else: response.json = AsyncMock(return_value=json_data) if text_side_effect is not None: response.text = AsyncMock(side_effect=text_side_effect) else: response.text = AsyncMock(return_value=text_data) return response return _build @pytest.fixture def aiohttp_session_mock(): """Factory for aiohttp.ClientSession context manager mocks.""" def _build(*, get=None, post=None) -> AsyncMock: session = AsyncMock() session.__aenter__ = AsyncMock(return_value=session) session.__aexit__ = AsyncMock(return_value=False) if get is None: session.get = MagicMock() elif isinstance(get, list): session.get = MagicMock(side_effect=[_aiohttp_context_manager(resp) for resp in get]) else: session.get = MagicMock(return_value=_aiohttp_context_manager(get)) if post is None: session.post = MagicMock() elif isinstance(post, list): session.post = MagicMock(side_effect=[_aiohttp_context_manager(resp) for resp in post]) else: session.post = MagicMock(return_value=_aiohttp_context_manager(post)) return session return _build