diff --git a/backend/src/cyclone/pubsub.py b/backend/src/cyclone/pubsub.py index 7047e84..795d228 100644 --- a/backend/src/cyclone/pubsub.py +++ b/backend/src/cyclone/pubsub.py @@ -56,3 +56,15 @@ class EventBus: while True: yield await queue.get() queue.task_done() + + +def get_event_bus() -> EventBus: + """Return the process-wide EventBus attached to the FastAPI app state. + + Late-imports ``cyclone.api`` to avoid a circular import at module + load time (the API module imports pubsub indirectly via lifespan). + Tests stub this via monkeypatch. + """ + from cyclone.api import app # late import to avoid circular + + return app.state.event_bus diff --git a/backend/tests/test_pubsub.py b/backend/tests/test_pubsub.py index 23923b2..5580b07 100644 --- a/backend/tests/test_pubsub.py +++ b/backend/tests/test_pubsub.py @@ -12,7 +12,7 @@ import time import pytest -from cyclone.pubsub import EventBus +from cyclone.pubsub import EventBus, get_event_bus @pytest.mark.asyncio @@ -97,3 +97,13 @@ async def test_multiple_concurrent_subscribers_each_get_all_events(): for _ in range(5): seen.append(await asyncio.wait_for(q.__anext__(), timeout=0.5)) assert [e["i"] for e in seen] == [0, 1, 2, 3, 4] + + +def test_get_event_bus_raises_when_app_state_uninitialized(): + """get_event_bus() late-imports cyclone.api; without a running app, + accessing app.state.event_bus raises AttributeError.""" + # Don't import cyclone.api at module level — that would force a load + # and could mask the real failure. Just call the helper and expect + # the AttributeError to surface from app.state. + with pytest.raises((AttributeError, RuntimeError)): + get_event_bus()