# -*- coding: utf-8 -*- """ Tests für Event-basierte Integrationen. Testet: - Satellite.speak() und Satellite.play() Event-Auslösung - SatelliteAPI.say() mit Warte-Logik - WakewordService._wait_for_processing_result() """ import asyncio import pytest from unittest.mock import AsyncMock, MagicMock, patch from dataclasses import dataclass, field from trixy_core.events.event_data.basic import ( TTSRequest, TTSCompleted, StreamStart, StreamStop, ProcessingResult, ) from trixy_core.satellite.satellite import Satellite, ConnectionState # ============================================================================= # Fixtures # ============================================================================= @pytest.fixture def mock_event_manager(): """Erstellt einen Mock-EventManager.""" manager = MagicMock() manager.trigger = AsyncMock() manager.register = MagicMock() manager.unregister = MagicMock(return_value=True) return manager @pytest.fixture def mock_application(mock_event_manager): """Erstellt eine Mock-Application.""" app = MagicMock() app.events = mock_event_manager app.event_manager = mock_event_manager return app @pytest.fixture def satellite_with_app(mock_application): """Erstellt einen Satellite mit Application-Referenz.""" sat = Satellite( satellite_id="test-sat-001", room_id="wohnzimmer", mac_address="AA:BB:CC:DD:EE:FF", alias="TestSatellite", application=mock_application ) return sat # ============================================================================= # Tests für Event-Datenklassen # ============================================================================= class TestEventDataClasses: """Tests für neue Event-Datenklassen.""" def test_tts_request_creation(self): """Testet TTSRequest Erstellung.""" request = TTSRequest( request_id="req-123", satellite_id="sat-001", text="Hallo Welt", voice="default", speed=1.0, volume=0.8 ) assert request.request_id == "req-123" assert request.text == "Hallo Welt" assert request.volume == 0.8 def test_tts_completed_creation(self): """Testet TTSCompleted Erstellung.""" completed = TTSCompleted( request_id="req-123", satellite_id="sat-001", success=True, audio_duration=2.5 ) assert completed.success is True assert completed.audio_duration == 2.5 def test_stream_start_creation(self): """Testet StreamStart Erstellung.""" stream = StreamStart( satellite_id="sat-001", source="http://radio.mp3", volume=0.7, stream_type="music" ) assert stream.source == "http://radio.mp3" assert stream.stream_type == "music" def test_stream_stop_creation(self): """Testet StreamStop Erstellung.""" stop = StreamStop( satellite_id="sat-001", stream_type="all" ) assert stop.stream_type == "all" def test_processing_result_creation(self): """Testet ProcessingResult Erstellung.""" result = ProcessingResult( session_id="session-123", success=True, text="Wie ist das Wetter?", intent="weather.query", response_text="Es ist sonnig." ) assert result.intent == "weather.query" assert result.response_text == "Es ist sonnig." # ============================================================================= # Tests für Satellite Event-Integration # ============================================================================= class TestSatelliteEventIntegration: """Tests für Satellite Event-Methoden.""" @pytest.mark.asyncio async def test_speak_triggers_tts_request_event(self, satellite_with_app, mock_event_manager): """Testet, dass speak() das tts_request Event auslöst.""" result = await satellite_with_app.speak("Hallo Welt", voice="alloy") assert result is True mock_event_manager.trigger.assert_called_once() # Prüfe Event-Name call_args = mock_event_manager.trigger.call_args assert call_args[0][0] == "tts_request" # Prüfe Event-Daten event_data = call_args[0][1] assert event_data.text == "Hallo Welt" assert event_data.voice == "alloy" assert event_data.satellite_id == "test-sat-001" @pytest.mark.asyncio async def test_speak_returns_false_without_application(self): """Testet, dass speak() False zurückgibt ohne Application.""" sat = Satellite(satellite_id="no-app") result = await sat.speak("Test") assert result is False @pytest.mark.asyncio async def test_speak_returns_false_when_cancelled(self, satellite_with_app, mock_event_manager): """Testet, dass speak() False zurückgibt wenn Event gecancelt.""" async def cancel_event(event_name, data): data.cancel() mock_event_manager.trigger = cancel_event result = await satellite_with_app.speak("Test") assert result is False @pytest.mark.asyncio async def test_play_triggers_stream_start_event(self, satellite_with_app, mock_event_manager): """Testet, dass play() das stream_start Event auslöst.""" result = await satellite_with_app.play("http://radio.mp3", volume=0.5) assert result is True mock_event_manager.trigger.assert_called_once() call_args = mock_event_manager.trigger.call_args assert call_args[0][0] == "stream_start" event_data = call_args[0][1] assert event_data.source == "http://radio.mp3" assert event_data.volume == 0.5 assert event_data.stream_type == "music" @pytest.mark.asyncio async def test_play_returns_false_without_application(self): """Testet, dass play() False zurückgibt ohne Application.""" sat = Satellite(satellite_id="no-app") result = await sat.play("http://test.mp3") assert result is False @pytest.mark.asyncio async def test_stop_playback_triggers_stream_stop_event(self, satellite_with_app, mock_event_manager): """Testet, dass stop_playback() das stream_stop Event auslöst.""" result = await satellite_with_app.stop_playback() assert result is True mock_event_manager.trigger.assert_called_once() call_args = mock_event_manager.trigger.call_args assert call_args[0][0] == "stream_stop" event_data = call_args[0][1] assert event_data.satellite_id == "test-sat-001" assert event_data.stream_type == "all" # ============================================================================= # Tests für Satellite Application-Referenz # ============================================================================= class TestSatelliteApplicationReference: """Tests für Satellite Application-Referenz.""" def test_satellite_with_application(self, mock_application): """Testet Satellite-Erstellung mit Application.""" sat = Satellite( satellite_id="test", application=mock_application ) assert sat.application is mock_application def test_satellite_application_setter(self, mock_application): """Testet Application-Setter.""" sat = Satellite(satellite_id="test") assert sat.application is None sat.application = mock_application assert sat.application is mock_application def test_satellite_without_application(self): """Testet Satellite ohne Application.""" sat = Satellite(satellite_id="test") assert sat.application is None # ============================================================================= # Tests für SatelliteAPI Warte-Logik # ============================================================================= class TestSatelliteAPIWaitLogic: """Tests für SatelliteAPI.say() Warte-Logik.""" @pytest.fixture def mock_satellite_manager(self): """Erstellt einen Mock-SatelliteManager.""" manager = MagicMock() connected_sat = MagicMock() connected_sat.id = "sat-001" connected_sat.is_connected = True connected_sat.room_id = "wohnzimmer" manager.get = MagicMock(return_value=connected_sat) manager.list_connected = MagicMock(return_value=[connected_sat]) manager.get_by_room = MagicMock(return_value=[connected_sat]) return manager @pytest.fixture def satellite_api(self, mock_application, mock_satellite_manager): """Erstellt eine SatelliteAPI-Instanz.""" from trixy_core.satellite.api import SatelliteAPI mock_application.satellite_manager = mock_satellite_manager return SatelliteAPI(mock_application) @pytest.mark.asyncio async def test_say_with_wait_registers_handler(self, satellite_api, mock_event_manager): """Testet, dass say() mit wait=True einen Handler registriert.""" # Simuliere sofortige Completion async def trigger_and_complete(event_name, data): if event_name == "tts_request": # Finde den registrierten Handler und rufe ihn auf for call in mock_event_manager.register.call_args_list: if call[0][0] == "tts_completed": handler = call[0][1] completed = TTSCompleted( request_id=data.request_id, success=True ) await handler("tts_completed", completed) mock_event_manager.trigger = trigger_and_complete result = await satellite_api.say("Test", satellites=["sat-001"], wait=True) # Handler sollte registriert worden sein assert mock_event_manager.register.called # Handler sollte wieder entfernt worden sein assert mock_event_manager.unregister.called @pytest.mark.asyncio async def test_say_without_wait_does_not_wait(self, satellite_api, mock_event_manager): """Testet, dass say() mit wait=False nicht wartet.""" result = await satellite_api.say("Test", satellites=["sat-001"], wait=False) assert result is True mock_event_manager.trigger.assert_called_once() # Kein Handler sollte registriert werden bei wait=False # (nur tts_request wird ausgelöst) @pytest.mark.asyncio async def test_say_returns_false_for_empty_targets(self, satellite_api, mock_satellite_manager): """Testet, dass say() False zurückgibt wenn keine Ziele.""" mock_satellite_manager.get_by_room = MagicMock(return_value=[]) mock_satellite_manager.list_connected = MagicMock(return_value=[]) result = await satellite_api.say("Test", room="leerer_raum") assert result is False # ============================================================================= # Tests für WakewordService Verarbeitungs-Warte-Logik # ============================================================================= class TestWakewordServiceProcessingWait: """Tests für WakewordService._wait_for_processing_result().""" @pytest.fixture def mock_wakeword_service(self, mock_application): """Erstellt einen Mock-WakewordService mittels MagicMock.""" from trixy_core.wakeword.service import ( WakewordServiceConfig, RecordingSession, ServiceState, WakewordService, ) from trixy_core.wakeword.detector import WakewordDetection, WakewordType from datetime import datetime import logging # Verwende MagicMock statt echter Klasse service = MagicMock(spec=WakewordService) service._application = mock_application service._config = WakewordServiceConfig( standalone_mode=True, follow_up_timeout_seconds=0.1 # Kurzer Timeout für Tests ) service.logger = logging.getLogger("test") # Simuliere aktive Session service._current_session = RecordingSession( session_id="test-session-123", wakeword_detection=WakewordDetection( wakeword_type=WakewordType.CUSTOM, model_name="test", confidence=0.95, audio_level=0.7, timestamp=datetime.now() ), start_time=datetime.now() ) # Binde die echte Methode an den Mock from trixy_core.wakeword.service import WakewordService as RealService service._wait_for_processing_result = lambda: RealService._wait_for_processing_result(service) return service @pytest.mark.asyncio async def test_wait_for_processing_result_with_immediate_response( self, mock_wakeword_service, mock_event_manager ): """Testet _wait_for_processing_result mit sofortiger Antwort.""" # Speichere den registrierten Handler um ihn später aufzurufen registered_handler = None def capture_register(event_name, handler): nonlocal registered_handler if event_name == "processing_result": registered_handler = handler mock_event_manager.register = capture_register # Starte den Wait in einem Task async def wait_and_respond(): # Kurz warten damit der Handler registriert wird await asyncio.sleep(0.01) if registered_handler: result = ProcessingResult( session_id="test-session-123", success=True, intent="test.intent" ) await registered_handler("processing_result", result) # Starte Response-Task response_task = asyncio.create_task(wait_and_respond()) # Führe wait aus await mock_wakeword_service._wait_for_processing_result() # Warte auf Response-Task await response_task # Handler sollte registriert worden sein assert registered_handler is not None @pytest.mark.asyncio async def test_wait_for_processing_result_timeout( self, mock_wakeword_service, mock_event_manager ): """Testet Timeout-Handling.""" # Registriere Handler aber triggere ihn nicht mock_event_manager.register = MagicMock() # Sollte durch Timeout beendet werden (0.1s Timeout) await mock_wakeword_service._wait_for_processing_result() # Handler sollte wieder entfernt werden (im finally-Block) assert mock_event_manager.unregister.called @pytest.mark.asyncio async def test_wait_for_processing_result_without_session( self, mock_wakeword_service, mock_event_manager ): """Testet Verhalten ohne aktive Session.""" mock_wakeword_service._current_session = None # Sollte sofort zurückkehren await mock_wakeword_service._wait_for_processing_result() # Kein Handler sollte registriert werden assert not mock_event_manager.register.called @pytest.mark.asyncio async def test_wait_for_processing_result_without_event_manager( self, mock_wakeword_service ): """Testet Verhalten ohne EventManager.""" mock_wakeword_service._application.events = None # Sollte ohne Fehler zurückkehren await mock_wakeword_service._wait_for_processing_result() # ============================================================================= # Tests für SatelliteManager Application-Referenz # ============================================================================= class TestSatelliteManagerApplicationRef: """Tests für SatelliteManager Application-Referenz Weitergabe.""" def test_add_sets_application_reference(self, mock_application): """Testet, dass add() die Application-Referenz setzt.""" from trixy_core.satellite.satellite_manager import SatelliteManager manager = SatelliteManager(mock_application) sat = Satellite(satellite_id="test-sat") assert sat.application is None manager.add(sat) assert sat.application is mock_application # ============================================================================= # Integration Tests # ============================================================================= class TestFullIntegration: """Vollständige Integrationstests.""" @pytest.mark.asyncio async def test_satellite_speak_full_flow(self): """Testet den vollständigen speak()-Flow.""" from trixy_core.events.eventmanager import EventManager # Mock Application für EventManager app = MagicMock() # Echten EventManager verwenden event_manager = EventManager(app) app.events = event_manager # Satellite erstellen sat = Satellite( satellite_id="integration-test", application=app ) # TTS-Handler registrieren tts_requests = [] async def on_tts_request(event_name, data): tts_requests.append(data) event_manager.register("tts_request", on_tts_request) # speak() aufrufen result = await sat.speak("Integration Test", voice="nova") assert result is True assert len(tts_requests) == 1 assert tts_requests[0].text == "Integration Test" assert tts_requests[0].voice == "nova" @pytest.mark.asyncio async def test_satellite_play_full_flow(self): """Testet den vollständigen play()-Flow.""" from trixy_core.events.eventmanager import EventManager app = MagicMock() event_manager = EventManager(app) app.events = event_manager sat = Satellite(satellite_id="play-test", application=app) stream_starts = [] async def on_stream_start(event_name, data): stream_starts.append(data) event_manager.register("stream_start", on_stream_start) result = await sat.play("http://example.com/music.mp3", volume=0.8) assert result is True assert len(stream_starts) == 1 assert stream_starts[0].source == "http://example.com/music.mp3" assert stream_starts[0].volume == 0.8