feat(#17): TUI Bifrost-bind trigger (slice 3b of the INV-008 lockstep)

Slice 3b of issue #17 — the TUI surface of the bind trigger (web is 3c). The TUI
consumes the same ParsedArgs the cli already parses (--bifrost-plane / --bifrost-url
/ consumer key from RATATOSKR_BIFROST_CONSUMER_KEY), so this wires the bind into
_resolve_then_run's pre-flight create_session:

- bifrost + consumer_key threaded into create_session at the pre-alt-screen
  resolution layer, so bind failures land on the operator's REAL stderr BEFORE
  the Textual alt-screen opens (INV-002, mirrors issue #6's pre-alt-screen
  routing) — never eaten by the alt-screen teardown.
- BifrostConsumerKeyMissing -> exit 22; BifrostHandshakeFailed -> exit 23 with the
  same 401-scoping hint, keyed on bifrost_error == bifrost.auth_rejected. Exit
  codes + label vocabulary match cli._amain exactly (INV-006).
- Bound-state indicator on success (pre-alt-screen): ". bifrost: status=bound
  plane=... endpoint=...".

3 new TUI bind tests (handshake-fail / consumer-key-missing / bound-create carries
binding + indicator, run_async stubbed). Full suite 465 green; added lines ruff +
mypy clean (pre-existing tui.py lint/type backlog untouched per surgical-changes).
This commit is contained in:
vh
2026-06-18 00:49:30 -07:00
parent 0bebad74ad
commit 016defcc01
4 changed files with 130 additions and 3 deletions
+96
View File
@@ -9,6 +9,7 @@ import respx
from textual.widgets import RichLog
from ratatoskr.cli import ParsedArgs
from ratatoskr.sessions import BifrostBinding
from ratatoskr.sse_client import (
Cancelled,
Done,
@@ -2849,3 +2850,98 @@ class TestResolveThenRunWithPicker:
err = capsys.readouterr().err
assert "[no_agents]" in err
assert picker_called is False
class TestTuiBifrostBind:
"""Issue #17 slice 3b — TUI bind trigger: bind failures route to the real
stderr BEFORE the alt-screen opens (INV-002, mirrors issue #6; same exit
codes/vocabulary as cli._amain per INV-006)."""
@respx.mock
async def test_handshake_failure_routes_pre_altscreen(
self, capsys: pytest.CaptureFixture[str]
) -> None:
from ratatoskr.tui import _resolve_then_run
respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(
502,
json={
"error_code": "bifrost_handshake_failed",
"detail": {"bifrost_error": "bifrost.auth_rejected"},
},
)
)
args = _args_new(
agent_id="ratatoskr:sindra",
bifrost=BifrostBinding(endpoint_url="http://10.100.10.50:8391"),
bifrost_plane="memory",
consumer_key="ck",
)
rc = await _resolve_then_run(args)
assert rc == 23
err = capsys.readouterr().err
assert "bifrost.auth_rejected" in err
assert "consumer key" in err # the 401-scoping hint
@respx.mock
async def test_consumer_key_missing_routes_pre_altscreen(
self, capsys: pytest.CaptureFixture[str]
) -> None:
from ratatoskr.tui import _resolve_then_run
args = _args_new(
agent_id="a",
bifrost=BifrostBinding(endpoint_url="http://x:8391"),
consumer_key=None,
)
rc = await _resolve_then_run(args)
assert rc == 22
assert "bifrost_consumer_key_missing" in capsys.readouterr().err
@respx.mock
async def test_bound_create_carries_binding_and_consumer_key(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""A successful bound create sends the bifrost body + the consumer-key
bearer and prints the bound-state indicator (run_async stubbed so no
alt-screen opens)."""
from ratatoskr import tui as tui_mod
from ratatoskr.tui import _resolve_then_run
route = respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(
201,
json={
"session_id": "s-bound",
"agent_id": "ratatoskr:sindra",
"message_count": 0,
"created_at": "2026-06-18T12:00:00+00:00",
"last_active": "2026-06-18T12:00:00+00:00",
"metadata": {},
},
)
)
async def fake_run_async(self) -> int:
return 0
monkeypatch.setattr(tui_mod.RatatoskrApp, "run_async", fake_run_async)
args = _args_new(
agent_id="ratatoskr:sindra",
bifrost=BifrostBinding(endpoint_url="http://10.100.10.50:8391"),
bifrost_plane="memory",
consumer_key="ck",
)
rc = await _resolve_then_run(args)
assert rc == 0
import json as _json
body = _json.loads(route.calls[0].request.content)
assert body["bifrost"] == {
"endpoint_url": "http://10.100.10.50:8391", "scope": None
}
assert route.calls[0].request.headers["Authorization"] == "Bearer ck"
err = capsys.readouterr().err
assert "bifrost: status=bound" in err
assert "plane=memory" in err