test_tui_app.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Smoke test: the Textual app mounts, subscribes, and ingests broadcasts."""
  2. import asyncio
  3. import os
  4. import shutil
  5. import tempfile
  6. import pytest
  7. from stt_daemon.assistant.control import ControlServer
  8. from stt_daemon.assistant.service import AssistantService
  9. from stt_daemon.config import Config
  10. class FakeSubscriber:
  11. def events(self):
  12. return iter(())
  13. def close(self):
  14. pass
  15. @pytest.fixture
  16. def server():
  17. directory = tempfile.mkdtemp(prefix="stta-")
  18. path = os.path.join(directory, "c.sock")
  19. cfg = Config()
  20. cfg.assistant.control_socket = path
  21. service = AssistantService(cfg, FakeSubscriber())
  22. control = ControlServer(cfg, service, path=path)
  23. service.set_sinks(emit=control.broadcast, relay=control.relay)
  24. control.start()
  25. yield path, service
  26. control.close()
  27. service.stop()
  28. shutil.rmtree(directory, ignore_errors=True)
  29. def test_enter_sends_execute_and_e_opens_editor(server):
  30. pytest.importorskip("textual")
  31. from stt_daemon.tui.app import AssistantTUI
  32. path, _service = server
  33. app = AssistantTUI(path)
  34. sent: list[tuple] = []
  35. async def scenario():
  36. async with app.run_test() as pilot:
  37. await pilot.pause()
  38. app.client.send = lambda type_, **fields: sent.append((type_, fields))
  39. await pilot.press("enter")
  40. await pilot.pause()
  41. await pilot.press("a")
  42. await pilot.pause()
  43. await pilot.press("escape")
  44. await pilot.pause()
  45. asyncio.run(scenario())
  46. assert ("command_execute", {}) in sent
  47. assert ("command_start", {}) in sent
  48. assert ("command_cancel", {}) in sent
  49. def test_tui_renders_command_output(server):
  50. """A tool result must actually reach a widget - otherwise 'nothing happens'."""
  51. pytest.importorskip("textual")
  52. from textual.widgets import RichLog
  53. from stt_daemon.tui.app import AssistantTUI
  54. path, _service = server
  55. app = AssistantTUI(path)
  56. async def scenario():
  57. async with app.run_test() as pilot:
  58. await pilot.pause()
  59. app.state.ingest(
  60. {
  61. "type": "tool_result",
  62. "tool": "get_datetime",
  63. "ok": True,
  64. "result": "2026-09-21T01:00:00",
  65. }
  66. )
  67. app._render()
  68. await pilot.pause()
  69. log = app.query_one("#chat", RichLog)
  70. return list(log.lines)
  71. lines = asyncio.run(scenario())
  72. assert lines, "command output was not rendered anywhere"
  73. def test_tui_mounts_subscribes_and_ingests_broadcasts(server):
  74. pytest.importorskip("textual")
  75. from stt_daemon.tui.app import AssistantTUI
  76. path, service = server
  77. app = AssistantTUI(path)
  78. async def scenario():
  79. async with app.run_test() as pilot:
  80. await pilot.pause()
  81. service.arm() # broadcast to the subscribed viewer
  82. for _ in range(60):
  83. await pilot.pause()
  84. if app.state.mode == "command_armed":
  85. break
  86. await asyncio.sleep(0.05)
  87. asyncio.run(scenario())
  88. assert app.state.mode == "command_armed"