Skip to content

Commit 0305b1a

Browse files
Fix sse-starlette event-loop binding in streaming tests (#22)
Root cause: sse_starlette.sse.AppStatus.should_exit_event is a class-level anyio.Event lazily created on first SSE response. It binds to the asyncio event loop that is running at that moment. pytest-asyncio (>=0.21, function-scoped loop default in 1.x) creates a fresh event loop per async test, so the second SSE test inherits an Event bound to a now-closed loop and the SSE response raises: RuntimeError: <asyncio.locks.Event ...> is bound to a different event loop This made 5 streaming tests fail on main: - test_stream_concatenated_content_matches_engine_decode - test_stream_finish_reason_length_on_max_tokens - test_stream_returns_done_sentinel_at_end - test_stream_each_chunk_has_required_openai_fields - test_stream_completion_id_consistent_across_chunks Fix: Add an autouse fixture in tests/inference_engine/server/conftest.py that resets AppStatus.should_exit_event to None before and after every test in this package. Production code is unaffected — uvicorn stays on a single loop for the lifetime of the process so the lazy init runs exactly once there. Result: tests/inference_engine/server/test_app_streaming.py: 11 passed tests/inference_engine/server/ (full suite): 215 passed Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent 9d2c8d6 commit 0305b1a

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

tests/inference_engine/server/conftest.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,51 @@
3030
from inference_engine.server.engine import EngineResult
3131

3232

33+
# ---------------------------------------------------------------------------
34+
# sse-starlette compatibility shim
35+
#
36+
# ``sse_starlette.sse.EventSourceResponse`` lazily creates a class-level
37+
# ``anyio.Event`` (``AppStatus.should_exit_event``) the first time it is
38+
# instantiated. The Event is bound to whichever asyncio event loop is
39+
# running at that moment. pytest-asyncio (>=0.21, with the function-
40+
# scoped event-loop default in 1.x) creates a fresh event loop per
41+
# async test, so the *second* SSE-driven test inherits an Event bound
42+
# to a now-closed loop and the SSE response raises::
43+
#
44+
# RuntimeError: <asyncio.locks.Event ...> is bound to a different event loop
45+
#
46+
# The fix is to reset the class-level cached Event before every test
47+
# that exercises the SSE app. Production code is unaffected — uvicorn
48+
# stays on a single loop for the lifetime of the process and the lazy
49+
# init runs exactly once there.
50+
#
51+
# We make this autouse at the package level so any future SSE test in
52+
# this directory picks the reset up automatically.
53+
# ---------------------------------------------------------------------------
54+
55+
56+
@pytest.fixture(autouse=True)
57+
def _reset_sse_starlette_app_status():
58+
"""Reset ``sse_starlette`` shutdown event between async tests.
59+
60+
Without this, only the first async streaming test in a session
61+
succeeds; subsequent tests fail because the cached anyio.Event is
62+
still bound to the previous, now-closed event loop.
63+
"""
64+
try:
65+
from sse_starlette.sse import AppStatus # type: ignore
66+
except ImportError: # pragma: no cover - sse_starlette is a hard dep
67+
yield
68+
return
69+
AppStatus.should_exit_event = None
70+
AppStatus.should_exit = False
71+
try:
72+
yield
73+
finally:
74+
AppStatus.should_exit_event = None
75+
AppStatus.should_exit = False
76+
77+
3378
class DeterministicTokenizer:
3479
"""Tiny deterministic tokenizer that maps words to integer ids.
3580

0 commit comments

Comments
 (0)