test_tui_state.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. """Tests for the TUI view-state reducer."""
  2. from stt_daemon.tui.state import MAX_TRANSCRIPTS, ViewState
  3. def test_mode_event_updates_mode_and_style():
  4. state = ViewState()
  5. state.ingest({"type": "mode", "mode": "command_armed"})
  6. assert state.mode == "command_armed"
  7. assert state.mode_style() == "yellow"
  8. def test_initial_health_is_not_optimistic():
  9. """Before the first heartbeat arrives, health must read as unknown/down."""
  10. state = ViewState()
  11. assert state.health_ok() is False
  12. state.ingest({"type": "heartbeat", "stt_ok": True, "device_ok": True})
  13. assert state.health_ok() is True
  14. def test_heartbeat_self_corrects_missed_mode():
  15. state = ViewState()
  16. state.ingest(
  17. {
  18. "type": "heartbeat",
  19. "mode": "chat",
  20. "stt_ok": True,
  21. "in_flight": False,
  22. "worker_stall_s": 0.0,
  23. "queue_depth": 2,
  24. "device_ok": True,
  25. }
  26. )
  27. assert state.mode == "chat"
  28. assert state.queue_depth == 2
  29. def test_health_red_when_stt_not_ok():
  30. state = ViewState()
  31. state.ingest({"type": "heartbeat", "mode": "idle", "stt_ok": False})
  32. assert state.health_ok() is False
  33. def test_health_red_when_device_down():
  34. state = ViewState()
  35. state.ingest({"type": "heartbeat", "mode": "idle", "stt_ok": True, "device_ok": False})
  36. assert state.health_ok() is False
  37. def test_worker_stall_recorded_and_degrades():
  38. state = ViewState()
  39. state.ingest({"type": "heartbeat", "stt_ok": False, "worker_stall_s": 16.5})
  40. assert state.worker_stall_s == 16.5
  41. assert state.health_ok() is False
  42. def test_command_text_and_edited_flag():
  43. state = ViewState()
  44. state.ingest({"type": "command_text", "text": "hello", "edited": True, "phase": "ready"})
  45. assert state.command_text == "hello"
  46. assert state.command_edited is True
  47. assert state.phase == "ready"
  48. def test_status_populates_fields():
  49. state = ViewState()
  50. state.ingest(
  51. {
  52. "type": "status",
  53. "mode": "command_armed",
  54. "ready": True,
  55. "phase": "ready",
  56. "command_text": "x",
  57. "stt_ok": True,
  58. }
  59. )
  60. assert state.mode == "command_armed"
  61. assert state.ready is True
  62. assert state.command_text == "x"
  63. def test_transcripts_are_bounded():
  64. state = ViewState()
  65. for i in range(MAX_TRANSCRIPTS + 25):
  66. state.ingest({"type": "transcript", "text": f"t{i}", "lang": "en"})
  67. assert len(state.transcripts) == MAX_TRANSCRIPTS
  68. assert state.transcripts[-1]["text"] == f"t{MAX_TRANSCRIPTS + 24}"
  69. assert state.transcript_lines()[-1].startswith("[en]")
  70. def test_command_output_is_collected_for_display():
  71. state = ViewState()
  72. state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
  73. state.ingest(
  74. {"type": "tool_result", "tool": "get_datetime", "ok": True, "result": "2026-09-21T01:00:00"}
  75. )
  76. state.ingest({"type": "error", "code": "not_ready", "message": "command is not ready"})
  77. lines = state.lower_lines()
  78. assert any("tool get_datetime" in line for line in lines)
  79. assert any("ok:" in line and "2026-09-21" in line for line in lines)
  80. assert any("not_ready" in line for line in lines)
  81. def test_chat_turns_also_appear_in_lower_log():
  82. state = ViewState()
  83. state.ingest({"type": "chat_message", "role": "user", "text": "hello"})
  84. assert state.lower_lines() == ["user: hello"]
  85. def test_lower_log_is_bounded():
  86. from stt_daemon.tui.state import MAX_LOWER
  87. state = ViewState()
  88. for i in range(MAX_LOWER + 20):
  89. state.ingest({"type": "error", "code": f"e{i}", "message": "m"})
  90. assert len(state.lower_lines()) == MAX_LOWER
  91. def test_speech_activity_is_tracked():
  92. state = ViewState()
  93. assert state.speech_open is False
  94. state.ingest({"type": "speech_start"})
  95. assert state.speech_open is True
  96. state.ingest({"type": "speech_stop"})
  97. assert state.speech_open is False
  98. state.ingest({"type": "status", "speech_open": True})
  99. assert state.speech_open is True
  100. def test_chat_and_tool_events_recorded():
  101. state = ViewState()
  102. state.ingest({"type": "chat_message", "role": "user", "text": "hi"})
  103. state.ingest({"type": "chat_message", "role": "assistant", "text": "hello"})
  104. state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
  105. state.ingest({"type": "tool_result", "tool": "get_datetime", "ok": True, "result": "x"})
  106. state.ingest({"type": "error", "code": "no_viewer", "message": "m"})
  107. assert state.chat_lines() == ["user: hi", "assistant: hello"]
  108. assert state.tool is not None and state.tool["tool"] == "get_datetime"
  109. assert state.tool_result is not None and state.tool_result["ok"] is True
  110. assert state.last_error is not None and state.last_error["code"] == "no_viewer"