|
@@ -677,6 +677,13 @@ def test_triage_enabled_layout_proportions(server):
|
|
|
async def scenario(size=None):
|
|
async def scenario(size=None):
|
|
|
kwargs = {"size": size} if size else {}
|
|
kwargs = {"size": size} if size else {}
|
|
|
async with app.run_test(**kwargs) as pilot:
|
|
async with app.run_test(**kwargs) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ # Chat needs content to be shown (hidden-by-default rule): put it in
|
|
|
|
|
+ # chat mode so the 1/3-2/3 split applies.
|
|
|
|
|
+ app.state.ingest({"type": "mode", "mode": "chat"})
|
|
|
|
|
+ app._render()
|
|
|
await pilot.pause()
|
|
await pilot.pause()
|
|
|
main = app.query_one("#main", Horizontal)
|
|
main = app.query_one("#main", Horizontal)
|
|
|
transcript = app.query_one("#transcript", RichLog)
|
|
transcript = app.query_one("#transcript", RichLog)
|
|
@@ -911,7 +918,7 @@ def test_maximize_triage_hides_chat_and_fills_screen(server):
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_maximize_toggle_restores_layout(server):
|
|
def test_maximize_toggle_restores_layout(server):
|
|
|
- """Press m again: minimize restores both panes."""
|
|
|
|
|
|
|
+ """Press m again: minimize restores panes; chat follows the content rule (hidden)."""
|
|
|
pytest.importorskip("textual")
|
|
pytest.importorskip("textual")
|
|
|
from textual.widgets import RichLog
|
|
from textual.widgets import RichLog
|
|
|
|
|
|
|
@@ -937,7 +944,8 @@ def test_maximize_toggle_restores_layout(server):
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
assert observed["maximized"] is None
|
|
assert observed["maximized"] is None
|
|
|
assert observed["triage_display"] is True
|
|
assert observed["triage_display"] is True
|
|
|
- assert observed["chat_display"] is True
|
|
|
|
|
|
|
+ # No chat/command output ingested: chat follows the content rule → hidden.
|
|
|
|
|
+ assert observed["chat_display"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_maximize_transcript_fills_screen(server):
|
|
def test_maximize_transcript_fills_screen(server):
|
|
@@ -1003,7 +1011,7 @@ def test_escape_while_maximized_restores_panes(server):
|
|
|
for _ in range(10):
|
|
for _ in range(10):
|
|
|
await pilot.pause()
|
|
await pilot.pause()
|
|
|
await asyncio.sleep(0.05)
|
|
await asyncio.sleep(0.05)
|
|
|
- if app.screen.maximized is None and app.query_one("#chat", RichLog).display:
|
|
|
|
|
|
|
+ if app.screen.maximized is None:
|
|
|
break
|
|
break
|
|
|
observed["maximized"] = app.screen.maximized
|
|
observed["maximized"] = app.screen.maximized
|
|
|
observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
@@ -1012,7 +1020,8 @@ def test_escape_while_maximized_restores_panes(server):
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
assert observed["maximized"] is None
|
|
assert observed["maximized"] is None
|
|
|
assert observed["triage_display"] is True
|
|
assert observed["triage_display"] is True
|
|
|
- assert observed["chat_display"] is True
|
|
|
|
|
|
|
+ # No chat/command output ingested: chat follows the content rule → hidden.
|
|
|
|
|
+ assert observed["chat_display"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_maximize_chat_when_triage_disabled(server):
|
|
def test_maximize_chat_when_triage_disabled(server):
|
|
@@ -1280,7 +1289,8 @@ def test_plain_m_maximizes_triage_pane(server):
|
|
|
await pilot.pause()
|
|
await pilot.pause()
|
|
|
assert app.screen.maximized is None
|
|
assert app.screen.maximized is None
|
|
|
assert app.query_one("#triage", RichLog).display is True
|
|
assert app.query_one("#triage", RichLog).display is True
|
|
|
- assert app.query_one("#chat", RichLog).display is True
|
|
|
|
|
|
|
+ # No chat/command output ingested: chat follows the content rule → hidden.
|
|
|
|
|
+ assert app.query_one("#chat", RichLog).display is False
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
|
|
|
|
@@ -1310,3 +1320,272 @@ def test_screenshot_saves_svg(server, tmp_path, monkeypatch):
|
|
|
assert os.path.getsize(files[0]) > 0
|
|
assert os.path.getsize(files[0]) > 0
|
|
|
assert glob.glob(str(tmp_path / "stt-tui-*.svg")) == [], \
|
|
assert glob.glob(str(tmp_path / "stt-tui-*.svg")) == [], \
|
|
|
"screenshot leaked into %s instead of screenshots/" % tmp_path
|
|
"screenshot leaked into %s instead of screenshots/" % tmp_path
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_chat_hidden_by_default_triage_fills_main(server):
|
|
|
|
|
+ """Fresh state: #chat hidden, #triage fills the whole #main width."""
|
|
|
|
|
+ pytest.importorskip("textual")
|
|
|
|
|
+ from textual.containers import Horizontal
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+ observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
|
|
|
+ observed["main_region"] = app.query_one("#main", Horizontal).region
|
|
|
|
|
+ observed["triage_region"] = app.query_one("#triage", RichLog).region
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ print("hidden: main=%s triage=%s" % (observed["main_region"], observed["triage_region"]))
|
|
|
|
|
+ assert observed["chat_display"] is False
|
|
|
|
|
+ assert observed["triage_display"] is True
|
|
|
|
|
+ assert (
|
|
|
|
|
+ abs(observed["triage_region"].width - observed["main_region"].width) <= 2
|
|
|
|
|
+ ), (observed["triage_region"], observed["main_region"])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_chat_mode_shows_chat_and_restores_split(server):
|
|
|
|
|
+ """mode=chat → #chat visible, triage back to ~1/3 width."""
|
|
|
|
|
+ pytest.importorskip("textual")
|
|
|
|
|
+ from textual.containers import Horizontal
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest({"type": "mode", "mode": "chat"})
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+ observed["main_region"] = app.query_one("#main", Horizontal).region
|
|
|
|
|
+ observed["triage_region"] = app.query_one("#triage", RichLog).region
|
|
|
|
|
+ observed["chat_region"] = app.query_one("#chat", RichLog).region
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ print(
|
|
|
|
|
+ "shown: main=%s triage=%s chat=%s"
|
|
|
|
|
+ % (observed["main_region"], observed["triage_region"], observed["chat_region"])
|
|
|
|
|
+ )
|
|
|
|
|
+ assert observed["chat_display"] is True
|
|
|
|
|
+ main_w = observed["main_region"].width
|
|
|
|
|
+ assert abs(observed["triage_region"].width / main_w - 1 / 3) < 0.08, observed
|
|
|
|
|
+ assert abs(observed["chat_region"].width / main_w - 2 / 3) < 0.08, observed
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_tool_result_shows_chat(server):
|
|
|
|
|
+ """Command output (tool_result) → #chat visible."""
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest(
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "tool_result",
|
|
|
|
|
+ "tool": "get_datetime",
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "result": "2026-09-21",
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ assert observed["chat_display"] is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_error_event_shows_chat(server):
|
|
|
|
|
+ """An error event → #chat visible."""
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest({"type": "error", "code": "boom", "message": "kaboom"})
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ assert observed["chat_display"] is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_executing_mode_shows_chat(server):
|
|
|
|
|
+ """Assistant executing a command → #chat visible."""
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest({"type": "mode", "mode": "executing"})
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ assert observed["chat_display"] is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_cleared_hides_chat_and_restores_full_width(server):
|
|
|
|
|
+ """cleared event → #chat hidden again, triage full width."""
|
|
|
|
|
+ pytest.importorskip("textual")
|
|
|
|
|
+ from textual.containers import Horizontal
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest(
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "tool_result",
|
|
|
|
|
+ "tool": "get_datetime",
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "result": "2026-09-21",
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ assert app.query_one("#chat", RichLog).display is True
|
|
|
|
|
+ app.state.ingest({"type": "cleared"})
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+ observed["main_region"] = app.query_one("#main", Horizontal).region
|
|
|
|
|
+ observed["triage_region"] = app.query_one("#triage", RichLog).region
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ print(
|
|
|
|
|
+ "cleared: main=%s triage=%s"
|
|
|
|
|
+ % (observed["main_region"], observed["triage_region"])
|
|
|
|
|
+ )
|
|
|
|
|
+ assert observed["chat_display"] is False
|
|
|
|
|
+ assert (
|
|
|
|
|
+ abs(observed["triage_region"].width - observed["main_region"].width) <= 2
|
|
|
|
|
+ ), (observed["triage_region"], observed["main_region"])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_triage_disabled_chat_stays_visible(server):
|
|
|
|
|
+ """Triage disabled: #chat stays visible, no crash on render/restore."""
|
|
|
|
|
+ pytest.importorskip("textual")
|
|
|
|
|
+ from textual.widgets import RichLog
|
|
|
|
|
+
|
|
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
|
|
+
|
|
|
|
|
+ path, _service = server
|
|
|
|
|
+ app = AssistantTUI(path)
|
|
|
|
|
+ observed: dict = {}
|
|
|
|
|
+
|
|
|
|
|
+ async def scenario():
|
|
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+ app._restore_panes() # must not raise with no #triage/#main
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["chat_display_after"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ assert observed["chat_display"] is True
|
|
|
|
|
+ assert observed["chat_display_after"] is True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_maximize_restore_with_chat_content_keeps_chat_visible(server):
|
|
|
|
|
+ """With ingested output, m then m restores with #chat still visible."""
|
|
|
|
|
+ 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(size=(80, 40)) as pilot:
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
|
|
+ app._drain()
|
|
|
|
|
+ app.state.ingest(
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "tool_result",
|
|
|
|
|
+ "tool": "get_datetime",
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "result": "2026-09-21",
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ app._render()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ assert app.query_one("#chat", RichLog).display is True
|
|
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ await pilot.press("m")
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ assert app.screen.maximized is not None
|
|
|
|
|
+ await pilot.press("m")
|
|
|
|
|
+ await pilot.pause()
|
|
|
|
|
+ observed["maximized"] = app.screen.maximized
|
|
|
|
|
+ observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
|
|
+
|
|
|
|
|
+ asyncio.run(scenario())
|
|
|
|
|
+ assert observed["maximized"] is None
|
|
|
|
|
+ assert observed["triage_display"] is True
|
|
|
|
|
+ assert observed["chat_display"] is True
|