test_conversation.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Conversation Module.
  4. """
  5. import pytest
  6. import asyncio
  7. from datetime import datetime, timedelta
  8. from unittest.mock import MagicMock, AsyncMock, patch
  9. from trixy_core.conversation.session import (
  10. ConversationSession,
  11. SessionConfig,
  12. SessionState,
  13. SessionTurn,
  14. TurnType,
  15. )
  16. from trixy_core.conversation.context import (
  17. ConversationContext,
  18. ContextVariable,
  19. ContextScope,
  20. )
  21. class TestSessionConfig:
  22. """Tests für SessionConfig."""
  23. def test_default_config(self):
  24. """Testet Standard-Konfiguration."""
  25. config = SessionConfig()
  26. assert config.input_timeout_seconds == 60.0
  27. assert config.follow_up_timeout_seconds == 60.0
  28. assert config.max_turns == 20
  29. assert config.max_follow_ups == 5
  30. class TestSessionState:
  31. """Tests für SessionState."""
  32. def test_states(self):
  33. """Testet Session-Zustände."""
  34. assert SessionState.CREATED.value == "created"
  35. assert SessionState.AWAITING_INPUT.value == "awaiting"
  36. assert SessionState.PROCESSING.value == "processing"
  37. assert SessionState.FOLLOW_UP.value == "follow_up"
  38. assert SessionState.COMPLETED.value == "completed"
  39. class TestTurnType:
  40. """Tests für TurnType."""
  41. def test_types(self):
  42. """Testet Turn-Typen."""
  43. assert TurnType.USER_SPEECH.value == "user_speech"
  44. assert TurnType.USER_TEXT.value == "user_text"
  45. assert TurnType.ASSISTANT_SPEECH.value == "assistant_speech"
  46. assert TurnType.FOLLOW_UP.value == "follow_up"
  47. class TestSessionTurn:
  48. """Tests für SessionTurn."""
  49. def test_turn_creation(self):
  50. """Testet Turn-Erstellung."""
  51. turn = SessionTurn(
  52. turn_id="t1",
  53. turn_type=TurnType.USER_SPEECH,
  54. timestamp=datetime.now(),
  55. content="Hallo Trixy",
  56. confidence=0.95,
  57. )
  58. assert turn.content == "Hallo Trixy"
  59. assert turn.confidence == 0.95
  60. def test_turn_to_dict(self):
  61. """Testet Dictionary-Konvertierung."""
  62. turn = SessionTurn(
  63. turn_id="t1",
  64. turn_type=TurnType.ASSISTANT_SPEECH,
  65. timestamp=datetime.now(),
  66. content="Hallo!",
  67. intent="greeting",
  68. )
  69. d = turn.to_dict()
  70. assert d["turn_id"] == "t1"
  71. assert d["turn_type"] == "assistant_speech"
  72. assert d["intent"] == "greeting"
  73. class TestConversationSession:
  74. """Tests für ConversationSession."""
  75. @pytest.fixture
  76. def config(self):
  77. """Erstellt Test-Konfiguration."""
  78. return SessionConfig(
  79. input_timeout_seconds=10.0,
  80. max_turns=5,
  81. max_follow_ups=2,
  82. )
  83. @pytest.fixture
  84. def session(self, config):
  85. """Erstellt Session."""
  86. return ConversationSession(
  87. session_id="test-123",
  88. satellite_id="sat-001",
  89. wakeword_type="custom",
  90. config=config,
  91. )
  92. def test_session_creation(self, session):
  93. """Testet Session-Erstellung."""
  94. assert session.session_id == "test-123"
  95. assert session.satellite_id == "sat-001"
  96. assert session.state == SessionState.CREATED
  97. assert not session.is_completed
  98. def test_session_start(self, session):
  99. """Testet Session-Start."""
  100. session.start()
  101. assert session.state == SessionState.AWAITING_INPUT
  102. assert session.started_at is not None
  103. def test_add_user_turn(self, session):
  104. """Testet User-Turn hinzufügen."""
  105. turn = session.add_user_turn(
  106. content="Wie ist das Wetter?",
  107. intent="weather_query",
  108. )
  109. assert session.turn_count == 1
  110. assert turn.content == "Wie ist das Wetter?"
  111. assert session.state == SessionState.PROCESSING
  112. def test_add_assistant_turn(self, session):
  113. """Testet Assistenten-Turn hinzufügen."""
  114. turn = session.add_assistant_turn(
  115. content="Das Wetter ist schön.",
  116. )
  117. assert session.turn_count == 1
  118. assert session.state == SessionState.RESPONDING
  119. def test_add_follow_up(self, session):
  120. """Testet Rückfrage hinzufügen."""
  121. session.add_user_turn("Mach das Licht an")
  122. turn = session.add_assistant_turn(
  123. content="Welches Licht meinst du?",
  124. is_follow_up=True,
  125. )
  126. assert turn.requires_response is True
  127. assert session.follow_up_count == 1
  128. assert session.has_pending_follow_up
  129. assert session.state == SessionState.FOLLOW_UP
  130. def test_answer_follow_up(self, session):
  131. """Testet Antwort auf Rückfrage."""
  132. session.add_user_turn("Mach das Licht an")
  133. follow_up = session.add_assistant_turn(
  134. content="Welches Licht?",
  135. is_follow_up=True,
  136. )
  137. answer = session.add_user_turn("Das Wohnzimmerlicht")
  138. assert follow_up.response_turn_id == answer.turn_id
  139. assert not session.has_pending_follow_up
  140. def test_complete_session(self, session):
  141. """Testet Session-Abschluss."""
  142. session.start()
  143. session.complete("Test abgeschlossen")
  144. assert session.is_completed
  145. assert session.state == SessionState.COMPLETED
  146. assert session.completed_at is not None
  147. def test_timeout_session(self, session):
  148. """Testet Session-Timeout."""
  149. session.timeout()
  150. assert session.is_completed
  151. assert session.state == SessionState.TIMEOUT
  152. def test_cancel_session(self, session):
  153. """Testet Session-Abbruch."""
  154. session.cancel("Abgebrochen")
  155. assert session.is_completed
  156. assert session.state == SessionState.CANCELLED
  157. def test_user_turns(self, session):
  158. """Testet User-Turns-Filter."""
  159. session.add_user_turn("User 1")
  160. session.add_assistant_turn("Assistent 1")
  161. session.add_user_turn("User 2")
  162. user_turns = session.user_turns
  163. assert len(user_turns) == 2
  164. def test_assistant_turns(self, session):
  165. """Testet Assistenten-Turns-Filter."""
  166. session.add_user_turn("User")
  167. session.add_assistant_turn("Antwort")
  168. session.add_assistant_turn("Rückfrage", is_follow_up=True)
  169. assistant_turns = session.assistant_turns
  170. assert len(assistant_turns) == 2
  171. def test_max_turns_limit(self, session):
  172. """Testet Turn-Limit."""
  173. for i in range(5):
  174. session.add_user_turn(f"Turn {i}")
  175. with pytest.raises(RuntimeError):
  176. session.add_user_turn("Zu viel")
  177. def test_context(self, session):
  178. """Testet Kontext."""
  179. session.set_context("room", "wohnzimmer")
  180. assert session.get_context("room") == "wohnzimmer"
  181. assert session.get_context("missing", "default") == "default"
  182. def test_to_dict(self, session):
  183. """Testet Dictionary-Konvertierung."""
  184. session.add_user_turn("Test")
  185. d = session.to_dict()
  186. assert d["session_id"] == "test-123"
  187. assert d["turn_count"] == 1
  188. assert "turns" in d
  189. def test_transcript(self, session):
  190. """Testet Transkript."""
  191. session.add_user_turn("Hallo")
  192. session.add_assistant_turn("Hallo!")
  193. session.add_assistant_turn("Wie kann ich helfen?", is_follow_up=True)
  194. transcript = session.get_transcript()
  195. assert "User: Hallo" in transcript
  196. assert "Assistent: Hallo!" in transcript
  197. assert "Rückfrage" in transcript
  198. def test_duration(self, session):
  199. """Testet Dauer-Berechnung."""
  200. assert session.duration_seconds >= 0
  201. session.complete()
  202. assert session.duration_seconds >= 0
  203. def test_is_active(self, session):
  204. """Testet is_active."""
  205. assert session.is_active
  206. session.complete()
  207. assert not session.is_active
  208. class TestContextScope:
  209. """Tests für ContextScope."""
  210. def test_scopes(self):
  211. """Testet Kontext-Scopes."""
  212. assert ContextScope.TURN.value == "turn"
  213. assert ContextScope.SESSION.value == "session"
  214. assert ContextScope.USER.value == "user"
  215. assert ContextScope.GLOBAL.value == "global"
  216. class TestContextVariable:
  217. """Tests für ContextVariable."""
  218. def test_variable_creation(self):
  219. """Testet Variable-Erstellung."""
  220. var = ContextVariable(
  221. name="test",
  222. value=42,
  223. scope=ContextScope.SESSION,
  224. )
  225. assert var.name == "test"
  226. assert var.value == 42
  227. def test_update(self):
  228. """Testet Update."""
  229. var = ContextVariable(name="test", value=1)
  230. old_time = var.updated_at
  231. var.update(2)
  232. assert var.value == 2
  233. assert var.updated_at >= old_time
  234. class TestConversationContext:
  235. """Tests für ConversationContext."""
  236. @pytest.fixture
  237. def context(self):
  238. """Erstellt Context."""
  239. return ConversationContext()
  240. def test_set_get(self, context):
  241. """Testet Set/Get."""
  242. context.set("test", "value")
  243. assert context.get("test") == "value"
  244. def test_get_default(self, context):
  245. """Testet Default-Wert."""
  246. assert context.get("missing", "default") == "default"
  247. def test_has(self, context):
  248. """Testet has."""
  249. context.set("exists", True)
  250. assert context.has("exists")
  251. assert not context.has("missing")
  252. def test_remove(self, context):
  253. """Testet remove."""
  254. context.set("test", "value")
  255. assert context.remove("test")
  256. assert not context.has("test")
  257. def test_turn_scope(self, context):
  258. """Testet Turn-Scope."""
  259. context.set("turn_var", "value", ContextScope.TURN)
  260. assert context.get("turn_var") == "value"
  261. context.clear_turn()
  262. assert not context.has("turn_var")
  263. def test_turn_overrides_session(self, context):
  264. """Testet Turn überschreibt Session."""
  265. context.set("var", "session", ContextScope.SESSION)
  266. context.set("var", "turn", ContextScope.TURN)
  267. assert context.get("var") == "turn"
  268. def test_clear_scope(self, context):
  269. """Testet Scope-Löschen."""
  270. context.set("a", 1, ContextScope.SESSION)
  271. context.set("b", 2, ContextScope.USER)
  272. context.clear_scope(ContextScope.SESSION)
  273. assert not context.has("a")
  274. assert context.has("b")
  275. def test_get_all(self, context):
  276. """Testet get_all."""
  277. context.set("a", 1, ContextScope.SESSION)
  278. context.set("b", 2, ContextScope.TURN)
  279. all_vars = context.get_all()
  280. assert all_vars["a"] == 1
  281. assert all_vars["b"] == 2
  282. def test_merge(self, context):
  283. """Testet merge."""
  284. other = ConversationContext()
  285. other.set("other_var", "value")
  286. context.merge(other)
  287. assert context.get("other_var") == "value"
  288. def test_intent_helpers(self, context):
  289. """Testet Intent-Helper."""
  290. context.set_intent("weather_query", 0.95)
  291. assert context.get_intent() == "weather_query"
  292. def test_entity_helpers(self, context):
  293. """Testet Entity-Helper."""
  294. context.set_entities({"location": "Berlin"})
  295. entities = context.get_entities()
  296. assert entities["location"] == "Berlin"
  297. def test_slot_helpers(self, context):
  298. """Testet Slot-Helper."""
  299. context.set_slot("room", "wohnzimmer")
  300. context.set_slot("device", "licht")
  301. assert context.get_slot("room") == "wohnzimmer"
  302. assert context.slots == {"room": "wohnzimmer", "device": "licht"}
  303. context.clear_slots()
  304. assert context.slots == {}
  305. def test_to_dict(self, context):
  306. """Testet Dictionary-Konvertierung."""
  307. context.set("test", "value")
  308. d = context.to_dict()
  309. assert "variables" in d
  310. assert "turn_variables" in d