Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed942972f |
@@ -239,6 +239,13 @@ re-anchor its coverage-map rows.
|
|||||||
(respx `httpx.ConnectError` side-effect) is the RED that proves this.
|
(respx `httpx.ConnectError` side-effect) is the RED that proves this.
|
||||||
- **`agents.get(agent_id)`** (SDK `GET /agents/{id}`) is NOT wrapped — ratatoskr has no
|
- **`agents.get(agent_id)`** (SDK `GET /agents/{id}`) is NOT wrapped — ratatoskr has no
|
||||||
`get_agent` consumer; only list/persona_state/define/patch/delete are in coverage.
|
`get_agent` consumer; only list/persona_state/define/patch/delete are in coverage.
|
||||||
|
- **Client-side Tier-3-id PRE on `patch_agent` / `delete_agent`.** Both assert
|
||||||
|
`":" in agent_id` pre-HTTP (a Tier-3 id is always `<user_id>:<agent_name>`, ADR-0019),
|
||||||
|
so a non-colon id fails fast with an `AssertionError` rather than reaching the SDK's
|
||||||
|
route-discriminated 404 → `Tier3AgentNotFound`. Intentional fail-fast on a
|
||||||
|
wrong-shaped id (carried over from the retired hand-rolled wrappers); documented here
|
||||||
|
per the heid-code-review slice-4 precision flag (the § Error map 404 rows assume a
|
||||||
|
well-formed Tier-3 id reaches the route).
|
||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability"
|
description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -448,6 +448,15 @@ async def _persona_state_endpoint(request: Request) -> JSONResponse:
|
|||||||
return JSONResponse({"error_code": "agent_not_available"}, status_code=404)
|
return JSONResponse({"error_code": "agent_not_available"}, status_code=404)
|
||||||
except AuthScopeDenied:
|
except AuthScopeDenied:
|
||||||
return JSONResponse({"error_code": "auth_scope_denied"}, status_code=403)
|
return JSONResponse({"error_code": "auth_scope_denied"}, status_code=403)
|
||||||
|
except wt.SessionApiFailed as exc:
|
||||||
|
# An unmatched upstream ApiError (a 500, or a coded-but-unmapped 4xx) →
|
||||||
|
# controlled envelope, for parity with _agents_endpoint / create / admin
|
||||||
|
# (heid-code-review slice-4: the persona endpoint was the lone sibling that
|
||||||
|
# let it escape as a raw 500 — a latent pre-cutover gap, closed here).
|
||||||
|
return JSONResponse(
|
||||||
|
{"error_code": "session_api_failed", "status": exc.status},
|
||||||
|
status_code=exc.status,
|
||||||
|
)
|
||||||
except (httpx.RequestError, ConnectFailed) as exc:
|
except (httpx.RequestError, ConnectFailed) as exc:
|
||||||
# SDK normalizes a transport failure to ConnectFailed(status=0) (slice-3
|
# SDK normalizes a transport failure to ConnectFailed(status=0) (slice-3
|
||||||
# foot-gun); surface the network envelope rather than a 500 crash.
|
# foot-gun); surface the network envelope rather than a 500 crash.
|
||||||
|
|||||||
@@ -290,6 +290,20 @@ class TestPersonaStateEndpoint:
|
|||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
assert resp.json()["error_code"] == "auth_scope_denied"
|
assert resp.json()["error_code"] == "auth_scope_denied"
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
def test_unmatched_error_maps_to_session_api_failed(self) -> None:
|
||||||
|
"""unmatched_error [error]: an upstream 500 (unmapped ApiError) → the
|
||||||
|
session_api_failed envelope carrying the upstream status, for parity with
|
||||||
|
_agents_endpoint (heid-code-review slice-4 fixup — was a raw 500 escape)."""
|
||||||
|
respx.get("https://w.example/agents/mimir/persona_state").mock(
|
||||||
|
return_value=httpx.Response(500, content=b"upstream out")
|
||||||
|
)
|
||||||
|
from ratatoskr.web.server import create_app
|
||||||
|
app = create_app(_mock_client_factory())
|
||||||
|
resp = TestClient(app).get("/api/agents/mimir/persona_state")
|
||||||
|
assert resp.status_code == 500
|
||||||
|
assert resp.json()["error_code"] == "session_api_failed"
|
||||||
|
|
||||||
|
|
||||||
class TestSubmitTurnEndpoint:
|
class TestSubmitTurnEndpoint:
|
||||||
"""submit_turn_endpoint FN — allocate turn_id, register in turn_registry."""
|
"""submit_turn_endpoint FN — allocate turn_id, register in turn_registry."""
|
||||||
|
|||||||
@@ -695,6 +695,31 @@ class TestDefineAgent:
|
|||||||
await define_agent(_wta(fake), agent_name="wizard", system_prompt="x", role="m")
|
await define_agent(_wta(fake), agent_name="wizard", system_prompt="x", role="m")
|
||||||
assert ei.value.field == "persona"
|
assert ei.value.field == "persona"
|
||||||
|
|
||||||
|
async def test_layer_deferred_flat_field_body(self) -> None:
|
||||||
|
# _error_field_from_body also handles a flat top-level `field` (both-shape
|
||||||
|
# unwrap) — locks the contract's "detail.field / flat field" claim.
|
||||||
|
fake = _FakeAgents(error=ApiError(
|
||||||
|
"layer_deferred", "no", status=422, body='{"field": "valence"}',
|
||||||
|
))
|
||||||
|
with pytest.raises(Tier3LayerDeferred) as ei:
|
||||||
|
await define_agent(_wta(fake), agent_name="wizard", system_prompt="x", role="m")
|
||||||
|
assert ei.value.field == "valence"
|
||||||
|
|
||||||
|
async def test_403_wrong_code_maps_to_default(self) -> None:
|
||||||
|
# Dual-key negative: a 403 whose code is NOT tier3_user_id_unsupported →
|
||||||
|
# generic default, not a spurious Tier3UserIdUnsupported (INV-CUT-2).
|
||||||
|
fake = _FakeAgents(error=ApiError("auth_revoked", "no", status=403))
|
||||||
|
with pytest.raises(SessionApiFailed) as ei:
|
||||||
|
await define_agent(_wta(fake), agent_name="wizard", system_prompt="x", role="m")
|
||||||
|
assert ei.value.status == 403
|
||||||
|
|
||||||
|
async def test_422_wrong_code_maps_to_default(self) -> None:
|
||||||
|
# Dual-key negative: a 422 whose code is NOT layer_deferred → default.
|
||||||
|
fake = _FakeAgents(error=ApiError("validation_failed", "no", status=422))
|
||||||
|
with pytest.raises(SessionApiFailed) as ei:
|
||||||
|
await define_agent(_wta(fake), agent_name="wizard", system_prompt="x", role="m")
|
||||||
|
assert ei.value.status == 422
|
||||||
|
|
||||||
async def test_bad_slug_asserts_no_call(self) -> None:
|
async def test_bad_slug_asserts_no_call(self) -> None:
|
||||||
fake = _FakeAgents(result={})
|
fake = _FakeAgents(result={})
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
@@ -761,6 +786,14 @@ class TestPatchAgent:
|
|||||||
await patch_agent(_wta(fake), "ratatoskr:wizard", system_prompt="x")
|
await patch_agent(_wta(fake), "ratatoskr:wizard", system_prompt="x")
|
||||||
assert ei.value.field == "agent_name"
|
assert ei.value.field == "agent_name"
|
||||||
|
|
||||||
|
async def test_422_wrong_code_maps_to_default(self) -> None:
|
||||||
|
# Dual-key negative: a 422 whose code is NOT field_not_mutable → default,
|
||||||
|
# not a spurious Tier3FieldNotMutable (INV-CUT-2).
|
||||||
|
fake = _FakeAgents(error=ApiError("validation_failed", "no", status=422))
|
||||||
|
with pytest.raises(SessionApiFailed) as ei:
|
||||||
|
await patch_agent(_wta(fake), "ratatoskr:wizard", system_prompt="x")
|
||||||
|
assert ei.value.status == 422
|
||||||
|
|
||||||
async def test_no_fields_asserts_no_call(self) -> None:
|
async def test_no_fields_asserts_no_call(self) -> None:
|
||||||
fake = _FakeAgents(result={})
|
fake = _FakeAgents(result={})
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
|
|||||||
Reference in New Issue
Block a user