mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[chore] mcp-servers: normalize exception bodies to Envelope + lift task_id/correlation_id on circuit_open (#232 #359 #57)
flow_server/do_server: the non-404 JSON path returned exception-handler bodies raw (dict `error` from roboco/generic/http exception handlers, or a 422 `detail` list) — neither is the Envelope wire format the agent is prompted to trust (string error kind + message + remediate + missing), so on any service/validation failure the agent got no remediate and flailed until the breaker tripped. _normalize_exception_envelope lifts the body into a real Envelope (code -> counted string kind via _classify_dict_error_code, NOT_FOUND -> not_found, message lifted, remediate synthesized, missing=[]; 422 -> incomplete_input with the validation detail preserved). The synthesized envelope still flows through the breaker so a 500/422 storm trips it. _record_and_check_circuit: the circuit_open substitution dropped task_id / correlation_id from the top level (the SDK's envelope omits them); lift them from the original rejection so the agent's envelope contract and ops audit-join of the trip event still work, not just nested in inner. intake_server._post_event: capture the relay response body under `detail` on non-success so the grok intake agent gets the real reason (e.g. 'session not in MegaTask scope' on a 422) instead of an opaque http_422 token with no remediation. TDD red->green; ruff + mypy clean; 157 mcp/SDK-breaker tests pass.
This commit is contained in:
@@ -70,7 +70,49 @@ async def test_post_draft_reports_http_error() -> None:
|
||||
|
||||
async with _client(handler) as client:
|
||||
result = await intake_server.post_draft("s", {}, client=client)
|
||||
assert result == {"error": "http_503"}
|
||||
# A no-body failure still surfaces the status; ``detail`` is None when the
|
||||
# body is absent/unparseable (#57 — the body is now captured, not dropped).
|
||||
assert result == {"error": "http_503", "detail": None}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_event_captures_body_on_non_success() -> None:
|
||||
"""#57: on a non-success response the relay's body carries the real reason
|
||||
(e.g. 'session not in MegaTask scope' on a 422); _post_event must capture it
|
||||
under ``detail`` so the intake agent gets actionable remediation instead of an
|
||||
opaque ``http_422`` token."""
|
||||
|
||||
def handler(_request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(422, json={"detail": "session not in MegaTask scope"})
|
||||
|
||||
async with _client(handler) as client:
|
||||
result = await intake_server.post_draft("s", {}, client=client)
|
||||
assert result["error"] == "http_422"
|
||||
assert result["detail"] == {"detail": "session not in MegaTask scope"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_propose_batch_result_string_includes_relay_detail(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The propose_batch failure string surfaces the captured relay body so the
|
||||
grok intake agent can tell whether to re-shape the draft or surface to the
|
||||
CEO (#57). ``post_batch`` is stubbed to the dict _post_event now returns when
|
||||
the relay rejects with a body."""
|
||||
|
||||
async def _relay_rejected(
|
||||
_session_id: str, _payload: dict[str, Any], **_kw: Any
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"error": "http_422",
|
||||
"detail": {"detail": "session not in MegaTask scope"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(intake_server, "post_batch", _relay_rejected)
|
||||
monkeypatch.setenv("ROBOCO_PROMPTER_SESSION_ID", "sess-1")
|
||||
msg = await intake_server.propose_batch([{"title": "A"}], "MegaTask")
|
||||
assert "Could not submit the MegaTask to the panel" in msg
|
||||
assert "session not in MegaTask scope" in msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user