| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- """Tests for the TUI view-state reducer."""
- from stt_daemon.tui.state import MAX_TRANSCRIPTS, ViewState
- def test_mode_event_updates_mode_and_style():
- state = ViewState()
- state.ingest({"type": "mode", "mode": "command_armed"})
- assert state.mode == "command_armed"
- assert state.mode_style() == "yellow"
- def test_initial_health_is_not_optimistic():
- """Before the first heartbeat arrives, health must read as unknown/down."""
- state = ViewState()
- assert state.health_ok() is False
- state.ingest({"type": "heartbeat", "stt_ok": True, "device_ok": True})
- assert state.health_ok() is True
- def test_heartbeat_self_corrects_missed_mode():
- state = ViewState()
- state.ingest(
- {
- "type": "heartbeat",
- "mode": "chat",
- "stt_ok": True,
- "in_flight": False,
- "worker_stall_s": 0.0,
- "queue_depth": 2,
- "device_ok": True,
- }
- )
- assert state.mode == "chat"
- assert state.queue_depth == 2
- def test_heartbeat_ingests_inference_depth_and_in_flight():
- state = ViewState()
- state.ingest(
- {
- "type": "heartbeat",
- "inference_depth": 3,
- "inference_in_flight": True,
- }
- )
- assert state.inference_depth == 3
- assert state.inference_in_flight is True
- def test_heartbeat_omitting_inference_fields_preserves_prior_values():
- state = ViewState()
- state.inference_depth = 5
- state.inference_in_flight = True
- state.ingest(
- {
- "type": "heartbeat",
- "mode": "idle",
- "stt_ok": True,
- }
- )
- assert state.inference_depth == 5
- assert state.inference_in_flight is True
- def test_health_red_when_stt_not_ok():
- state = ViewState()
- state.ingest({"type": "heartbeat", "mode": "idle", "stt_ok": False})
- assert state.health_ok() is False
- def test_health_red_when_device_down():
- state = ViewState()
- state.ingest({"type": "heartbeat", "mode": "idle", "stt_ok": True, "device_ok": False})
- assert state.health_ok() is False
- def test_worker_stall_recorded_and_degrades():
- state = ViewState()
- state.ingest({"type": "heartbeat", "stt_ok": False, "worker_stall_s": 16.5})
- assert state.worker_stall_s == 16.5
- assert state.health_ok() is False
- def test_command_text_and_edited_flag():
- state = ViewState()
- state.ingest({"type": "command_text", "text": "hello", "edited": True, "phase": "ready"})
- assert state.command_text == "hello"
- assert state.command_edited is True
- assert state.phase == "ready"
- def test_status_populates_fields():
- state = ViewState()
- state.ingest(
- {
- "type": "status",
- "mode": "command_armed",
- "ready": True,
- "phase": "ready",
- "command_text": "x",
- "stt_ok": True,
- }
- )
- assert state.mode == "command_armed"
- assert state.ready is True
- assert state.command_text == "x"
- def test_transcripts_are_bounded():
- state = ViewState()
- for i in range(MAX_TRANSCRIPTS + 25):
- state.ingest({"type": "transcript", "text": f"t{i}", "lang": "en"})
- assert len(state.transcripts) == MAX_TRANSCRIPTS
- assert state.transcripts[-1]["text"] == f"t{MAX_TRANSCRIPTS + 24}"
- assert state.transcript_lines()[-1].startswith("[en]")
- def test_command_output_is_collected_for_display():
- state = ViewState()
- state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
- state.ingest(
- {"type": "tool_result", "tool": "get_datetime", "ok": True, "result": "2026-09-21T01:00:00"}
- )
- state.ingest({"type": "error", "code": "not_ready", "message": "command is not ready"})
- lines = state.lower_lines()
- assert any("tool get_datetime" in line for line in lines)
- assert any("ok:" in line and "2026-09-21" in line for line in lines)
- assert any("not_ready" in line for line in lines)
- def test_chat_turns_also_appear_in_lower_log():
- state = ViewState()
- state.ingest({"type": "chat_message", "role": "user", "text": "hello"})
- assert state.lower_lines() == ["user: hello"]
- def test_lower_log_is_bounded():
- from stt_daemon.tui.state import MAX_LOWER
- state = ViewState()
- for i in range(MAX_LOWER + 20):
- state.ingest({"type": "error", "code": f"e{i}", "message": "m"})
- assert len(state.lower_lines()) == MAX_LOWER
- def test_speech_activity_is_tracked():
- state = ViewState()
- assert state.speech_open is False
- state.ingest({"type": "speech_start"})
- assert state.speech_open is True
- state.ingest({"type": "speech_stop"})
- assert state.speech_open is False
- state.ingest({"type": "status", "speech_open": True})
- assert state.speech_open is True
- def test_chat_and_tool_events_recorded():
- state = ViewState()
- state.ingest({"type": "chat_message", "role": "user", "text": "hi"})
- state.ingest({"type": "chat_message", "role": "assistant", "text": "hello"})
- state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
- state.ingest({"type": "tool_result", "tool": "get_datetime", "ok": True, "result": "x"})
- state.ingest({"type": "error", "code": "no_viewer", "message": "m"})
- assert state.chat_lines() == ["user: hi", "assistant: hello"]
- assert state.tool is not None and state.tool["tool"] == "get_datetime"
- assert state.tool_result is not None and state.tool_result["ok"] is True
- assert state.last_error is not None and state.last_error["code"] == "no_viewer"
- def test_leaving_chat_clears_chat_pane_but_keeps_command_output():
- state = ViewState()
- state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
- state.ingest({"type": "mode", "mode": "chat"})
- state.ingest({"type": "chat_message", "role": "user", "text": "hello"})
- state.ingest({"type": "chat_message", "role": "assistant", "text": "hi"})
- assert state.chat_lines() == ["user: hello", "assistant: hi"]
- epoch = state.chat_epoch
- state.ingest({"type": "mode", "mode": "idle"})
- assert state.chat_lines() == []
- assert state.lower_lines() == ["-> tool get_datetime"]
- assert state.chat_epoch == epoch + 1
- def test_heartbeat_mode_change_clears_chat():
- state = ViewState()
- state.ingest({"type": "mode", "mode": "chat"})
- state.ingest({"type": "chat_message", "role": "user", "text": "x"})
- state.ingest({"type": "heartbeat", "mode": "idle", "stt_ok": True})
- assert state.chat_lines() == []
- assert state.lower_lines() == []
- def test_compaction_note_renders_and_clears_on_chat_exit():
- state = ViewState()
- state.ingest({"type": "mode", "mode": "chat"})
- state.ingest(
- {
- "type": "chat_message",
- "role": "note",
- "text": "Compacted 4 earlier turns into a summary.",
- }
- )
- assert "note: Compacted 4 earlier turns into a summary." in state.lower_lines()
- state.ingest({"type": "mode", "mode": "idle"})
- assert state.lower_lines() == []
- def test_entering_chat_does_not_clear_rendered_log():
- state = ViewState()
- state.ingest({"type": "tool_result", "tool": "get_datetime", "ok": True, "result": "x"})
- epoch = state.chat_epoch
- state.ingest({"type": "mode", "mode": "chat"})
- assert state.chat_epoch == epoch
- assert any("ok:" in line for line in state.lower_lines())
- def test_cleared_event_wipes_output_and_increments_epoch():
- state = ViewState()
- state.ingest({"type": "chat_message", "role": "user", "text": "hello"})
- state.ingest({"type": "chat_message", "role": "assistant", "text": "hi"})
- state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
- state.ingest(
- {
- "type": "tool_result",
- "tool": "get_datetime",
- "ok": True,
- "result": "2026-09-21T01:00:00",
- }
- )
- state.ingest({"type": "error", "code": "test_error", "message": "test message"})
- state.ingest({"type": "transcript", "text": "preserve this", "lang": "en"})
- epoch = state.chat_epoch
- assert len(state.chat_lines()) == 2
- assert len(state.lower_lines()) >= 4
- assert state.tool is not None
- assert state.tool_result is not None
- assert state.last_error is not None
- assert len(state.transcripts) == 1
- state.ingest({"type": "cleared"})
- assert state.chat_lines() == []
- assert state.lower_lines() == []
- assert state.tool is None
- assert state.tool_result is None
- assert state.last_error is None
- assert state.chat_epoch == epoch + 1
- assert len(state.transcripts) == 1
- assert state.transcript_lines() == ["[en] preserve this"]
|