fix(tui): address Volva code-vs-contract drift (issue #4)
Volva code-review surfaced 8 findings against the TDD-passing
TUI shell. All 8 addressed.
Drift fixes (code):
- Primary: INV-002 + INV-003 require visible Footer-area rendering
of session-identity + Ctrl-C state hint. Implementation stored
the strings in `self.sub_title` (which lands in the Header, not
Footer) and `self.hint` (a plain attribute, never rendered). Fixed
by adding two `Static` widgets (id="identity" and id="hint") in
compose; the `_set_hint()` helper mirrors state into the widget on
every state transition. Same-model TDD missed this because tests
asserted internal state, not visible widget content.
- Reverted `_stream_turn_worker(content, log)` to single-param
`(content)` per the contract FN signature. The widened signature
was a TDD-time workaround for a NoMatches-during-worker
execution; root cause was test timing (added `await pilot.pause()`
before the polling loop in `_submit_and_wait`).
- Restored `exclusive=True` on `self.run_worker(...)` per the
contract STEP 6 spec.
- Added missing `isinstance(args, ParsedArgs)` PRE assertion to
`run_tui`. Required hoisting `from ratatoskr.cli import
ParsedArgs` out of TYPE_CHECKING — runtime import is fine (no
circular dependency: cli lazy-imports tui inside main; tui
imports cli unconditionally at module load).
- Added missing union-type PRE assertion to `_render_event_to_log`.
Contract amendments (precision):
- COMPOSE shape: RichLog `markup=False, highlight=False` (was True,
True). Explanatory comment in-line: bracketed labels like
[cancel_failed] would otherwise be interpreted+stripped as Rich
style spans; the post-Done Markdown rendering still works via
Markdown() Renderable.
- INV-002 reworded: identity rendered via dedicated
Static(id="identity") widget composed adjacent to Footer (Textual's
built-in Footer renders BINDINGS descriptions; a sibling Static
carries custom content in the same visual region).
- on_mount POST-003 amended to allow `agent_id is None` when
--session is used without --agent (matches INV-002 carve-out;
GET /sessions/{id} agent lookup is out of scope for this shell).
- run_tui happy_returns_zero_on_quit test description clarified:
App.run() is sync and can't be driven by Pilot, so run_tui's
wrapping behavior is tested via monkeypatch; the piloted Ctrl-D
exit path is covered separately by TestActionQuit.
Test fixes:
- footer_identity_visible_first_frame, footer_hint_flips_to_cancel,
streaming_first_ctrl_c_cancels: now query the Static(#identity) /
Static(#hint) widgets via `widget.render()` instead of asserting
on `app.sub_title` / `app.hint` internal state. The internal
state still exists (mirror), but the load-bearing assertion is
on visible widget content.
Meta-note from Volva: "TDD pass caught most stream/session/error
mechanics, but tested internal state where the contract required
visible Footer behavior, so same-model TDD would plausibly miss the
primary drift." Calibration shape continues across all four issues:
the post-TDD cross-model review consistently catches assert-boundary
+ observability-shape gaps the test-author's hypotheses don't cover
(#1: 4 findings, #2: 3, #3: 5, #4: 8).
164/164 tests GREEN; ruff clean; both contract drift checks clean.
This commit is contained in:
+18
-9
@@ -306,16 +306,19 @@ class TestAppMount:
|
||||
@respx.mock
|
||||
async def test_footer_identity_visible_first_frame(self) -> None:
|
||||
"""footer_identity_visible_first_frame [trace]: …"""
|
||||
from textual.widgets import Static
|
||||
|
||||
respx.post("https://w.example/sessions").mock(
|
||||
return_value=httpx.Response(201, json=_CREATE_OK_RESP)
|
||||
)
|
||||
app = RatatoskrApp(_args_new())
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Both halves of the identity present
|
||||
assert "mimir" in (app.sub_title or "")
|
||||
assert "·" in (app.sub_title or "")
|
||||
assert app.session_id[-8:] in (app.sub_title or "")
|
||||
identity_widget = app.query_one("#identity", Static)
|
||||
rendered = str(identity_widget.render())
|
||||
assert "mimir" in rendered
|
||||
assert "·" in rendered
|
||||
assert app.session_id[-8:] in rendered
|
||||
|
||||
@respx.mock
|
||||
async def test_client_open_after_mount(self) -> None:
|
||||
@@ -349,7 +352,7 @@ import asyncio # noqa: E402
|
||||
from textual.widgets import Input # noqa: E402
|
||||
|
||||
|
||||
async def _noop_worker(self, content: str, log) -> None:
|
||||
async def _noop_worker(self, content: str) -> None:
|
||||
"""Fake _stream_turn_worker that never completes (lets state stay 'streaming')."""
|
||||
await asyncio.Future() # await forever; cancelled when test exits
|
||||
|
||||
@@ -439,17 +442,20 @@ class TestOnInputSubmitted:
|
||||
async def test_footer_hint_flips_to_cancel(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""footer_hint_flips_to_cancel [trace]: after submit, hint shows 'Ctrl-C to cancel'."""
|
||||
"""footer_hint_flips_to_cancel [trace]: hint widget shows 'Ctrl-C to cancel'."""
|
||||
from textual.widgets import Static
|
||||
|
||||
monkeypatch.setattr(RatatoskrApp, "_stream_turn_worker", _noop_worker)
|
||||
app = RatatoskrApp(_args_existing())
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert app.hint == RatatoskrApp.HINT_IDLE
|
||||
hint_widget = app.query_one("#hint", Static)
|
||||
assert str(hint_widget.render()) == RatatoskrApp.HINT_IDLE
|
||||
inp = app.query_one("#prompt", Input)
|
||||
inp.value = "hi"
|
||||
await inp.action_submit()
|
||||
await pilot.pause()
|
||||
assert app.hint == RatatoskrApp.HINT_STREAMING
|
||||
assert str(hint_widget.render()) == RatatoskrApp.HINT_STREAMING
|
||||
|
||||
|
||||
import json # noqa: E402
|
||||
@@ -798,7 +804,10 @@ class TestActionInterrupt:
|
||||
await pilot.pause(0.02)
|
||||
assert cancel_route.call_count == 1
|
||||
assert app.state == "cancelling"
|
||||
assert app.hint == RatatoskrApp.HINT_CANCELLING
|
||||
from textual.widgets import Static
|
||||
|
||||
hint_widget = app.query_one("#hint", Static)
|
||||
assert str(hint_widget.render()) == RatatoskrApp.HINT_CANCELLING
|
||||
# Release the gate so the stream worker can finish cleanly during teardown
|
||||
gate.set()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user