"""Tests for the TUI view-state reducer.""" import pytest from stt_daemon.tui.state import ( MAX_TRANSCRIPTS, SPEAKER_NAMED_STYLE, SPEAKER_UNKNOWN_STYLE, TIME_STYLE, ViewState, _fmt_clock, ) 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}" last_block = state.transcript_lines()[-1].plain assert f"t{MAX_TRANSCRIPTS + 24}" in last_block assert "EN" in last_block def test_transcript_block_header_has_clock_lang_duration_and_reason(): state = ViewState() state.ingest( { "type": "transcript", "ts": 1759073916.19, "lang": "es", "lang_prob": 0.99, "duration_s": 7.83, "end_reason": "end_of_turn", "text": "hola mundo", } ) header, body = state.transcript_lines()[0].plain.split("\n") assert header[2] == ":" and header[5] == ":" and len(header[:8]) == 8 assert "ES" in header assert "7.8s" in header assert "end_of_turn" in header assert body.strip() == "hola mundo" def _header_span_style(block, fragment: str) -> str: for span in block.spans: if block.plain[span.start : span.end] == fragment: return str(span.style) raise AssertionError(f"no span rendering {fragment!r} in {block.plain!r}") def test_transcript_header_places_named_speaker_between_lang_and_duration(): state = ViewState() state.ingest( { "type": "transcript", "ts": 1759073916.19, "lang": "en", "duration_s": 3.24, "end_reason": "pause", "speaker": "Efren", "speaker_id": "spk_0001", "speaker_status": "matched", "speaker_score": 0.87, "text": "turn on the lights", } ) block = state.transcript_lines()[0] header, body = block.plain.split("\n") assert header.index("EN") < header.index("Efren") < header.index("3.2s") assert _header_span_style(block, "Efren") == SPEAKER_NAMED_STYLE == "bold green" assert body.strip() == "turn on the lights" def test_transcript_header_renders_matched_but_unnamed_speaker_as_unknown(): state = ViewState() state.ingest( { "type": "transcript", "ts": 1759073916.19, "lang": "en", "duration_s": 3.24, "speaker": "Unknown", "speaker_id": "spk_0002", "speaker_status": "matched", "text": "hello there", } ) block = state.transcript_lines()[0] header = block.plain.split("\n")[0] assert header.index("EN") < header.index("Unknown") < header.index("3.2s") assert _header_span_style(block, "Unknown") == SPEAKER_UNKNOWN_STYLE == "bold red" @pytest.mark.parametrize("status", ["uncertain", "unknown", "none"]) def test_transcript_header_renders_unmatched_statuses_as_unknown(status): state = ViewState() state.ingest( { "type": "transcript", "ts": 1759073916.19, "lang": "es", "duration_s": 1.0, "speaker": "Unknown", "speaker_status": status, "text": "hola", } ) block = state.transcript_lines()[0] assert "Unknown" in block.plain assert _header_span_style(block, "Unknown") == SPEAKER_UNKNOWN_STYLE def test_transcript_header_without_speaker_field_renders_unknown(): state = ViewState() state.ingest( { "type": "transcript", "ts": 1759073916.19, "lang": "en", "duration_s": 1.0, "text": "no speaker field", } ) block = state.transcript_lines()[0] assert _header_span_style(block, "Unknown") == SPEAKER_UNKNOWN_STYLE def test_speakers_event_populates_the_speaker_snapshot(): speakers = [ { "id": "spk_0001", "name": "Efren", "status": "confirmed", "cumulative_s": 42.5, "sample_path": "speakers/samples/spk_0001.wav", "created_at": "2026-09-28T12:00:00", } ] state = ViewState() state.ingest({"type": "speakers", "speakers": speakers}) assert state.speakers == speakers def test_speakers_event_without_payload_clears_the_snapshot(): state = ViewState() state.speakers = [{"id": "spk_0001"}] state.ingest({"type": "speakers"}) assert state.speakers == [] def test_transcript_time_language_and_text_colors_are_disjoint(): state = ViewState() for lang in ("es", "en", "fr", "de", "pt", "it", "zz"): state.ingest( { "type": "transcript", "ts": 1.0, "lang": lang, "duration_s": 1.0, "end_reason": "pause", "text": "x", } ) palette = { str(span.style).split()[-1] for block in state.transcript_lines() for span in block.spans } time_color = str(state.transcript_lines()[0].spans[0].style).split()[-1] lang_color = str(state.transcript_lines()[0].spans[1].style).split()[-1] text_colors = { str(block.spans[-1].style).split()[-1] for block in state.transcript_lines() } assert not ({time_color, lang_color} & text_colors), palette assert time_color != lang_color def test_transcripts_total_keeps_counting_past_the_cap(): """The bounded buffer drops old lines; the total must still advance.""" state = ViewState() for i in range(MAX_TRANSCRIPTS + 25): state.ingest({"type": "transcript", "text": f"t{i}", "lang": "en"}) assert state.transcripts_total == MAX_TRANSCRIPTS + 25 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 block = state.transcript_lines()[0].plain assert "preserve this" in block assert "EN" in block def _signals_triage(): return { "order": ["q_choice", "q_noul", "q_score", "q_unknown"], "items": { "q_choice": { "kind": "choice", "choice": "yes", "answer_confidence": 0.91, "flagged": True, }, "q_noul": {"kind": "noul", "p": 0.75, "flagged": False}, "q_score": {"kind": "score", "level": 3, "label": "high", "flagged": True}, "q_unknown": {"kind": "mystery", "flagged": False}, }, "escalated": True, "input_tokens": 42, } def test_triage_signals_event_stores_and_bumps_epoch(): state = ViewState() epoch = state.triage_epoch state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) assert state.triage == _signals_triage() assert state.triage_epoch == epoch + 1 def test_triage_brief_event_replaces_triage_and_bumps_epoch(): state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) epoch = state.triage_epoch merged = {**_signals_triage(), "brief": "one brief line"} state.ingest({"type": "triage", "stage": "content", "triage": merged}) assert state.triage == merged assert state.triage_epoch == epoch + 1 def test_triage_cleared_event_empties_and_bumps_epoch(): state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) epoch = state.triage_epoch state.ingest({"type": "triage", "stage": "cleared"}) assert state.triage is None assert state.triage_epoch == epoch + 1 def test_status_adopts_triage_fields(): state = ViewState() epoch = state.triage_epoch state.ingest( { "type": "status", "triage": _signals_triage(), "triage_state": "ok", "triage_reason": "", } ) assert state.triage_state == "ok" assert state.triage == _signals_triage() assert state.triage_epoch == epoch + 1 def test_status_without_triage_fields_bumps_nothing(): state = ViewState() state.ingest({"type": "status", "mode": "idle", "ready": True}) assert state.triage_epoch == 0 assert state.triage_state == "disabled" def test_triage_lines_choice_noul_score_and_flag_marks(): state = ViewState() state.triage = _signals_triage() state.triage_state = "ok" lines = state.triage_lines() assert lines[0] == "q_choice: yes (0.91) ⚠" assert lines[1].startswith("███████ q_noul: 0.75") assert lines[2].startswith("███") and "q_score: high ⚠" in lines[2] assert lines[3] == "q_unknown" def test_triage_lines_brief_appended_line_by_line(): state = ViewState() state.triage = { "order": ["q1"], "items": {"q1": {"kind": "choice", "choice": "no", "answer_confidence": 0.9}}, "brief": "First line.\nSecond line.\nThird line.", } state.triage_state = "ok" assert state.triage_lines() == [ "q1: no (0.90)", "", "First line.", "Second line.", "Third line.", ] def test_triage_lines_missing_or_non_str_brief_degrades_to_signals_only(): state = ViewState() base = { "order": ["q1"], "items": {"q1": {"kind": "choice", "choice": "yes", "answer_confidence": 0.5}}, } state.triage = base state.triage_state = "ok" assert state.triage_lines() == ["q1: yes (0.50)"] for malformed in (None, 123, ["not", "a", "str"], {"brief": "nested"}): state.triage = {**base, "brief": malformed} assert state.triage_lines() == ["q1: yes (0.50)"] def test_triage_lines_brief_none_never_raises(): state = ViewState() state.triage = {"brief": None} state.triage_state = "ok" assert state.triage_lines() == ["triage: no signals"] def test_triage_lines_empty_brief_renders_nothing_extra(): state = ViewState() state.triage = { "order": ["q1"], "items": {"q1": {"kind": "choice", "choice": "yes", "answer_confidence": 0.5}}, "brief": "", } state.triage_state = "ok" assert state.triage_lines() == ["q1: yes (0.50)"] def test_triage_lines_non_ok_state_renders_reason(): state = ViewState() state.triage_state = "loading" state.triage_reason = "model warming up" assert state.triage_lines() == ["triage: unavailable — model warming up"] def test_triage_lines_unknown_reason_falls_back_generically(): state = ViewState() state.triage_state = "service_down" assert state.triage_lines() == ["triage: unavailable — service_down"] def test_triage_lines_ok_without_data_renders_no_data(): state = ViewState() state.triage_state = "ok" assert state.triage_lines() == ["triage: unavailable — no data"] def test_triage_lines_malformed_payload_never_raises(): state = ViewState() state.triage_state = "ok" state.triage = { "order": ["a", "b", "c"], "items": { "a": {"kind": "choice", "flagged": True}, "b": {"kind": "noul", "p": "not-a-number", "flagged": True}, "c": {"kind": "score", "level": None}, }, } lines = state.triage_lines() assert lines[0] == "a: ? (?) ⚠" assert "b: ? ⚠" in lines[1] assert "c: ?" in lines[2] def test_triage_lines_empty_order_renders_no_signals(): state = ViewState() state.triage_state = "ok" state.triage = {"order": [], "items": {}} assert state.triage_lines() == ["triage: no signals"] def test_clear_output_resets_triage_and_bumps_epoch(): state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) epoch = state.triage_epoch state.clear_output() assert state.triage is None assert state.triage_epoch == epoch + 1 def test_status_adopted_triage_survives_then_cleared_event_empties_panel(): state = ViewState() state.ingest( {"type": "status", "triage": _signals_triage(), "triage_state": "ok", "triage_reason": ""} ) assert state.triage_lines()[0].startswith("q_choice:") state.ingest({"type": "triage", "stage": "cleared"}) assert state.triage is None def test_lone_signals_event_renders_without_status_snapshot(): state = ViewState() state.ingest( { "type": "triage", "stage": "signals", "triage": { "order": ["category"], "items": { "category": {"kind": "choice", "choice": "question", "answer_confidence": 0.91} }, }, } ) assert state.triage_state == "ok" assert state.triage_lines() == ["category: question (0.91)"] def test_lone_brief_event_renders_without_status_snapshot(): state = ViewState() state.ingest( { "type": "triage", "stage": "content", "triage": { "order": ["category"], "items": { "category": {"kind": "choice", "choice": "question", "answer_confidence": 0.91} }, "brief": "wants action", }, } ) assert state.triage_state == "ok" lines = state.triage_lines() assert lines[0] == "category: question (0.91)" assert lines[1] == "" assert lines[2] == "wants action" def test_cleared_event_after_lone_signals_leaves_state_usable(): state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) state.ingest({"type": "triage", "stage": "cleared"}) assert state.triage is None assert state.triage_lines() == ["triage: unavailable — no data"] def test_triage_event_with_ts_records_last_triage_time(): state = ViewState() state.ingest( {"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": 1759073916.19} ) assert state.triage_ts == 1759073916.19 def test_triage_event_without_ts_keeps_previous_time(): state = ViewState() state.ingest( {"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": 1759073916.19} ) state.ingest( { "type": "triage", "stage": "content", "triage": {**_signals_triage(), "content": {"summary": "s"}}, } ) assert state.triage_ts == 1759073916.19 def test_triage_renderables_prepends_clock_in_transcript_time_style(): ts = 1759073916.19 state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": ts}) renderables = state.triage_renderables() assert renderables[0].plain == _fmt_clock(ts) assert str(renderables[0].spans[0].style) == TIME_STYLE assert [item.plain for item in renderables[1:]] == state.triage_lines() def test_triage_cleared_event_and_clear_output_drop_the_clock(): ts = 1759073916.19 state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": ts}) state.ingest({"type": "triage", "stage": "cleared"}) assert state.triage_ts is None assert all(item.plain != _fmt_clock(ts) for item in state.triage_renderables()) state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": ts}) state.clear_output() assert state.triage_ts is None assert all(item.plain != _fmt_clock(ts) for item in state.triage_renderables()) def test_triage_renderables_clock_only_for_ok_dict_triage(): ts = 1759073916.19 clock = _fmt_clock(ts) state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": ts}) state.triage_state = "loading" state.triage_reason = "warming up" assert state.triage_renderables()[0].plain == "triage: unavailable — warming up" assert all(item.plain != clock for item in state.triage_renderables()) state.triage_state = "ok" state.triage = None assert state.triage_renderables()[0].plain == "triage: unavailable — no data" assert all(item.plain != clock for item in state.triage_renderables()) def test_status_adoption_preserves_last_triage_time(): ts = 1759073916.19 state = ViewState() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage(), "ts": ts}) state.ingest( { "type": "status", "triage": _signals_triage(), "triage_state": "ok", "triage_reason": "", } ) assert state.triage_ts == ts assert state.triage_renderables()[0].plain == _fmt_clock(ts) def _questions(): return { "topic": { "type": "choice", "instructions": "What is the main theme?", "criteria": { "planning": "roadmap or prioritization", "problem": "a blocker or risk", }, }, "urgency": { "type": "choice", "instructions": "How urgent?", "criteria": ["low", "high"], }, "sentiment": { "type": "score", "instructions": "Overall tone?", "criteria": ["negative", "neutral", "positive"], }, "blocker": { "type": "noul", "instructions": "A blocker was raised.", "criteria": {"true": "an obstacle was raised", "false": "no obstacle"}, }, "owner_named": { "type": "noul", "instructions": "A responsible person was named.", }, } def test_status_adopts_questions_and_bumps_questions_epoch_only(): state = ViewState() triage_epoch = state.triage_epoch questions = _questions() state.ingest({"type": "status", "questions": questions}) assert state.questions == questions assert state.questions_epoch == 1 assert state.triage_epoch == triage_epoch def test_status_without_questions_preserves_loaded_questions(): state = ViewState() questions = _questions() state.ingest({"type": "status", "questions": questions}) state.ingest({"type": "status", "mode": "idle", "ready": True}) assert state.questions == questions assert state.questions_epoch == 1 def test_questions_lines_render_choice_dict_choice_list_score_and_noul(): state = ViewState() state.questions = _questions() assert state.questions_lines() == [ "topic [choice]", " What is the main theme?", " planning — roadmap or prioritization", " problem — a blocker or risk", "urgency [choice]", " How urgent?", " low", " high", "sentiment [score]", " Overall tone?", " 1. negative", " 2. neutral", " 3. positive", "blocker [noul]", " A blocker was raised.", " true — an obstacle was raised", " false — no obstacle", "owner_named [noul]", " A responsible person was named.", ] def test_questions_render_while_triage_lane_is_loading(): state = ViewState() state.questions = _questions() state.triage_state = "loading" state.triage_reason = "warming up" assert state.questions_lines()[0] == "topic [choice]" assert not any("unavailable" in line for line in state.questions_lines()) def test_questions_lines_unavailable_when_missing(): state = ViewState() state.triage_state = "loading" state.triage_reason = "model warming up" assert state.questions_lines() == ["questions: unavailable — model warming up"] def test_questions_lines_unavailable_falls_back_to_state_then_generic(): state = ViewState() assert state.questions_lines() == ["questions: unavailable — disabled"] state.triage_state = "" state.triage_reason = "" assert state.questions_lines() == ["questions: unavailable — no questions loaded"] def test_questions_lines_unavailable_for_empty_or_non_dict_payload(): state = ViewState() state.triage_state = "no_questions" state.triage_reason = "no questions loaded from questions.json" for payload in (None, {}, [], "junk", 7): state.ingest({"type": "status", "questions": payload}) assert state.questions_lines() == [ "questions: unavailable — no questions loaded from questions.json" ] def test_questions_lines_malformed_entries_never_raise(): state = ViewState() state.questions = { "good": {"type": "choice", "instructions": "pick", "criteria": {"a": "b"}}, "not_a_dict": "boom", "no_instructions": {"type": "noul"}, "bad_criteria": {"type": "score", "instructions": "level?", "criteria": {}}, "empty_criteria": {"type": "choice", "instructions": "choose", "criteria": []}, "no_type": {"instructions": "huh"}, } lines = state.questions_lines() assert lines == [ "good [choice]", " pick", " a — b", "no_instructions [noul]", "bad_criteria [score]", " level?", "empty_criteria [choice]", " choose", "no_type [?]", " huh", ] def test_questions_lines_all_malformed_falls_back_to_unavailable(): state = ViewState() state.triage_state = "ok" state.questions = {"broken": "not a dict"} assert state.questions_lines() == ["questions: unavailable — ok"] def test_questions_renderables_plain_matches_lines_with_styled_header(): state = ViewState() state.questions = _questions() renderables = state.questions_renderables() assert [item.plain for item in renderables] == state.questions_lines() header = renderables[0] assert header.plain == "topic [choice]" assert [(header.plain[s.start : s.end], str(s.style)) for s in header.spans] == [ ("topic", "bold"), ("[choice]", "dim"), ] assert all(not item.spans for item in renderables[1:4]) def test_questions_renderables_unavailable_is_plain_single_line(): state = ViewState() state.triage_state = "loading" state.triage_reason = "warming up" renderables = state.questions_renderables() assert [item.plain for item in renderables] == [ "questions: unavailable — warming up" ] assert not renderables[0].spans def test_clear_output_keeps_questions(): state = ViewState() state.questions = _questions() state.ingest({"type": "triage", "stage": "signals", "triage": _signals_triage()}) state.clear_output() assert state.questions == _questions() assert state.questions_epoch == 0 assert state.questions_lines()[0] == "topic [choice]"