feat(tui): implement issue #4 contract via TDD; amend cli for TUI dispatch
47 contract-listed tests authored + GREEN (43 tui + 4 issue-#3 amendments). 164/164 tests GREEN suite-wide; ruff clean. Vertical-slice ordering: _render_event_to_log → _cancel_via_sse → CLI amendments → RatatoskrApp class + on_mount + on_unmount → on_input_submitted → _stream_turn_worker → action_interrupt + action_quit → run_tui. Two in-flight contract amendments caught during TDD: - PRE-002 of run_tui was `(args.session_id is None) != args.new` — backwards (fails when --session is set + new=False). Corrected to `bool(args.session_id) != bool(args.new)`. - RichLog created with markup=False (contract drafted markup=True). Rich interprets `[xxx]` as style markup and strips it, which would break every labeled stderr-style line ([cancel_failed], [done], [error], etc.). The post-Done Markdown rendering still works because rich.markdown.Markdown is a Renderable and doesn't need widget-level markup. Implementation notes: - _stream_turn_worker takes the log widget as a parameter passed from on_input_submitted. Querying #transcript from inside a Textual worker context fails with NoMatches; capturing the reference once at handler-time and threading it through the worker sidesteps the issue. - _spy_writes(monkeypatch) test helper records every RichLog.write call. RichLog's `.lines` Strip buffer isn't populated synchronously after .write() returns, which makes post-app-shutdown inspection unreliable; a write-spy gives deterministic verification. - SIGINT-mid-stream tests use custom httpx.AsyncByteStream subclasses with asyncio.Event gates to make timing deterministic without sleep-based polling — the cancel-respx-mock sets the gate event when its endpoint is observed, releasing the next SSE chunk. - _submit_and_wait test helper needs `await pilot.pause()` BEFORE the polling loop so the Input.Submitted message has a chance to dispatch. Discovered via debug-print trace; tracked in the test helper. CLI amendments (per issue #4 in-place amendment of #3 contract): - ParsedArgs.send_content: str | None (was str) - ParsedArgs.raw: bool added - _parse_args: --send default=None; empty-string still rejected; --raw added - main: branches on args.send_content — None → lazy `from ratatoskr.tui import run_tui` + run_tui(args); else asyncio.run(_amain(args)). Lazy import preserves issue #3 INV-001. Persistent-memory updated per the commit-along rule: tui module landed, recent-decisions entries for #4 (contract + Volva + TDD), next natural moves rotated to Volva code-review + manual smoke against the personal Worldtree (key landed in env.sh per infra-ops's earlier delivery).
This commit is contained in:
+45
-4
@@ -99,6 +99,7 @@ class TestParseArgs:
|
||||
agent_id="mimir",
|
||||
api_key="k",
|
||||
server_url="http://localhost:8000",
|
||||
raw=False,
|
||||
)
|
||||
|
||||
def test_happy_existing_session(self) -> None:
|
||||
@@ -111,6 +112,7 @@ class TestParseArgs:
|
||||
agent_id=None,
|
||||
api_key="k",
|
||||
server_url="http://localhost:8000",
|
||||
raw=False,
|
||||
)
|
||||
|
||||
def test_api_key_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -144,10 +146,23 @@ class TestParseArgs:
|
||||
)
|
||||
assert args.server_url == "flag"
|
||||
|
||||
def test_usage_no_send(self) -> None:
|
||||
"""usage_no_send: missing --send → UsageError (argparse required-flag)."""
|
||||
with pytest.raises(UsageError):
|
||||
_parse_args(["--new", "--agent", "mimir", "--api-key", "k"])
|
||||
def test_no_send_marks_tui_mode(self) -> None:
|
||||
"""no_send_marks_tui_mode: missing --send → send_content=None (TUI marker)."""
|
||||
args = _parse_args(["--new", "--agent", "mimir", "--api-key", "k"])
|
||||
assert args.send_content is None
|
||||
# Other fields still populate normally
|
||||
assert args.new is True
|
||||
assert args.agent_id == "mimir"
|
||||
|
||||
def test_raw_flag_default_false(self) -> None:
|
||||
"""raw_flag_default_false: --raw absent → ParsedArgs.raw == False."""
|
||||
args = _parse_args(["--send", "hi", "--new", "--agent", "m", "--api-key", "k"])
|
||||
assert args.raw is False
|
||||
|
||||
def test_raw_flag_set(self) -> None:
|
||||
"""raw_flag_set: --raw → ParsedArgs.raw == True."""
|
||||
args = _parse_args(["--send", "hi", "--new", "--agent", "m", "--api-key", "k", "--raw"])
|
||||
assert args.raw is True
|
||||
|
||||
def test_usage_both_session_and_new(self) -> None:
|
||||
"""usage_both_session_and_new: --session AND --new → UsageError('mutually exclusive')."""
|
||||
@@ -757,6 +772,7 @@ _PARSED_NEW = ParsedArgs(
|
||||
agent_id="mimir",
|
||||
api_key="k",
|
||||
server_url="https://w.example",
|
||||
raw=False,
|
||||
)
|
||||
_PARSED_EXISTING = ParsedArgs(
|
||||
send_content="hi",
|
||||
@@ -765,6 +781,7 @@ _PARSED_EXISTING = ParsedArgs(
|
||||
agent_id=None,
|
||||
api_key="k",
|
||||
server_url="https://w.example",
|
||||
raw=False,
|
||||
)
|
||||
_CREATE_OK_RESP = {
|
||||
"session_id": "s-new",
|
||||
@@ -990,3 +1007,27 @@ class TestMain:
|
||||
assert amain_calls == []
|
||||
# argparse prints help text to stdout
|
||||
assert "ratatoskr" in capsys.readouterr().out
|
||||
|
||||
def test_no_send_dispatches_to_tui(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""no_send_dispatches_to_tui: --send omitted → main calls run_tui, NOT _amain."""
|
||||
from ratatoskr import tui as tui_mod
|
||||
|
||||
tui_calls: list[ParsedArgs] = []
|
||||
amain_calls: list[int] = []
|
||||
|
||||
def fake_run_tui(args: ParsedArgs) -> int:
|
||||
tui_calls.append(args)
|
||||
return 0
|
||||
|
||||
async def fake_amain(args: ParsedArgs) -> int:
|
||||
amain_calls.append(1)
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(tui_mod, "run_tui", fake_run_tui)
|
||||
monkeypatch.setattr(cli_mod, "_amain", fake_amain)
|
||||
rc = main(["--session", "s-1", "--api-key", "k"])
|
||||
assert rc == 0
|
||||
assert len(tui_calls) == 1
|
||||
assert tui_calls[0].send_content is None
|
||||
assert tui_calls[0].session_id == "s-1"
|
||||
assert amain_calls == []
|
||||
|
||||
Reference in New Issue
Block a user