Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2806abac44 | ||
|
|
016defcc01 |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.17.10"
|
version = "0.17.12"
|
||||||
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:
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return 11
|
return 11
|
||||||
server_url = os.environ.get("WORLDTREE_API_URL", "http://localhost:8000")
|
server_url = os.environ.get("WORLDTREE_API_URL", "http://localhost:8000")
|
||||||
end_user_id = os.environ.get("RATATOSKR_END_USER_ID")
|
end_user_id = os.environ.get("RATATOSKR_END_USER_ID")
|
||||||
|
# Issue #17 (web bind split): server-held Bifrost binding config. The browser
|
||||||
|
# selects the plane; the consumer key + visible host live server-side only.
|
||||||
|
bifrost_consumer_key = os.environ.get("RATATOSKR_BIFROST_CONSUMER_KEY")
|
||||||
|
bifrost_visible_host = os.environ.get("RATATOSKR_PROVIDER_VISIBLE_HOST")
|
||||||
|
|
||||||
# INV-001: lazy import. Users without [web] extras get a clean hint
|
# INV-001: lazy import. Users without [web] extras get a clean hint
|
||||||
# instead of a raw ImportError. Scoped narrowly to the OPTIONAL
|
# instead of a raw ImportError. Scoped narrowly to the OPTIONAL
|
||||||
@@ -93,7 +97,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0),
|
timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
app = create_app(client_factory, end_user_id=end_user_id)
|
app = create_app(
|
||||||
|
client_factory,
|
||||||
|
end_user_id=end_user_id,
|
||||||
|
bifrost_consumer_key=bifrost_consumer_key,
|
||||||
|
bifrost_visible_host=bifrost_visible_host,
|
||||||
|
)
|
||||||
|
|
||||||
# Boot banner to stderr (so stdout stays clean for piping).
|
# Boot banner to stderr (so stdout stays clean for piping).
|
||||||
version = _pkg_version("ratatoskr")
|
version = _pkg_version("ratatoskr")
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ from ratatoskr.sessions import (
|
|||||||
AgentNotAvailable,
|
AgentNotAvailable,
|
||||||
AgentNotFound,
|
AgentNotFound,
|
||||||
AuthScopeDenied,
|
AuthScopeDenied,
|
||||||
|
BifrostBinding,
|
||||||
|
BifrostConsumerKeyMissing,
|
||||||
|
BifrostHandshakeFailed,
|
||||||
PersonaNotConfigured,
|
PersonaNotConfigured,
|
||||||
SessionApiFailed,
|
SessionApiFailed,
|
||||||
create_session,
|
create_session,
|
||||||
|
endpoint_for_plane,
|
||||||
get_persona_state,
|
get_persona_state,
|
||||||
list_agents,
|
list_agents,
|
||||||
)
|
)
|
||||||
@@ -125,17 +129,65 @@ async def _create_session_endpoint(request: Request) -> JSONResponse:
|
|||||||
return JSONResponse({"error_code": "missing_agent_id"}, status_code=400)
|
return JSONResponse({"error_code": "missing_agent_id"}, status_code=400)
|
||||||
end_user_id = request.app.state.end_user_id
|
end_user_id = request.app.state.end_user_id
|
||||||
client_factory = request.app.state.client_factory
|
client_factory = request.app.state.client_factory
|
||||||
|
|
||||||
|
# Issue #17 (web bind split): the browser may select a PLANE; the server holds
|
||||||
|
# the consumer key + visible host and constructs the binding. The consumer key
|
||||||
|
# NEVER reaches the browser (INV-008/INV-009).
|
||||||
|
bifrost: BifrostBinding | None = None
|
||||||
|
bifrost_plane = body.get("bifrost_plane") if isinstance(body, dict) else None
|
||||||
|
consumer_key = request.app.state.bifrost_consumer_key
|
||||||
|
visible_host = request.app.state.bifrost_visible_host
|
||||||
|
if bifrost_plane:
|
||||||
|
if bifrost_plane not in ("memory", "affect"):
|
||||||
|
return JSONResponse(
|
||||||
|
{"error_code": "invalid_bifrost_plane"}, status_code=400
|
||||||
|
)
|
||||||
|
if not (consumer_key and visible_host):
|
||||||
|
return JSONResponse(
|
||||||
|
{"error_code": "bifrost_not_configured"}, status_code=400
|
||||||
|
)
|
||||||
|
bifrost = BifrostBinding(
|
||||||
|
endpoint_url=endpoint_for_plane(bifrost_plane, visible_host)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with client_factory() as client:
|
async with client_factory() as client:
|
||||||
info = await create_session(client, agent_id, end_user_id=end_user_id)
|
info = await create_session(
|
||||||
|
client,
|
||||||
|
agent_id,
|
||||||
|
end_user_id=end_user_id,
|
||||||
|
bifrost=bifrost,
|
||||||
|
consumer_key=consumer_key if bifrost else None,
|
||||||
|
)
|
||||||
except AgentNotFound:
|
except AgentNotFound:
|
||||||
return JSONResponse({"error_code": "agent_not_found"}, status_code=404)
|
return JSONResponse({"error_code": "agent_not_found"}, status_code=404)
|
||||||
|
except BifrostConsumerKeyMissing:
|
||||||
|
# Server misconfiguration: a plane was requested but no consumer key.
|
||||||
|
return JSONResponse(
|
||||||
|
{"error_code": "bifrost_not_configured"}, status_code=400
|
||||||
|
)
|
||||||
|
except BifrostHandshakeFailed as exc:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"error_code": "bifrost_handshake_failed",
|
||||||
|
"bifrost_error": exc.bifrost_error,
|
||||||
|
},
|
||||||
|
status_code=502,
|
||||||
|
)
|
||||||
except SessionApiFailed as exc:
|
except SessionApiFailed as exc:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error_code": "session_api_failed", "status": exc.status},
|
{"error_code": "session_api_failed", "status": exc.status},
|
||||||
status_code=exc.status,
|
status_code=exc.status,
|
||||||
)
|
)
|
||||||
return JSONResponse(_as_dict(info), status_code=201)
|
payload = _as_dict(info)
|
||||||
|
if bifrost is not None:
|
||||||
|
# Bound-state for the UI indicator — plane + endpoint only, never the key.
|
||||||
|
payload["bifrost"] = {
|
||||||
|
"plane": bifrost_plane,
|
||||||
|
"endpoint": bifrost.endpoint_url,
|
||||||
|
"status": "bound",
|
||||||
|
}
|
||||||
|
return JSONResponse(payload, status_code=201)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -345,6 +397,8 @@ def create_app(
|
|||||||
client_factory: Callable[[], httpx.AsyncClient],
|
client_factory: Callable[[], httpx.AsyncClient],
|
||||||
*,
|
*,
|
||||||
end_user_id: str | None = None,
|
end_user_id: str | None = None,
|
||||||
|
bifrost_consumer_key: str | None = None,
|
||||||
|
bifrost_visible_host: str | None = None,
|
||||||
) -> Starlette:
|
) -> Starlette:
|
||||||
"""Construct the Starlette app — wire routes + state per FN create_app.
|
"""Construct the Starlette app — wire routes + state per FN create_app.
|
||||||
|
|
||||||
@@ -411,6 +465,11 @@ def create_app(
|
|||||||
app = Starlette(routes=routes, lifespan=lifespan)
|
app = Starlette(routes=routes, lifespan=lifespan)
|
||||||
app.state.client_factory = client_factory
|
app.state.client_factory = client_factory
|
||||||
app.state.end_user_id = end_user_id
|
app.state.end_user_id = end_user_id
|
||||||
|
# Issue #17 (web bind split): the consumer key + Worldtree-visible provider
|
||||||
|
# host are SERVER-HELD config (env), never sent from the browser. The browser
|
||||||
|
# selects only the PLANE; the server constructs the bound session (INV-008).
|
||||||
|
app.state.bifrost_consumer_key = bifrost_consumer_key
|
||||||
|
app.state.bifrost_visible_host = bifrost_visible_host
|
||||||
# INV-002: turn registry is in-process memory, keyed (session_id, turn_id)
|
# INV-002: turn registry is in-process memory, keyed (session_id, turn_id)
|
||||||
app.state.turn_registry = {}
|
app.state.turn_registry = {}
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -792,3 +792,113 @@ class TestDisconnectCancel:
|
|||||||
await asyncio.sleep(0.02)
|
await asyncio.sleep(0.02)
|
||||||
gate.set()
|
gate.set()
|
||||||
assert cancel_route.called, "browser disconnect must cancel the UPSTREAM turn (42)"
|
assert cancel_route.called, "browser disconnect must cancel the UPSTREAM turn (42)"
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebBifrostBind:
|
||||||
|
"""Issue #17 slice 3c — web bind split: the browser selects the PLANE; the
|
||||||
|
consumer key + visible host are SERVER-HELD and never reach the browser
|
||||||
|
(INV-008/INV-009)."""
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_bound_create_server_constructs_binding_key_never_leaks(self) -> None:
|
||||||
|
"""tracer: a plane from the browser → the server builds the binding with
|
||||||
|
its OWN consumer key + host, sends the bifrost body + consumer-key bearer
|
||||||
|
upstream, and returns bound-state WITHOUT the key."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
|
||||||
|
route = respx.post("https://w.example/sessions").mock(
|
||||||
|
return_value=httpx.Response(201, json=_CREATE_OK)
|
||||||
|
)
|
||||||
|
app = create_app(
|
||||||
|
_mock_client_factory(),
|
||||||
|
bifrost_consumer_key="server-ck",
|
||||||
|
bifrost_visible_host="10.100.10.50",
|
||||||
|
)
|
||||||
|
resp = TestClient(app).post(
|
||||||
|
"/api/sessions", json={"agent_id": "ratatoskr:sindra", "bifrost_plane": "memory"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
# bound-state echoed for the UI indicator — plane + endpoint, NO key
|
||||||
|
assert resp.json()["bifrost"] == {
|
||||||
|
"plane": "memory",
|
||||||
|
"endpoint": "http://10.100.10.50:8391",
|
||||||
|
"status": "bound",
|
||||||
|
}
|
||||||
|
assert "server-ck" not in resp.text # the key never reaches the browser
|
||||||
|
# upstream got the bifrost body + the consumer-key bearer override
|
||||||
|
upstream = route.calls[0].request
|
||||||
|
body = _json.loads(upstream.content)
|
||||||
|
assert body["bifrost"] == {
|
||||||
|
"endpoint_url": "http://10.100.10.50:8391", "scope": None
|
||||||
|
}
|
||||||
|
assert upstream.headers["Authorization"] == "Bearer server-ck"
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_plane_without_server_config_is_400(self) -> None:
|
||||||
|
"""A plane requested but no server-held key/host → bifrost_not_configured."""
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
|
||||||
|
app = create_app(_mock_client_factory()) # no bifrost config
|
||||||
|
resp = TestClient(app).post(
|
||||||
|
"/api/sessions", json={"agent_id": "a", "bifrost_plane": "memory"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.json()["error_code"] == "bifrost_not_configured"
|
||||||
|
|
||||||
|
def test_invalid_plane_is_400(self) -> None:
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
|
||||||
|
app = create_app(
|
||||||
|
_mock_client_factory(),
|
||||||
|
bifrost_consumer_key="ck",
|
||||||
|
bifrost_visible_host="h",
|
||||||
|
)
|
||||||
|
resp = TestClient(app).post(
|
||||||
|
"/api/sessions", json={"agent_id": "a", "bifrost_plane": "persona"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.json()["error_code"] == "invalid_bifrost_plane"
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_handshake_failure_is_502(self) -> None:
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
|
||||||
|
respx.post("https://w.example/sessions").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
502,
|
||||||
|
json={
|
||||||
|
"error_code": "bifrost_handshake_failed",
|
||||||
|
"detail": {"bifrost_error": "bifrost.auth_rejected"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app = create_app(
|
||||||
|
_mock_client_factory(),
|
||||||
|
bifrost_consumer_key="ck",
|
||||||
|
bifrost_visible_host="h",
|
||||||
|
)
|
||||||
|
resp = TestClient(app).post(
|
||||||
|
"/api/sessions", json={"agent_id": "a", "bifrost_plane": "memory"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 502
|
||||||
|
assert resp.json()["error_code"] == "bifrost_handshake_failed"
|
||||||
|
assert resp.json()["bifrost_error"] == "bifrost.auth_rejected"
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_no_plane_is_unbound_no_bifrost_in_response(self) -> None:
|
||||||
|
"""regression: no bifrost_plane → pre-#17 unbound create, no bifrost key."""
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
|
||||||
|
respx.post("https://w.example/sessions").mock(
|
||||||
|
return_value=httpx.Response(201, json=_CREATE_OK)
|
||||||
|
)
|
||||||
|
app = create_app(
|
||||||
|
_mock_client_factory(),
|
||||||
|
bifrost_consumer_key="ck",
|
||||||
|
bifrost_visible_host="h",
|
||||||
|
)
|
||||||
|
resp = TestClient(app).post("/api/sessions", json={"agent_id": "mimir"})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert "bifrost" not in resp.json()
|
||||||
|
|||||||
Reference in New Issue
Block a user