diff --git a/backend/app/routers/sse.py b/backend/app/routers/sse.py index c1b0235..7b17a16 100644 --- a/backend/app/routers/sse.py +++ b/backend/app/routers/sse.py @@ -70,8 +70,9 @@ async def event_generator( "data": json.dumps(event.model_dump()), } except asyncio.TimeoutError: - # No event in KEEPALIVE_TIMEOUT — loop back and wait again. - # sse-starlette's built-in ping handles the actual keepalive. + # Yield an explicit ping so SSE clients see stream liveness + # (in addition to sse-starlette's built-in TCP keepalive). + yield {"event": "ping", "data": ""} continue finally: broker.unsubscribe(session_id, queue) diff --git a/backend/tests/test_sse.py b/backend/tests/test_sse.py index dec757f..738abd7 100644 --- a/backend/tests/test_sse.py +++ b/backend/tests/test_sse.py @@ -37,10 +37,14 @@ def _make_job(session_id: str, *, status: str = "queued", **overrides) -> Job: async def _collect_events(gen, *, count: int = 1, timeout: float = 5.0): """Consume *count* events from an async generator with a safety timeout.""" events = [] - async for event in gen: - events.append(event) - if len(events) >= count: - break + + async def _drain(): + async for event in gen: + events.append(event) + if len(events) >= count: + break + + await asyncio.wait_for(_drain(), timeout=timeout) return events