test_keyboard_input.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Keyboard Input Service.
  4. Testet:
  5. - KeyboardInputService Initialisierung und Lifecycle
  6. - SimpleKeyboardInput Fallback
  7. - create_keyboard_input Factory
  8. - Conversation State Management
  9. - Follow-Up Handling
  10. """
  11. import asyncio
  12. import pytest
  13. from unittest.mock import MagicMock, AsyncMock, patch
  14. from trixy_core.input.keyboard_input import (
  15. KeyboardInputService,
  16. SimpleKeyboardInput,
  17. create_keyboard_input,
  18. _check_prompt_toolkit,
  19. )
  20. from trixy_core.network.cmd import FollowUpRequest, TextInput
  21. # =============================================================================
  22. # Fixtures
  23. # =============================================================================
  24. @pytest.fixture
  25. def mock_app():
  26. """Mock-Application mit Events und Properties."""
  27. app = MagicMock()
  28. app.room = "test_room"
  29. app.alias = "test_alias"
  30. app.shutdown = MagicMock()
  31. # Events Mock
  32. app.events = MagicMock()
  33. app.events.trigger = AsyncMock()
  34. return app
  35. @pytest.fixture
  36. def mock_connection():
  37. """Mock-ClientConnection."""
  38. conn = MagicMock()
  39. conn.is_connected = True
  40. conn.send_command = AsyncMock(return_value=True)
  41. return conn
  42. @pytest.fixture
  43. def keyboard_service(mock_app):
  44. """Erstellt einen KeyboardInputService ohne Connection."""
  45. return KeyboardInputService(mock_app, connection=None, prompt=">>> ")
  46. @pytest.fixture
  47. def keyboard_service_with_connection(mock_app, mock_connection):
  48. """Erstellt einen KeyboardInputService mit Connection."""
  49. return KeyboardInputService(mock_app, connection=mock_connection, prompt=">>> ")
  50. @pytest.fixture
  51. def simple_input(mock_app):
  52. """Erstellt einen SimpleKeyboardInput."""
  53. return SimpleKeyboardInput(mock_app, connection=None, prompt=">>> ")
  54. @pytest.fixture
  55. def simple_input_with_connection(mock_app, mock_connection):
  56. """Erstellt einen SimpleKeyboardInput mit Connection."""
  57. return SimpleKeyboardInput(mock_app, connection=mock_connection, prompt=">>> ")
  58. # =============================================================================
  59. # Tests für _check_prompt_toolkit
  60. # =============================================================================
  61. class TestCheckPromptToolkit:
  62. """Tests für die prompt_toolkit Verfügbarkeitsprüfung."""
  63. def test_returns_bool(self):
  64. """Testet, dass ein Boolean zurückgegeben wird."""
  65. result = _check_prompt_toolkit()
  66. assert isinstance(result, bool)
  67. def test_result_is_cached(self):
  68. """Testet, dass das Ergebnis gecacht wird."""
  69. result1 = _check_prompt_toolkit()
  70. result2 = _check_prompt_toolkit()
  71. assert result1 == result2
  72. # =============================================================================
  73. # Tests für KeyboardInputService - Initialisierung
  74. # =============================================================================
  75. class TestKeyboardInputServiceInit:
  76. """Tests für KeyboardInputService Initialisierung."""
  77. def test_initial_state(self, keyboard_service):
  78. """Testet den Initialzustand."""
  79. assert keyboard_service.is_running is False
  80. assert keyboard_service.has_active_session is False
  81. assert keyboard_service._input_active is False
  82. def test_initial_prompt(self, keyboard_service):
  83. """Testet den initialen Prompt."""
  84. assert keyboard_service._prompt == ">>> "
  85. def test_custom_prompt(self, mock_app):
  86. """Testet benutzerdefinierten Prompt."""
  87. service = KeyboardInputService(mock_app, prompt="Test> ")
  88. assert service._prompt == "Test> "
  89. def test_no_connection_initially(self, keyboard_service):
  90. """Testet, dass keine Connection initial gesetzt ist."""
  91. assert keyboard_service._connection is None
  92. def test_with_connection(self, keyboard_service_with_connection, mock_connection):
  93. """Testet Service mit Connection."""
  94. assert keyboard_service_with_connection._connection is mock_connection
  95. # =============================================================================
  96. # Tests für KeyboardInputService - Properties
  97. # =============================================================================
  98. class TestKeyboardInputServiceProperties:
  99. """Tests für KeyboardInputService Properties."""
  100. def test_is_running_false_initially(self, keyboard_service):
  101. """Testet is_running initial."""
  102. assert keyboard_service.is_running is False
  103. def test_has_active_session_false_initially(self, keyboard_service):
  104. """Testet has_active_session initial."""
  105. assert keyboard_service.has_active_session is False
  106. def test_has_active_session_true_with_session(self, keyboard_service):
  107. """Testet has_active_session mit Session."""
  108. keyboard_service._current_session_id = "test-session-123"
  109. assert keyboard_service.has_active_session is True
  110. def test_has_active_session_false_with_empty_session(self, keyboard_service):
  111. """Testet has_active_session mit leerer Session."""
  112. keyboard_service._current_session_id = ""
  113. assert keyboard_service.has_active_session is False
  114. # =============================================================================
  115. # Tests für KeyboardInputService - set_connection
  116. # =============================================================================
  117. class TestKeyboardInputServiceSetConnection:
  118. """Tests für set_connection Methode."""
  119. def test_set_connection(self, keyboard_service, mock_connection):
  120. """Testet Setzen der Connection."""
  121. assert keyboard_service._connection is None
  122. keyboard_service.set_connection(mock_connection)
  123. assert keyboard_service._connection is mock_connection
  124. # =============================================================================
  125. # Tests für KeyboardInputService - Conversation State
  126. # =============================================================================
  127. class TestKeyboardInputServiceConversationState:
  128. """Tests für Conversation State Management."""
  129. @pytest.mark.asyncio
  130. async def test_request_input_new_conversation(self, keyboard_service):
  131. """Testet request_input für neue Conversation."""
  132. await keyboard_service.request_input()
  133. assert keyboard_service._awaiting_follow_up is False
  134. assert keyboard_service._follow_up_question == ""
  135. assert keyboard_service._follow_up_number == 0
  136. assert keyboard_service._current_session_id == ""
  137. @pytest.mark.asyncio
  138. async def test_request_input_follow_up(self, keyboard_service):
  139. """Testet request_input für Follow-Up."""
  140. await keyboard_service.request_input(
  141. follow_up_question="Was meinst du damit?",
  142. session_id="session-123"
  143. )
  144. assert keyboard_service._awaiting_follow_up is True
  145. assert keyboard_service._follow_up_question == "Was meinst du damit?"
  146. assert keyboard_service._follow_up_number == 1
  147. assert keyboard_service._current_session_id == "session-123"
  148. @pytest.mark.asyncio
  149. async def test_request_input_multiple_follow_ups(self, keyboard_service):
  150. """Testet mehrere Follow-Ups hintereinander."""
  151. await keyboard_service.request_input(
  152. follow_up_question="Frage 1?",
  153. session_id="session-123"
  154. )
  155. assert keyboard_service._follow_up_number == 1
  156. await keyboard_service.request_input(
  157. follow_up_question="Frage 2?",
  158. session_id="session-123"
  159. )
  160. assert keyboard_service._follow_up_number == 2
  161. @pytest.mark.asyncio
  162. async def test_handle_follow_up_request(self, keyboard_service):
  163. """Testet handle_follow_up_request."""
  164. request = FollowUpRequest(
  165. session_id="session-456",
  166. question="Kannst du das genauer erklären?"
  167. )
  168. # Muss als Task erstellt werden (async)
  169. keyboard_service.handle_follow_up_request(request)
  170. # Kurz warten damit Task ausgeführt wird
  171. await asyncio.sleep(0.01)
  172. assert keyboard_service._awaiting_follow_up is True
  173. assert keyboard_service._follow_up_question == "Kannst du das genauer erklären?"
  174. assert keyboard_service._current_session_id == "session-456"
  175. def test_handle_conversation_end_matching_session(self, keyboard_service):
  176. """Testet handle_conversation_end mit passender Session."""
  177. keyboard_service._current_session_id = "session-789"
  178. keyboard_service._awaiting_follow_up = True
  179. keyboard_service._follow_up_question = "Test?"
  180. keyboard_service._follow_up_number = 3
  181. keyboard_service.handle_conversation_end("session-789")
  182. assert keyboard_service._current_session_id == ""
  183. assert keyboard_service._awaiting_follow_up is False
  184. assert keyboard_service._follow_up_question == ""
  185. assert keyboard_service._follow_up_number == 0
  186. def test_handle_conversation_end_different_session(self, keyboard_service):
  187. """Testet handle_conversation_end mit anderer Session."""
  188. keyboard_service._current_session_id = "session-789"
  189. keyboard_service._awaiting_follow_up = True
  190. keyboard_service._follow_up_number = 3
  191. keyboard_service.handle_conversation_end("session-other")
  192. # State sollte unverändert sein
  193. assert keyboard_service._current_session_id == "session-789"
  194. assert keyboard_service._awaiting_follow_up is True
  195. assert keyboard_service._follow_up_number == 3
  196. # =============================================================================
  197. # Tests für KeyboardInputService - _process_input
  198. # =============================================================================
  199. class TestKeyboardInputServiceProcessInput:
  200. """Tests für _process_input Methode."""
  201. @pytest.mark.asyncio
  202. async def test_process_input_standalone_mode(self, keyboard_service, mock_app):
  203. """Testet Input-Verarbeitung im Standalone-Modus (keine Connection)."""
  204. await keyboard_service._process_input("Hallo Trixy")
  205. # Event sollte ausgelöst worden sein
  206. mock_app.events.trigger.assert_called_once()
  207. # Prüfe Event-Parameter
  208. call_args = mock_app.events.trigger.call_args
  209. assert call_args[0][0] == "speech_recognized"
  210. event_data = call_args[0][1]
  211. assert event_data.text == "Hallo Trixy"
  212. assert event_data.source == "keyboard"
  213. assert event_data.confidence == 1.0
  214. @pytest.mark.asyncio
  215. async def test_process_input_client_mode(self, keyboard_service_with_connection, mock_connection):
  216. """Testet Input-Verarbeitung im Client-Modus (mit Connection)."""
  217. await keyboard_service_with_connection._process_input("Test Input")
  218. # Command sollte gesendet worden sein
  219. mock_connection.send_command.assert_called_once()
  220. # Prüfe Command-Parameter
  221. call_args = mock_connection.send_command.call_args
  222. cmd = call_args[0][0]
  223. assert isinstance(cmd, TextInput)
  224. assert cmd.text == "Test Input"
  225. assert cmd.satellite_id == "test_alias"
  226. assert cmd.room_id == "test_room"
  227. assert cmd.language == "de"
  228. @pytest.mark.asyncio
  229. async def test_process_input_generates_session_id(self, keyboard_service):
  230. """Testet, dass Session-ID generiert wird."""
  231. assert keyboard_service._current_session_id == ""
  232. await keyboard_service._process_input("Test")
  233. assert keyboard_service._current_session_id != ""
  234. assert keyboard_service._current_session_id.startswith("kb-")
  235. @pytest.mark.asyncio
  236. async def test_process_input_preserves_session_id(self, keyboard_service):
  237. """Testet, dass existierende Session-ID beibehalten wird."""
  238. keyboard_service._current_session_id = "existing-session"
  239. await keyboard_service._process_input("Test")
  240. assert keyboard_service._current_session_id == "existing-session"
  241. @pytest.mark.asyncio
  242. async def test_process_input_resets_follow_up(self, keyboard_service):
  243. """Testet, dass Follow-Up-State zurückgesetzt wird."""
  244. keyboard_service._awaiting_follow_up = True
  245. keyboard_service._follow_up_question = "Test?"
  246. await keyboard_service._process_input("Antwort")
  247. assert keyboard_service._awaiting_follow_up is False
  248. assert keyboard_service._follow_up_question == ""
  249. @pytest.mark.asyncio
  250. async def test_process_input_follow_up_flag(self, keyboard_service_with_connection, mock_connection):
  251. """Testet, dass Follow-Up-Flag im Command gesetzt wird."""
  252. keyboard_service_with_connection._awaiting_follow_up = True
  253. keyboard_service_with_connection._follow_up_number = 2
  254. keyboard_service_with_connection._current_session_id = "session-123"
  255. await keyboard_service_with_connection._process_input("Follow-Up Antwort")
  256. call_args = mock_connection.send_command.call_args
  257. cmd = call_args[0][0]
  258. assert cmd.is_follow_up is True
  259. assert cmd.follow_up_number == 2
  260. # =============================================================================
  261. # Tests für SimpleKeyboardInput - Initialisierung
  262. # =============================================================================
  263. class TestSimpleKeyboardInputInit:
  264. """Tests für SimpleKeyboardInput Initialisierung."""
  265. def test_initial_state(self, simple_input):
  266. """Testet den Initialzustand."""
  267. assert simple_input._running is False
  268. assert simple_input._task is None
  269. def test_initial_prompt(self, simple_input):
  270. """Testet den initialen Prompt."""
  271. assert simple_input._prompt == ">>> "
  272. def test_custom_prompt(self, mock_app):
  273. """Testet benutzerdefinierten Prompt."""
  274. service = SimpleKeyboardInput(mock_app, prompt="Simple> ")
  275. assert service._prompt == "Simple> "
  276. def test_conversation_state_initial(self, simple_input):
  277. """Testet initialen Conversation State."""
  278. assert simple_input._current_session_id == ""
  279. assert simple_input._awaiting_follow_up is False
  280. assert simple_input._follow_up_number == 0
  281. assert simple_input._follow_up_question == ""
  282. # =============================================================================
  283. # Tests für SimpleKeyboardInput - set_connection
  284. # =============================================================================
  285. class TestSimpleKeyboardInputSetConnection:
  286. """Tests für SimpleKeyboardInput set_connection."""
  287. def test_set_connection(self, simple_input, mock_connection):
  288. """Testet Setzen der Connection."""
  289. assert simple_input._connection is None
  290. simple_input.set_connection(mock_connection)
  291. assert simple_input._connection is mock_connection
  292. # =============================================================================
  293. # Tests für SimpleKeyboardInput - Lifecycle
  294. # =============================================================================
  295. class TestSimpleKeyboardInputLifecycle:
  296. """Tests für SimpleKeyboardInput Lifecycle."""
  297. @pytest.mark.asyncio
  298. async def test_start_sets_running(self, simple_input):
  299. """Testet, dass start() _running setzt."""
  300. # Patch input() um blocking zu vermeiden
  301. with patch('builtins.input', return_value=""):
  302. result = await simple_input.start()
  303. assert result is True
  304. assert simple_input._running is True
  305. assert simple_input._task is not None
  306. # Cleanup
  307. await simple_input.stop()
  308. @pytest.mark.asyncio
  309. async def test_start_already_running(self, simple_input):
  310. """Testet start() wenn bereits läuft."""
  311. simple_input._running = True
  312. result = await simple_input.start()
  313. assert result is True
  314. @pytest.mark.asyncio
  315. async def test_stop_sets_not_running(self, simple_input):
  316. """Testet, dass stop() _running zurücksetzt."""
  317. simple_input._running = True
  318. simple_input._task = asyncio.create_task(asyncio.sleep(10))
  319. await simple_input.stop()
  320. assert simple_input._running is False
  321. # =============================================================================
  322. # Tests für SimpleKeyboardInput - Conversation State
  323. # =============================================================================
  324. class TestSimpleKeyboardInputConversationState:
  325. """Tests für SimpleKeyboardInput Conversation State."""
  326. @pytest.mark.asyncio
  327. async def test_request_input_follow_up(self, simple_input):
  328. """Testet request_input für Follow-Up."""
  329. await simple_input.request_input(
  330. follow_up_question="Follow-Up Frage?",
  331. session_id="session-abc"
  332. )
  333. assert simple_input._awaiting_follow_up is True
  334. assert simple_input._follow_up_question == "Follow-Up Frage?"
  335. assert simple_input._follow_up_number == 1
  336. assert simple_input._current_session_id == "session-abc"
  337. def test_handle_conversation_end(self, simple_input):
  338. """Testet handle_conversation_end."""
  339. simple_input._current_session_id = "session-end"
  340. simple_input._awaiting_follow_up = True
  341. simple_input._follow_up_number = 5
  342. simple_input.handle_conversation_end("session-end")
  343. assert simple_input._current_session_id == ""
  344. assert simple_input._awaiting_follow_up is False
  345. assert simple_input._follow_up_number == 0
  346. # =============================================================================
  347. # Tests für SimpleKeyboardInput - _process_input
  348. # =============================================================================
  349. class TestSimpleKeyboardInputProcessInput:
  350. """Tests für SimpleKeyboardInput _process_input."""
  351. @pytest.mark.asyncio
  352. async def test_process_input_standalone(self, simple_input, mock_app):
  353. """Testet Input-Verarbeitung ohne Connection."""
  354. await simple_input._process_input("Simple Test")
  355. mock_app.events.trigger.assert_called_once()
  356. call_args = mock_app.events.trigger.call_args
  357. assert call_args[0][0] == "speech_recognized"
  358. event_data = call_args[0][1]
  359. assert event_data.text == "Simple Test"
  360. assert event_data.source == "keyboard"
  361. @pytest.mark.asyncio
  362. async def test_process_input_with_connection(self, simple_input_with_connection, mock_connection):
  363. """Testet Input-Verarbeitung mit Connection."""
  364. await simple_input_with_connection._process_input("Connected Test")
  365. mock_connection.send_command.assert_called_once()
  366. call_args = mock_connection.send_command.call_args
  367. cmd = call_args[0][0]
  368. assert isinstance(cmd, TextInput)
  369. assert cmd.text == "Connected Test"
  370. @pytest.mark.asyncio
  371. async def test_process_input_resets_follow_up(self, simple_input):
  372. """Testet, dass Follow-Up zurückgesetzt wird."""
  373. simple_input._awaiting_follow_up = True
  374. simple_input._follow_up_question = "Test?"
  375. await simple_input._process_input("Antwort")
  376. assert simple_input._awaiting_follow_up is False
  377. assert simple_input._follow_up_question == ""
  378. # =============================================================================
  379. # Tests für create_keyboard_input Factory
  380. # =============================================================================
  381. class TestCreateKeyboardInput:
  382. """Tests für create_keyboard_input Factory-Funktion."""
  383. def test_returns_keyboard_input_service_or_simple(self, mock_app):
  384. """Testet, dass die Factory die richtige Klasse zurückgibt."""
  385. result = create_keyboard_input(mock_app)
  386. # Je nach prompt_toolkit Verfügbarkeit
  387. assert isinstance(result, (KeyboardInputService, SimpleKeyboardInput))
  388. def test_with_connection(self, mock_app, mock_connection):
  389. """Testet Factory mit Connection."""
  390. result = create_keyboard_input(mock_app, connection=mock_connection)
  391. assert result._connection is mock_connection
  392. def test_with_custom_prompt(self, mock_app):
  393. """Testet Factory mit benutzerdefiniertem Prompt."""
  394. result = create_keyboard_input(mock_app, prompt="Custom> ")
  395. assert result._prompt == "Custom> "
  396. def test_returns_simple_when_no_prompt_toolkit(self, mock_app):
  397. """Testet, dass SimpleKeyboardInput zurückgegeben wird ohne prompt_toolkit."""
  398. with patch('trixy_core.input.keyboard_input._check_prompt_toolkit', return_value=False):
  399. # Importiere erneut um den Patch zu nutzen
  400. from trixy_core.input.keyboard_input import create_keyboard_input as factory
  401. result = factory(mock_app)
  402. assert isinstance(result, SimpleKeyboardInput)
  403. def test_returns_service_when_prompt_toolkit_available(self, mock_app):
  404. """Testet, dass KeyboardInputService zurückgegeben wird mit prompt_toolkit."""
  405. with patch('trixy_core.input.keyboard_input._check_prompt_toolkit', return_value=True):
  406. from trixy_core.input.keyboard_input import create_keyboard_input as factory
  407. result = factory(mock_app)
  408. assert isinstance(result, KeyboardInputService)
  409. # =============================================================================
  410. # Tests für TextInput Command
  411. # =============================================================================
  412. class TestTextInputCommand:
  413. """Tests für TextInput Command Erstellung."""
  414. @pytest.mark.asyncio
  415. async def test_text_input_command_fields(self, keyboard_service_with_connection, mock_connection):
  416. """Testet, dass alle TextInput Felder korrekt gesetzt werden."""
  417. keyboard_service_with_connection._current_session_id = "test-session"
  418. keyboard_service_with_connection._awaiting_follow_up = True
  419. keyboard_service_with_connection._follow_up_number = 3
  420. await keyboard_service_with_connection._process_input("Test Text")
  421. call_args = mock_connection.send_command.call_args
  422. cmd = call_args[0][0]
  423. assert cmd.satellite_id == "test_alias"
  424. assert cmd.text == "Test Text"
  425. assert cmd.session_id == "test-session"
  426. assert cmd.room_id == "test_room"
  427. assert cmd.language == "de"
  428. assert cmd.is_follow_up is True
  429. assert cmd.follow_up_number == 3
  430. # =============================================================================
  431. # Integration Tests
  432. # =============================================================================
  433. class TestKeyboardInputIntegration:
  434. """Integrationstests für Keyboard Input."""
  435. @pytest.mark.asyncio
  436. async def test_full_conversation_flow_standalone(self, keyboard_service, mock_app):
  437. """Testet vollständigen Conversation-Flow im Standalone-Modus."""
  438. # Erste Eingabe
  439. await keyboard_service._process_input("Erste Nachricht")
  440. assert keyboard_service._current_session_id.startswith("kb-")
  441. session_id = keyboard_service._current_session_id
  442. # Follow-Up Request
  443. await keyboard_service.request_input(
  444. follow_up_question="Kannst du mehr sagen?",
  445. session_id=session_id
  446. )
  447. assert keyboard_service._awaiting_follow_up is True
  448. # Follow-Up Antwort
  449. await keyboard_service._process_input("Mehr Details")
  450. assert keyboard_service._awaiting_follow_up is False
  451. # Conversation Ende
  452. keyboard_service.handle_conversation_end(session_id)
  453. assert keyboard_service._current_session_id == ""
  454. @pytest.mark.asyncio
  455. async def test_full_conversation_flow_client(self, keyboard_service_with_connection, mock_connection):
  456. """Testet vollständigen Conversation-Flow im Client-Modus."""
  457. # Erste Eingabe
  458. await keyboard_service_with_connection._process_input("Client Nachricht")
  459. # Prüfe ersten Command
  460. first_call = mock_connection.send_command.call_args_list[0]
  461. first_cmd = first_call[0][0]
  462. assert first_cmd.is_follow_up is False
  463. session_id = keyboard_service_with_connection._current_session_id
  464. # Follow-Up Request
  465. await keyboard_service_with_connection.request_input(
  466. follow_up_question="Rückfrage?",
  467. session_id=session_id
  468. )
  469. # Follow-Up Antwort
  470. await keyboard_service_with_connection._process_input("Follow-Up Antwort")
  471. # Prüfe zweiten Command
  472. second_call = mock_connection.send_command.call_args_list[1]
  473. second_cmd = second_call[0][0]
  474. assert second_cmd.is_follow_up is True
  475. @pytest.mark.asyncio
  476. async def test_multiple_sessions(self, keyboard_service, mock_app):
  477. """Testet mehrere unabhängige Sessions."""
  478. # Erste Session
  479. await keyboard_service._process_input("Session 1")
  480. session1 = keyboard_service._current_session_id
  481. keyboard_service.handle_conversation_end(session1)
  482. # Zweite Session
  483. await keyboard_service._process_input("Session 2")
  484. session2 = keyboard_service._current_session_id
  485. # Sessions sollten unterschiedlich sein
  486. assert session1 != session2
  487. assert session2.startswith("kb-")
  488. # =============================================================================
  489. # Edge Cases
  490. # =============================================================================
  491. class TestEdgeCases:
  492. """Tests für Grenzfälle."""
  493. def test_handle_conversation_end_empty_session(self, keyboard_service):
  494. """Testet handle_conversation_end mit leerer Session."""
  495. keyboard_service._current_session_id = ""
  496. keyboard_service.handle_conversation_end("")
  497. # Sollte keinen Fehler werfen
  498. def test_handle_conversation_end_none_like(self, keyboard_service):
  499. """Testet handle_conversation_end mit None-ähnlichem Wert."""
  500. keyboard_service._current_session_id = "session-123"
  501. keyboard_service.handle_conversation_end("")
  502. # Session sollte unverändert sein
  503. assert keyboard_service._current_session_id == "session-123"
  504. @pytest.mark.asyncio
  505. async def test_process_input_missing_app_attributes(self, mock_app):
  506. """Testet _process_input wenn App-Attribute fehlen."""
  507. # Entferne Attribute
  508. del mock_app.room
  509. del mock_app.alias
  510. service = KeyboardInputService(mock_app)
  511. # Sollte Defaults verwenden
  512. await service._process_input("Test")
  513. call_args = mock_app.events.trigger.call_args
  514. event_data = call_args[0][1]
  515. assert event_data.room_id == "local"
  516. assert event_data.satellite_id == "keyboard"
  517. @pytest.mark.asyncio
  518. async def test_connection_disconnected(self, keyboard_service_with_connection, mock_connection, mock_app):
  519. """Testet Verhalten bei getrennter Connection."""
  520. mock_connection.is_connected = False
  521. await keyboard_service_with_connection._process_input("Offline Test")
  522. # Sollte auf Events zurückfallen
  523. mock_app.events.trigger.assert_called_once()
  524. mock_connection.send_command.assert_not_called()
  525. @pytest.mark.asyncio
  526. async def test_follow_up_request_with_empty_question(self, keyboard_service):
  527. """Testet handle_follow_up_request mit leerer Frage."""
  528. request = FollowUpRequest(session_id="session-123", question="")
  529. keyboard_service.handle_follow_up_request(request)
  530. await asyncio.sleep(0.01)
  531. # Sollte keinen Fehler werfen