diff --git a/src/agentevals/api/app.py b/src/agentevals/api/app.py index 7a8cf59..25f827b 100644 --- a/src/agentevals/api/app.py +++ b/src/agentevals/api/app.py @@ -10,7 +10,8 @@ from pathlib import Path from typing import TYPE_CHECKING -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi import WebSocket from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse @@ -91,9 +92,6 @@ def create_app( if trace_manager is None: raise ValueError("enable_streaming requires a trace_manager") - from fastapi import Request as _Request - from fastapi import WebSocket - from .streaming_routes import streaming_router app.include_router(streaming_router, prefix="/api/streaming") @@ -103,7 +101,7 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.app.state.trace_manager.handle_connection(websocket) @app.get("/stream/ui-updates") - async def ui_updates_stream(request: _Request): + async def ui_updates_stream(request: Request): mgr = request.app.state.trace_manager async def event_generator(): diff --git a/tests/test_api.py b/tests/test_api.py index 452d181..bf3abee 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1023,3 +1023,51 @@ def test_load_success(self): assert body["data"]["count"] == 1 assert "sess1" in body["data"]["loadedSessions"] _assert_all_keys_camel(body) + + +# --------------------------------------------------------------------------- +# GET /stream/ui-updates (SSE) +# --------------------------------------------------------------------------- + + +class TestUIUpdatesSSE: + """Regression test for the _Request alias bug. + + With ``from __future__ import annotations`` active, importing + ``Request as _Request`` inside the ``enable_streaming`` block caused + FastAPI to treat ``request`` as a required query parameter, returning + 422 on every SSE connection attempt. The fix moves the import to + module level without an alias. + """ + + def _make_streaming_app(self): + import asyncio + + from agentevals.api.app import create_app + from agentevals.streaming.ws_server import StreamingTraceManager + + mgr = StreamingTraceManager() + # Replace register_sse_client so the queue immediately closes (None sentinel) + # so the streaming response can be read synchronously in tests. + q: asyncio.Queue = asyncio.Queue() + q.put_nowait(None) + mgr.register_sse_client = MagicMock(return_value=q) + mgr.unregister_sse_client = MagicMock() + return create_app(enable_streaming=True, trace_manager=mgr) + + def test_sse_endpoint_returns_200_not_422(self): + """GET /stream/ui-updates must return 200, not 422.""" + app = self._make_streaming_app() + client = TestClient(app) + resp = client.get("/stream/ui-updates") + assert resp.status_code == 200, ( + f"Expected 200 but got {resp.status_code}. " + "A 422 indicates the Request type annotation was not resolved correctly." + ) + + def test_sse_endpoint_content_type(self): + """The response must use the text/event-stream media type.""" + app = self._make_streaming_app() + client = TestClient(app) + resp = client.get("/stream/ui-updates") + assert resp.headers["content-type"].startswith("text/event-stream")