Compare commits

..

1 Commits

Author SHA1 Message Date
vh 016defcc01 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).
2026-06-18 00:49:30 -07:00
4 changed files with 130 additions and 3 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.17.10"
version = "0.17.11"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+32 -1
View File
@@ -38,6 +38,8 @@ from ratatoskr.sessions import (
AgentNotAvailable,
AgentNotFound,
AuthScopeDenied,
BifrostConsumerKeyMissing,
BifrostHandshakeFailed,
PersonaNotConfigured,
SessionApiFailed,
create_session,
@@ -1504,11 +1506,33 @@ async def _resolve_then_run(args: ParsedArgs) -> int:
assert chosen_agent_id is not None
try:
info = await create_session(
client, chosen_agent_id, end_user_id=args.end_user_id
client,
chosen_agent_id,
end_user_id=args.end_user_id,
bifrost=args.bifrost,
consumer_key=args.consumer_key,
)
except AgentNotFound as exc:
sys.stderr.write(f"[agent_not_found] agent_id={exc.agent_id}\n")
return 12
except BifrostConsumerKeyMissing as exc:
# INV-001/INV-002: bind failures land on real stderr BEFORE the
# alt-screen opens (mirrors cli._amain exit codes / vocab, INV-006).
sys.stderr.write(
f"[bifrost_consumer_key_missing] {exc} "
f"(set RATATOSKR_BIFROST_CONSUMER_KEY)\n"
)
return 22
except BifrostHandshakeFailed as exc:
sys.stderr.write(
f"[bifrost_handshake_failed] bifrost_error={exc.bifrost_error}\n"
)
if exc.bifrost_error == "bifrost.auth_rejected":
sys.stderr.write(
" bound create requires the consumer key "
"(RATATOSKR_BIFROST_CONSUMER_KEY), not WORLDTREE_API_KEY\n"
)
return 23
except SessionApiFailed as exc:
sys.stderr.write(
f"[session_api_failed] status={exc.status} body={exc.body!r}\n"
@@ -1517,6 +1541,13 @@ async def _resolve_then_run(args: ParsedArgs) -> int:
except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc:
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
return 21
# Issue #17 bound-state indicator (pre-alt-screen, mirrors cli._amain).
if args.bifrost is not None:
plane = args.bifrost_plane or "direct"
sys.stderr.write(
f". bifrost: status=bound plane={plane} "
f"endpoint={args.bifrost.endpoint_url}\n"
)
session_id = info.session_id
agent_id: str | None = info.agent_id
else:
+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
Generated
+1 -1
View File
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.17.10"
version = "0.17.11"
source = { editable = "." }
dependencies = [
{ name = "httpx" },