|
|
@@ -1115,7 +1115,7 @@ def test_question_mark_key_toggles_help_panel(server):
|
|
|
|
|
|
|
|
|
def test_app_bindings_list():
|
|
|
- """BINDINGS: 7 visible footer entries; hidden ones after; no h binding."""
|
|
|
+ """BINDINGS: 8 visible footer entries; hidden ones after; no h binding."""
|
|
|
pytest.importorskip("textual")
|
|
|
from stt_daemon.tui.app import AssistantTUI
|
|
|
|
|
|
@@ -1128,6 +1128,7 @@ def test_app_bindings_list():
|
|
|
assert ("escape", "cancel") in pairs
|
|
|
assert ("e", "edit command") in pairs
|
|
|
assert ("t", "chat toggle") in pairs
|
|
|
+ assert ("s", "questions view") in pairs
|
|
|
assert ("c", "clear") in pairs
|
|
|
assert ("q", "quit") in pairs
|
|
|
assert ("ctrl+p", "instructions") in pairs
|
|
|
@@ -1138,11 +1139,12 @@ def test_app_bindings_list():
|
|
|
assert not any(key == "ctrl+k" for key, _ in pairs)
|
|
|
by_key = {b.key: b for b in AssistantTUI.BINDINGS}
|
|
|
assert by_key["question_mark"].show is True
|
|
|
+ assert by_key["s"].show is True
|
|
|
assert by_key["m"].show is False
|
|
|
assert by_key["c"].show is False
|
|
|
assert by_key["ctrl+s"].show is False
|
|
|
assert by_key["ctrl+p"].show is False
|
|
|
- # Visible seven first, in footer order.
|
|
|
+ # Visible eight first, in footer order.
|
|
|
visible = [(b.key, b.description) for b in AssistantTUI.BINDINGS if b.show]
|
|
|
assert visible == [
|
|
|
("a", "arm command"),
|
|
|
@@ -1150,6 +1152,7 @@ def test_app_bindings_list():
|
|
|
("escape", "cancel"),
|
|
|
("e", "edit command"),
|
|
|
("t", "chat toggle"),
|
|
|
+ ("s", "questions view"),
|
|
|
("question_mark", "help"),
|
|
|
("q", "quit"),
|
|
|
]
|
|
|
@@ -1157,7 +1160,7 @@ def test_app_bindings_list():
|
|
|
|
|
|
|
|
|
def test_footer_shows_ordered_entries(server):
|
|
|
- """Left list is the 7 visible bindings (? for help); instructions only right-aligned."""
|
|
|
+ """Left list is the 8 visible bindings (? for help); instructions only right-aligned."""
|
|
|
pytest.importorskip("textual")
|
|
|
from textual.widgets import Footer
|
|
|
from textual.widgets._footer import FooterKey
|
|
|
@@ -1191,6 +1194,7 @@ def test_footer_shows_ordered_entries(server):
|
|
|
("escape", "cancel"),
|
|
|
("e", "edit command"),
|
|
|
("t", "chat toggle"),
|
|
|
+ ("s", "questions view"),
|
|
|
("question_mark", "help"),
|
|
|
("q", "quit"),
|
|
|
]
|
|
|
@@ -1622,3 +1626,175 @@ def test_transcript_pane_keeps_updating_past_cap(server):
|
|
|
asyncio.run(scenario())
|
|
|
assert any("after cap" in line for line in observed["lines"])
|
|
|
assert len(observed["lines"]) > MAX_TRANSCRIPTS
|
|
|
+
|
|
|
+
|
|
|
+def _questions_payload():
|
|
|
+ return {
|
|
|
+ "topic": {
|
|
|
+ "type": "choice",
|
|
|
+ "instructions": "What is the main theme?",
|
|
|
+ "criteria": {"planning": "roadmap or prioritization"},
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _rendered_lines(log):
|
|
|
+ return [line.text for line in log.lines]
|
|
|
+
|
|
|
+
|
|
|
+def _ingest_triage_signals(app):
|
|
|
+ app.state.ingest(
|
|
|
+ {
|
|
|
+ "type": "triage",
|
|
|
+ "stage": "signals",
|
|
|
+ "triage": {
|
|
|
+ "order": ["topic"],
|
|
|
+ "items": {
|
|
|
+ "topic": {
|
|
|
+ "kind": "choice",
|
|
|
+ "choice": "planning",
|
|
|
+ "answer_confidence": 0.91,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ )
|
|
|
+ app.state.ingest(
|
|
|
+ {
|
|
|
+ "type": "status",
|
|
|
+ "triage_state": "ok",
|
|
|
+ "triage_reason": "",
|
|
|
+ "questions": _questions_payload(),
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def test_s_key_swaps_triage_pane_to_questions_view(server):
|
|
|
+ """Press s: #triage border flips to questions and renders the structured blocks."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
+ app._drain()
|
|
|
+ _ingest_triage_signals(app)
|
|
|
+ app._render()
|
|
|
+ await pilot.pause()
|
|
|
+ observed["default_title"] = app.query_one("#triage", RichLog).border_title
|
|
|
+ observed["default_lines"] = _rendered_lines(app.query_one("#triage", RichLog))
|
|
|
+
|
|
|
+ await pilot.press("s")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["questions_title"] = app.query_one("#triage", RichLog).border_title
|
|
|
+ observed["questions_lines"] = _rendered_lines(app.query_one("#triage", RichLog))
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["default_title"] == "triage"
|
|
|
+ assert any("topic: planning (0.91)" in line for line in observed["default_lines"])
|
|
|
+ assert observed["questions_title"] == "questions"
|
|
|
+ rendered = "\n".join(observed["questions_lines"])
|
|
|
+ assert "topic [choice]" in rendered
|
|
|
+ assert "What is the main theme?" in rendered
|
|
|
+ assert "planning — roadmap or prioritization" in rendered
|
|
|
+ assert "topic: planning" not in rendered
|
|
|
+
|
|
|
+
|
|
|
+def test_s_key_again_restores_triage_view(server):
|
|
|
+ """Second s: border flips back to triage and the signals view is rewritten."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
+ app._drain()
|
|
|
+ _ingest_triage_signals(app)
|
|
|
+ app._render()
|
|
|
+ await pilot.pause()
|
|
|
+
|
|
|
+ await pilot.press("s")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["mid_title"] = app.query_one("#triage", RichLog).border_title
|
|
|
+
|
|
|
+ await pilot.press("s")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["triage_title"] = app.query_one("#triage", RichLog).border_title
|
|
|
+ observed["triage_lines"] = _rendered_lines(app.query_one("#triage", RichLog))
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["mid_title"] == "questions"
|
|
|
+ assert observed["triage_title"] == "triage"
|
|
|
+ rendered = "\n".join(observed["triage_lines"])
|
|
|
+ assert "topic: planning (0.91)" in rendered
|
|
|
+ assert "What is the main theme?" not in rendered
|
|
|
+
|
|
|
+
|
|
|
+def test_s_key_is_noop_when_triage_disabled(server):
|
|
|
+ """triage_enabled=False: action_toggle_questions and s do nothing and never crash."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.action_toggle_questions()
|
|
|
+ await pilot.pause()
|
|
|
+ assert app._show_questions is False
|
|
|
+ assert not app.query("#triage")
|
|
|
+ await pilot.press("s")
|
|
|
+ await pilot.pause()
|
|
|
+ assert app._show_questions is False
|
|
|
+ assert not app.query("#triage")
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+
|
|
|
+
|
|
|
+def test_status_event_questions_populate_questions_view(server):
|
|
|
+ """A status event carrying questions fills state and the toggled pane."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
+ app._drain()
|
|
|
+ observed["before_epoch"] = app.state.questions_epoch
|
|
|
+ app.state.ingest({"type": "status", "questions": _questions_payload()})
|
|
|
+ app._render()
|
|
|
+ await pilot.pause()
|
|
|
+ observed["questions"] = app.state.questions
|
|
|
+ observed["epoch"] = app.state.questions_epoch
|
|
|
+
|
|
|
+ await pilot.press("s")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["lines"] = _rendered_lines(app.query_one("#triage", RichLog))
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["questions"] == _questions_payload()
|
|
|
+ assert observed["epoch"] == observed["before_epoch"] + 1
|
|
|
+ assert any("topic [choice]" in line for line in observed["lines"])
|