test(agent_session_endpoints): assert v2 error envelope shape in tests

* conftest.py registers register_v2_exception_handlers on the test app
  so the test client sees the same envelope as production.
* Existing test assertions on res.json()["detail"] updated to
  res.json()["error"]["message"] for /v2 endpoints.
* New test_error_envelope.py covers 404 + 422 + 409 status mapping.
This commit is contained in:
Ishaan Jaffer 2026-05-06 16:27:05 -07:00
parent c67ca5b299
commit c958cc3f88
No known key found for this signature in database
4 changed files with 91 additions and 2 deletions

View file

@ -247,6 +247,9 @@ def _build_test_app(
run_router,
session_router,
)
from litellm.proxy.agent_session_endpoints.error_envelope import (
register_v2_exception_handlers,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
app = FastAPI()
@ -254,6 +257,8 @@ def _build_test_app(
app.include_router(session_router)
app.include_router(run_router)
app.include_router(internal_router)
# Mirror the production setup so tests see the same /v2 error envelope.
register_v2_exception_handlers(app)
def _fake_auth() -> UserAPIKeyAuth:
return UserAPIKeyAuth(

View file

@ -0,0 +1,82 @@
"""
Verify the standardized {error: {code, message, status, details?}} envelope
applies to /v2/* endpoints (HTTPException + ValidationError) and that
non-/v2 paths keep FastAPI's default detail shape.
"""
import pytest
def test_404_emits_envelope_for_missing_session(client, noop_provider):
"""A 404 from the v2 surface should be wrapped in the envelope."""
res = client.get(
"/v2/sessions/does-not-exist",
headers={"Authorization": "Bearer k"},
)
# find_unique returns None -> ownership assert raises HTTPException(404).
assert res.status_code == 404
body = res.json()
assert "error" in body and "detail" not in body
assert body["error"]["status"] == 404
assert body["error"]["code"] == "not_found"
assert isinstance(body["error"]["message"], str)
def test_validation_error_wraps_in_envelope(client, noop_provider):
"""A 422 ValidationError from a missing required body field should
arrive as ``{error: {code: validation_error, details: [...]}}``."""
# POST /v2/agents requires `name` + `model`; omit both.
res = client.post(
"/v2/agents",
headers={"Authorization": "Bearer k"},
json={},
)
assert res.status_code == 422
body = res.json()
assert "error" in body and "detail" not in body
assert body["error"]["code"] == "validation_error"
assert body["error"]["status"] == 422
assert isinstance(body["error"].get("details"), list)
# Validation details preserve the per-field {loc, msg, type} shape.
assert any("name" in str(d.get("loc", [])) for d in body["error"]["details"])
def test_http_status_code_to_envelope_code_mapping(client, noop_provider):
"""409 should map to ``conflict``, not a generic name."""
# Create an agent + session, then try to POST with missing agent_id
# path -> simpler: trigger a 409 by creating two runs back-to-back
# on the same session.
agent = client.post(
"/v2/agents",
headers={"Authorization": "Bearer k"},
json={"name": "t", "model": "gpt-4"},
).json()
sess = client.post(
f"/v2/agents/{agent['id']}/sessions",
headers={"Authorization": "Bearer k"},
json={"repos": []},
).json()
sid = sess["id"]
daemon_token = sess["daemon_token"]
client.post(
f"/v2/sessions/{sid}/internal/register",
headers={"Authorization": f"Bearer {daemon_token}"},
json={"vm_id": "i-noop"},
)
# First run: queued.
client.post(
f"/v2/sessions/{sid}/runs",
headers={"Authorization": "Bearer k"},
json={"prompt": {"text": "hi"}},
)
# Second run on same session while first is still queued -> 409 run_busy.
res = client.post(
f"/v2/sessions/{sid}/runs",
headers={"Authorization": "Bearer k"},
json={"prompt": {"text": "again"}},
)
assert res.status_code == 409
body = res.json()
assert body["error"]["status"] == 409
assert body["error"]["code"] == "conflict"
assert "run_busy" in body["error"]["message"]

View file

@ -90,7 +90,8 @@ def test_followup_returns_409_when_active_run_is_not_the_latest(
json={"prompt": {"text": "should be blocked"}},
)
assert res.status_code == 409
assert "run_busy" in res.json()["detail"]
# Error envelope shape: {"error": {"code", "message", ...}}
assert "run_busy" in res.json()["error"]["message"]
def test_followup_normal_path_creates_run_when_no_active_runs(client, noop_provider):

View file

@ -44,7 +44,8 @@ def test_view_only_admin_cannot_create_agent(view_only_admin_client, noop_provid
json={"name": "evil", "model": "gpt-4"},
)
assert res.status_code == 403
assert "view-only" in res.json()["detail"].lower()
# Error envelope: {"error": {"code", "message", "status"}}
assert "view-only" in res.json()["error"]["message"].lower()
def test_view_only_admin_cannot_update_other_tenant_agent(