test_event_integration.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Event-basierte Integrationen.
  4. Testet:
  5. - Satellite.speak() und Satellite.play() Event-Auslösung
  6. - SatelliteAPI.say() mit Warte-Logik
  7. - WakewordService._wait_for_processing_result()
  8. """
  9. import asyncio
  10. import pytest
  11. from unittest.mock import AsyncMock, MagicMock, patch
  12. from dataclasses import dataclass, field
  13. from trixy_core.events.event_data.basic import (
  14. TTSRequest,
  15. TTSCompleted,
  16. StreamStart,
  17. StreamStop,
  18. ProcessingResult,
  19. )
  20. from trixy_core.satellite.satellite import Satellite, ConnectionState
  21. # =============================================================================
  22. # Fixtures
  23. # =============================================================================
  24. @pytest.fixture
  25. def mock_event_manager():
  26. """Erstellt einen Mock-EventManager."""
  27. manager = MagicMock()
  28. manager.trigger = AsyncMock()
  29. manager.register = MagicMock()
  30. manager.unregister = MagicMock(return_value=True)
  31. return manager
  32. @pytest.fixture
  33. def mock_application(mock_event_manager):
  34. """Erstellt eine Mock-Application."""
  35. app = MagicMock()
  36. app.events = mock_event_manager
  37. app.event_manager = mock_event_manager
  38. return app
  39. @pytest.fixture
  40. def satellite_with_app(mock_application):
  41. """Erstellt einen Satellite mit Application-Referenz."""
  42. sat = Satellite(
  43. satellite_id="test-sat-001",
  44. room_id="wohnzimmer",
  45. mac_address="AA:BB:CC:DD:EE:FF",
  46. alias="TestSatellite",
  47. application=mock_application
  48. )
  49. return sat
  50. # =============================================================================
  51. # Tests für Event-Datenklassen
  52. # =============================================================================
  53. class TestEventDataClasses:
  54. """Tests für neue Event-Datenklassen."""
  55. def test_tts_request_creation(self):
  56. """Testet TTSRequest Erstellung."""
  57. request = TTSRequest(
  58. request_id="req-123",
  59. satellite_id="sat-001",
  60. text="Hallo Welt",
  61. voice="default",
  62. speed=1.0,
  63. volume=0.8
  64. )
  65. assert request.request_id == "req-123"
  66. assert request.text == "Hallo Welt"
  67. assert request.volume == 0.8
  68. def test_tts_completed_creation(self):
  69. """Testet TTSCompleted Erstellung."""
  70. completed = TTSCompleted(
  71. request_id="req-123",
  72. satellite_id="sat-001",
  73. success=True,
  74. audio_duration=2.5
  75. )
  76. assert completed.success is True
  77. assert completed.audio_duration == 2.5
  78. def test_stream_start_creation(self):
  79. """Testet StreamStart Erstellung."""
  80. stream = StreamStart(
  81. satellite_id="sat-001",
  82. source="http://radio.mp3",
  83. volume=0.7,
  84. stream_type="music"
  85. )
  86. assert stream.source == "http://radio.mp3"
  87. assert stream.stream_type == "music"
  88. def test_stream_stop_creation(self):
  89. """Testet StreamStop Erstellung."""
  90. stop = StreamStop(
  91. satellite_id="sat-001",
  92. stream_type="all"
  93. )
  94. assert stop.stream_type == "all"
  95. def test_processing_result_creation(self):
  96. """Testet ProcessingResult Erstellung."""
  97. result = ProcessingResult(
  98. session_id="session-123",
  99. success=True,
  100. text="Wie ist das Wetter?",
  101. intent="weather.query",
  102. response_text="Es ist sonnig."
  103. )
  104. assert result.intent == "weather.query"
  105. assert result.response_text == "Es ist sonnig."
  106. # =============================================================================
  107. # Tests für Satellite Event-Integration
  108. # =============================================================================
  109. class TestSatelliteEventIntegration:
  110. """Tests für Satellite Event-Methoden."""
  111. @pytest.mark.asyncio
  112. async def test_speak_triggers_tts_request_event(self, satellite_with_app, mock_event_manager):
  113. """Testet, dass speak() das tts_request Event auslöst."""
  114. result = await satellite_with_app.speak("Hallo Welt", voice="alloy")
  115. assert result is True
  116. mock_event_manager.trigger.assert_called_once()
  117. # Prüfe Event-Name
  118. call_args = mock_event_manager.trigger.call_args
  119. assert call_args[0][0] == "tts_request"
  120. # Prüfe Event-Daten
  121. event_data = call_args[0][1]
  122. assert event_data.text == "Hallo Welt"
  123. assert event_data.voice == "alloy"
  124. assert event_data.satellite_id == "test-sat-001"
  125. @pytest.mark.asyncio
  126. async def test_speak_returns_false_without_application(self):
  127. """Testet, dass speak() False zurückgibt ohne Application."""
  128. sat = Satellite(satellite_id="no-app")
  129. result = await sat.speak("Test")
  130. assert result is False
  131. @pytest.mark.asyncio
  132. async def test_speak_returns_false_when_cancelled(self, satellite_with_app, mock_event_manager):
  133. """Testet, dass speak() False zurückgibt wenn Event gecancelt."""
  134. async def cancel_event(event_name, data):
  135. data.cancel()
  136. mock_event_manager.trigger = cancel_event
  137. result = await satellite_with_app.speak("Test")
  138. assert result is False
  139. @pytest.mark.asyncio
  140. async def test_play_triggers_stream_start_event(self, satellite_with_app, mock_event_manager):
  141. """Testet, dass play() das stream_start Event auslöst."""
  142. result = await satellite_with_app.play("http://radio.mp3", volume=0.5)
  143. assert result is True
  144. mock_event_manager.trigger.assert_called_once()
  145. call_args = mock_event_manager.trigger.call_args
  146. assert call_args[0][0] == "stream_start"
  147. event_data = call_args[0][1]
  148. assert event_data.source == "http://radio.mp3"
  149. assert event_data.volume == 0.5
  150. assert event_data.stream_type == "music"
  151. @pytest.mark.asyncio
  152. async def test_play_returns_false_without_application(self):
  153. """Testet, dass play() False zurückgibt ohne Application."""
  154. sat = Satellite(satellite_id="no-app")
  155. result = await sat.play("http://test.mp3")
  156. assert result is False
  157. @pytest.mark.asyncio
  158. async def test_stop_playback_triggers_stream_stop_event(self, satellite_with_app, mock_event_manager):
  159. """Testet, dass stop_playback() das stream_stop Event auslöst."""
  160. result = await satellite_with_app.stop_playback()
  161. assert result is True
  162. mock_event_manager.trigger.assert_called_once()
  163. call_args = mock_event_manager.trigger.call_args
  164. assert call_args[0][0] == "stream_stop"
  165. event_data = call_args[0][1]
  166. assert event_data.satellite_id == "test-sat-001"
  167. assert event_data.stream_type == "all"
  168. # =============================================================================
  169. # Tests für Satellite Application-Referenz
  170. # =============================================================================
  171. class TestSatelliteApplicationReference:
  172. """Tests für Satellite Application-Referenz."""
  173. def test_satellite_with_application(self, mock_application):
  174. """Testet Satellite-Erstellung mit Application."""
  175. sat = Satellite(
  176. satellite_id="test",
  177. application=mock_application
  178. )
  179. assert sat.application is mock_application
  180. def test_satellite_application_setter(self, mock_application):
  181. """Testet Application-Setter."""
  182. sat = Satellite(satellite_id="test")
  183. assert sat.application is None
  184. sat.application = mock_application
  185. assert sat.application is mock_application
  186. def test_satellite_without_application(self):
  187. """Testet Satellite ohne Application."""
  188. sat = Satellite(satellite_id="test")
  189. assert sat.application is None
  190. # =============================================================================
  191. # Tests für SatelliteAPI Warte-Logik
  192. # =============================================================================
  193. class TestSatelliteAPIWaitLogic:
  194. """Tests für SatelliteAPI.say() Warte-Logik."""
  195. @pytest.fixture
  196. def mock_satellite_manager(self):
  197. """Erstellt einen Mock-SatelliteManager."""
  198. manager = MagicMock()
  199. connected_sat = MagicMock()
  200. connected_sat.id = "sat-001"
  201. connected_sat.is_connected = True
  202. connected_sat.room_id = "wohnzimmer"
  203. manager.get = MagicMock(return_value=connected_sat)
  204. manager.list_connected = MagicMock(return_value=[connected_sat])
  205. manager.get_by_room = MagicMock(return_value=[connected_sat])
  206. return manager
  207. @pytest.fixture
  208. def satellite_api(self, mock_application, mock_satellite_manager):
  209. """Erstellt eine SatelliteAPI-Instanz."""
  210. from trixy_core.satellite.api import SatelliteAPI
  211. mock_application.satellite_manager = mock_satellite_manager
  212. return SatelliteAPI(mock_application)
  213. @pytest.mark.asyncio
  214. async def test_say_with_wait_registers_handler(self, satellite_api, mock_event_manager):
  215. """Testet, dass say() mit wait=True einen Handler registriert."""
  216. # Simuliere sofortige Completion
  217. async def trigger_and_complete(event_name, data):
  218. if event_name == "tts_request":
  219. # Finde den registrierten Handler und rufe ihn auf
  220. for call in mock_event_manager.register.call_args_list:
  221. if call[0][0] == "tts_completed":
  222. handler = call[0][1]
  223. completed = TTSCompleted(
  224. request_id=data.request_id,
  225. success=True
  226. )
  227. await handler("tts_completed", completed)
  228. mock_event_manager.trigger = trigger_and_complete
  229. result = await satellite_api.say("Test", satellites=["sat-001"], wait=True)
  230. # Handler sollte registriert worden sein
  231. assert mock_event_manager.register.called
  232. # Handler sollte wieder entfernt worden sein
  233. assert mock_event_manager.unregister.called
  234. @pytest.mark.asyncio
  235. async def test_say_without_wait_does_not_wait(self, satellite_api, mock_event_manager):
  236. """Testet, dass say() mit wait=False nicht wartet."""
  237. result = await satellite_api.say("Test", satellites=["sat-001"], wait=False)
  238. assert result is True
  239. mock_event_manager.trigger.assert_called_once()
  240. # Kein Handler sollte registriert werden bei wait=False
  241. # (nur tts_request wird ausgelöst)
  242. @pytest.mark.asyncio
  243. async def test_say_returns_false_for_empty_targets(self, satellite_api, mock_satellite_manager):
  244. """Testet, dass say() False zurückgibt wenn keine Ziele."""
  245. mock_satellite_manager.get_by_room = MagicMock(return_value=[])
  246. mock_satellite_manager.list_connected = MagicMock(return_value=[])
  247. result = await satellite_api.say("Test", room="leerer_raum")
  248. assert result is False
  249. # =============================================================================
  250. # Tests für WakewordService Verarbeitungs-Warte-Logik
  251. # =============================================================================
  252. class TestWakewordServiceProcessingWait:
  253. """Tests für WakewordService._wait_for_processing_result()."""
  254. @pytest.fixture
  255. def mock_wakeword_service(self, mock_application):
  256. """Erstellt einen Mock-WakewordService mittels MagicMock."""
  257. from trixy_core.wakeword.service import (
  258. WakewordServiceConfig,
  259. RecordingSession,
  260. ServiceState,
  261. WakewordService,
  262. )
  263. from trixy_core.wakeword.detector import WakewordDetection, WakewordType
  264. from datetime import datetime
  265. import logging
  266. # Verwende MagicMock statt echter Klasse
  267. service = MagicMock(spec=WakewordService)
  268. service._application = mock_application
  269. service._config = WakewordServiceConfig(
  270. standalone_mode=True,
  271. follow_up_timeout_seconds=0.1 # Kurzer Timeout für Tests
  272. )
  273. service.logger = logging.getLogger("test")
  274. # Simuliere aktive Session
  275. service._current_session = RecordingSession(
  276. session_id="test-session-123",
  277. wakeword_detection=WakewordDetection(
  278. wakeword_type=WakewordType.CUSTOM,
  279. model_name="test",
  280. confidence=0.95,
  281. audio_level=0.7,
  282. timestamp=datetime.now()
  283. ),
  284. start_time=datetime.now()
  285. )
  286. # Binde die echte Methode an den Mock
  287. from trixy_core.wakeword.service import WakewordService as RealService
  288. service._wait_for_processing_result = lambda: RealService._wait_for_processing_result(service)
  289. return service
  290. @pytest.mark.asyncio
  291. async def test_wait_for_processing_result_with_immediate_response(
  292. self, mock_wakeword_service, mock_event_manager
  293. ):
  294. """Testet _wait_for_processing_result mit sofortiger Antwort."""
  295. # Speichere den registrierten Handler um ihn später aufzurufen
  296. registered_handler = None
  297. def capture_register(event_name, handler):
  298. nonlocal registered_handler
  299. if event_name == "processing_result":
  300. registered_handler = handler
  301. mock_event_manager.register = capture_register
  302. # Starte den Wait in einem Task
  303. async def wait_and_respond():
  304. # Kurz warten damit der Handler registriert wird
  305. await asyncio.sleep(0.01)
  306. if registered_handler:
  307. result = ProcessingResult(
  308. session_id="test-session-123",
  309. success=True,
  310. intent="test.intent"
  311. )
  312. await registered_handler("processing_result", result)
  313. # Starte Response-Task
  314. response_task = asyncio.create_task(wait_and_respond())
  315. # Führe wait aus
  316. await mock_wakeword_service._wait_for_processing_result()
  317. # Warte auf Response-Task
  318. await response_task
  319. # Handler sollte registriert worden sein
  320. assert registered_handler is not None
  321. @pytest.mark.asyncio
  322. async def test_wait_for_processing_result_timeout(
  323. self, mock_wakeword_service, mock_event_manager
  324. ):
  325. """Testet Timeout-Handling."""
  326. # Registriere Handler aber triggere ihn nicht
  327. mock_event_manager.register = MagicMock()
  328. # Sollte durch Timeout beendet werden (0.1s Timeout)
  329. await mock_wakeword_service._wait_for_processing_result()
  330. # Handler sollte wieder entfernt werden (im finally-Block)
  331. assert mock_event_manager.unregister.called
  332. @pytest.mark.asyncio
  333. async def test_wait_for_processing_result_without_session(
  334. self, mock_wakeword_service, mock_event_manager
  335. ):
  336. """Testet Verhalten ohne aktive Session."""
  337. mock_wakeword_service._current_session = None
  338. # Sollte sofort zurückkehren
  339. await mock_wakeword_service._wait_for_processing_result()
  340. # Kein Handler sollte registriert werden
  341. assert not mock_event_manager.register.called
  342. @pytest.mark.asyncio
  343. async def test_wait_for_processing_result_without_event_manager(
  344. self, mock_wakeword_service
  345. ):
  346. """Testet Verhalten ohne EventManager."""
  347. mock_wakeword_service._application.events = None
  348. # Sollte ohne Fehler zurückkehren
  349. await mock_wakeword_service._wait_for_processing_result()
  350. # =============================================================================
  351. # Tests für SatelliteManager Application-Referenz
  352. # =============================================================================
  353. class TestSatelliteManagerApplicationRef:
  354. """Tests für SatelliteManager Application-Referenz Weitergabe."""
  355. def test_add_sets_application_reference(self, mock_application):
  356. """Testet, dass add() die Application-Referenz setzt."""
  357. from trixy_core.satellite.satellite_manager import SatelliteManager
  358. manager = SatelliteManager(mock_application)
  359. sat = Satellite(satellite_id="test-sat")
  360. assert sat.application is None
  361. manager.add(sat)
  362. assert sat.application is mock_application
  363. # =============================================================================
  364. # Integration Tests
  365. # =============================================================================
  366. class TestFullIntegration:
  367. """Vollständige Integrationstests."""
  368. @pytest.mark.asyncio
  369. async def test_satellite_speak_full_flow(self):
  370. """Testet den vollständigen speak()-Flow."""
  371. from trixy_core.events.eventmanager import EventManager
  372. # Mock Application für EventManager
  373. app = MagicMock()
  374. # Echten EventManager verwenden
  375. event_manager = EventManager(app)
  376. app.events = event_manager
  377. # Satellite erstellen
  378. sat = Satellite(
  379. satellite_id="integration-test",
  380. application=app
  381. )
  382. # TTS-Handler registrieren
  383. tts_requests = []
  384. async def on_tts_request(event_name, data):
  385. tts_requests.append(data)
  386. event_manager.register("tts_request", on_tts_request)
  387. # speak() aufrufen
  388. result = await sat.speak("Integration Test", voice="nova")
  389. assert result is True
  390. assert len(tts_requests) == 1
  391. assert tts_requests[0].text == "Integration Test"
  392. assert tts_requests[0].voice == "nova"
  393. @pytest.mark.asyncio
  394. async def test_satellite_play_full_flow(self):
  395. """Testet den vollständigen play()-Flow."""
  396. from trixy_core.events.eventmanager import EventManager
  397. app = MagicMock()
  398. event_manager = EventManager(app)
  399. app.events = event_manager
  400. sat = Satellite(satellite_id="play-test", application=app)
  401. stream_starts = []
  402. async def on_stream_start(event_name, data):
  403. stream_starts.append(data)
  404. event_manager.register("stream_start", on_stream_start)
  405. result = await sat.play("http://example.com/music.mp3", volume=0.8)
  406. assert result is True
  407. assert len(stream_starts) == 1
  408. assert stream_starts[0].source == "http://example.com/music.mp3"
  409. assert stream_starts[0].volume == 0.8