Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
016defcc01 |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.17.10"
|
version = "0.17.11"
|
||||||
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+32
-1
@@ -38,6 +38,8 @@ from ratatoskr.sessions import (
|
|||||||
AgentNotAvailable,
|
AgentNotAvailable,
|
||||||
AgentNotFound,
|
AgentNotFound,
|
||||||
AuthScopeDenied,
|
AuthScopeDenied,
|
||||||
|
BifrostConsumerKeyMissing,
|
||||||
|
BifrostHandshakeFailed,
|
||||||
PersonaNotConfigured,
|
PersonaNotConfigured,
|
||||||
SessionApiFailed,
|
SessionApiFailed,
|
||||||
create_session,
|
create_session,
|
||||||
@@ -1504,11 +1506,33 @@ async def _resolve_then_run(args: ParsedArgs) -> int:
|
|||||||
assert chosen_agent_id is not None
|
assert chosen_agent_id is not None
|
||||||
try:
|
try:
|
||||||
info = await create_session(
|
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:
|
except AgentNotFound as exc:
|
||||||
sys.stderr.write(f"[agent_not_found] agent_id={exc.agent_id}\n")
|
sys.stderr.write(f"[agent_not_found] agent_id={exc.agent_id}\n")
|
||||||
return 12
|
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:
|
except SessionApiFailed as exc:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
f"[session_api_failed] status={exc.status} body={exc.body!r}\n"
|
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:
|
except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc:
|
||||||
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
|
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
|
||||||
return 21
|
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
|
session_id = info.session_id
|
||||||
agent_id: str | None = info.agent_id
|
agent_id: str | None = info.agent_id
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import respx
|
|||||||
from textual.widgets import RichLog
|
from textual.widgets import RichLog
|
||||||
|
|
||||||
from ratatoskr.cli import ParsedArgs
|
from ratatoskr.cli import ParsedArgs
|
||||||
|
from ratatoskr.sessions import BifrostBinding
|
||||||
from ratatoskr.sse_client import (
|
from ratatoskr.sse_client import (
|
||||||
Cancelled,
|
Cancelled,
|
||||||
Done,
|
Done,
|
||||||
@@ -2849,3 +2850,98 @@ class TestResolveThenRunWithPicker:
|
|||||||
err = capsys.readouterr().err
|
err = capsys.readouterr().err
|
||||||
assert "[no_agents]" in err
|
assert "[no_agents]" in err
|
||||||
assert picker_called is False
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user