| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106 |
- """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_chat_capture_defaults_off_and_follows_event_and_status():
- state = ViewState()
- assert state.chat_capture is False
- state.ingest({"type": "chat_capture", "on": True})
- assert state.chat_capture is True
- state.ingest({"type": "status", "chat_capture": False})
- assert state.chat_capture is False
- def test_triage_chat_defaults_off_and_follows_event_and_status():
- state = ViewState()
- assert state.triage_chat is False
- state.ingest({"type": "triage_chat", "on": True})
- assert state.triage_chat is True
- state.ingest({"type": "status", "triage_chat": False})
- assert state.triage_chat is False
- 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_output_and_chat_selectors_partition_lower_log():
- state = ViewState()
- state.ingest({"type": "tool_call", "tool": "get_datetime", "args": {}})
- state.ingest({"type": "chat_message", "role": "user", "text": "hi"})
- state.ingest({"type": "error", "code": "boom", "message": "m"})
- assert state.output_lines() == ["-> tool get_datetime", "! boom: m"]
- assert state.chat_output_lines() == ["user: hi"]
- assert sorted(state.output_lines() + state.chat_output_lines()) == sorted(
- state.lower_lines()
- )
- 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_keeps_chat_and_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() == ["user: hello", "assistant: hi"]
- assert state.lower_lines() == ["-> tool get_datetime", "user: hello", "assistant: hi"]
- assert state.chat_epoch == epoch
- def test_heartbeat_mode_change_keeps_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() == ["user: x"]
- assert state.lower_lines() == ["user: x"]
- def test_compaction_note_persists_until_cleared():
- 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 "note: Compacted 4 earlier turns into a summary." in state.lower_lines()
- state.ingest({"type": "cleared"})
- 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]"
- def test_status_event_carrying_session_dir_sets_it():
- state = ViewState()
- assert state.session_dir is None
- state.ingest({"type": "status", "session_dir": "transcripts/2026-09-30-12-47-29"})
- assert state.session_dir == "transcripts/2026-09-30-12-47-29"
- def test_status_without_session_dir_preserves_prior_value():
- state = ViewState()
- state.ingest({"type": "status", "session_dir": "transcripts/2026-09-30-12-47-29"})
- state.ingest({"type": "status", "mode": "idle", "ready": True})
- assert state.session_dir == "transcripts/2026-09-30-12-47-29"
- def test_session_event_sets_session_dir():
- state = ViewState()
- state.ingest(
- {
- "type": "session",
- "session_id": "2026-09-30-12-47-29",
- "session_dir": "transcripts/2026-09-30-12-47-29",
- }
- )
- assert state.session_dir == "transcripts/2026-09-30-12-47-29"
- def test_transcript_carrying_session_dir_sets_it():
- state = ViewState()
- state.ingest(
- {
- "type": "transcript",
- "text": "hola",
- "lang": "es",
- "session_dir": "transcripts/2026-09-30-12-47-29",
- }
- )
- assert state.session_dir == "transcripts/2026-09-30-12-47-29"
- def test_cleared_keeps_session_dir():
- state = ViewState()
- state.ingest({"type": "status", "session_dir": "transcripts/2026-09-30-12-47-29"})
- state.ingest({"type": "chat_message", "role": "user", "text": "hello"})
- state.ingest({"type": "cleared"})
- assert state.session_dir == "transcripts/2026-09-30-12-47-29"
- assert state.chat_lines() == []
- def test_triage_lines_marks_probability_movement_against_previous():
- state = ViewState()
- state.triage_prev = {
- "order": ["q"],
- "items": {"q": {"kind": "noul", "p": 0.20, "flagged": False}},
- }
- state.triage = {
- "order": ["q"],
- "items": {"q": {"kind": "noul", "p": 0.75, "flagged": True}},
- }
- state.triage_state = "ok"
- line = state.triage_lines()[0]
- assert line.endswith("\u2191 \u26a0")
- assert "0.75" in line
- def test_triage_lines_marks_downward_movement():
- state = ViewState()
- state.triage_prev = {
- "order": ["q"],
- "items": {"q": {"kind": "noul", "p": 0.80, "flagged": True}},
- }
- state.triage = {
- "order": ["q"],
- "items": {"q": {"kind": "noul", "p": 0.30, "flagged": False}},
- }
- state.triage_state = "ok"
- assert state.triage_lines()[0].endswith("\u2193")
- def test_triage_lines_only_warns_on_a_new_flag():
- state = ViewState()
- prev = {"order": ["q"], "items": {"q": {"kind": "noul", "p": 0.9, "flagged": True}}}
- state.triage_prev = prev
- state.triage = {
- "order": ["q"],
- "items": {"q": {"kind": "noul", "p": 0.9, "flagged": True}},
- }
- state.triage_state = "ok"
- assert "\u26a0" not in state.triage_lines()[0]
- def test_triage_lines_shows_the_previous_choice_label():
- state = ViewState()
- state.triage_prev = {
- "order": ["q"],
- "items": {"q": {"kind": "choice", "choice": "proposed", "answer_confidence": 0.9}},
- }
- state.triage = {
- "order": ["q"],
- "items": {"q": {"kind": "choice", "choice": "made", "answer_confidence": 0.9}},
- }
- state.triage_state = "ok"
- assert state.triage_lines()[0] == "q: made (0.90) \u2190 proposed"
- def test_triage_lines_reports_hidden_low_signals():
- state = ViewState()
- state.triage = {
- "order": ["q1"],
- "items": {"q1": {"kind": "choice", "choice": "yes", "answer_confidence": 0.9}},
- "hidden": 2,
- }
- state.triage_state = "ok"
- assert state.triage_lines()[-1] == "(2 low signals hidden)"
- state.triage = {**state.triage, "hidden": 1}
- assert state.triage_lines()[-1] == "(1 low signal hidden)"
|