from __future__ import annotations import asyncio import pytest from yuxi.channels.infra.broadcast import EventBroadcaster class TestEventBroadcaster: @pytest.mark.asyncio async def test_broadcast_delivers_to_subscriber(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("test.event") await broadcaster.broadcast("test.event", {"key": "value"}) received = await asyncio.wait_for(queue.get(), timeout=1.0) assert received == {"event": "test.event", "payload": {"key": "value"}} @pytest.mark.asyncio async def test_broadcast_does_not_deliver_to_other_events(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("event.a") await broadcaster.broadcast("event.b", {"key": "value"}) with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(queue.get(), timeout=0.1) @pytest.mark.asyncio async def test_wildcard_subscriber_receives_all(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("*") await broadcaster.broadcast("any.event", {"key": "value"}) received = await asyncio.wait_for(queue.get(), timeout=1.0) assert received == {"event": "any.event", "payload": {"key": "value"}} @pytest.mark.asyncio async def test_unsubscribe_stops_delivery(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("test.event") broadcaster.unsubscribe("test.event", queue) await broadcaster.broadcast("test.event", {}) with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(queue.get(), timeout=0.1) @pytest.mark.asyncio async def test_unsubscribe_nonexistent_does_not_raise(self): broadcaster = EventBroadcaster() queue: asyncio.Queue = asyncio.Queue() broadcaster.unsubscribe("nonexistent", queue) @pytest.mark.asyncio async def test_node_send_to_session(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("session:sess-1") await broadcaster.node_send_to_session("sess-1", "custom.event", {"data": 1}) received = await asyncio.wait_for(queue.get(), timeout=1.0) assert received == {"event": "custom.event", "payload": {"data": 1}} @pytest.mark.asyncio async def test_multiple_subscribers_same_event(self): broadcaster = EventBroadcaster() q1 = broadcaster.subscribe("multi") q2 = broadcaster.subscribe("multi") await broadcaster.broadcast("multi", {"x": 1}) r1 = await asyncio.wait_for(q1.get(), timeout=1.0) r2 = await asyncio.wait_for(q2.get(), timeout=1.0) assert r1["payload"] == {"x": 1} assert r2["payload"] == {"x": 1} def test_subscriber_count(self): broadcaster = EventBroadcaster() assert broadcaster.subscriber_count() == 0 broadcaster.subscribe("e1") broadcaster.subscribe("e1") broadcaster.subscribe("e2") assert broadcaster.subscriber_count("e1") == 2 assert broadcaster.subscriber_count("e2") == 1 assert broadcaster.subscriber_count() == 3 @pytest.mark.asyncio async def test_callback_subscription_receives_broadcast(self): broadcaster = EventBroadcaster() received = [] def handler(event, payload): received.append((event, payload)) broadcaster.subscribe_callback("test.event", handler) await broadcaster.broadcast("test.event", {"data": 1}) assert received == [("test.event", {"data": 1})] @pytest.mark.asyncio async def test_async_callback_subscription(self): broadcaster = EventBroadcaster() received = [] async def handler(event, payload): received.append((event, payload)) broadcaster.subscribe_callback("test.event", handler) await broadcaster.broadcast("test.event", {"data": 1}) assert received == [("test.event", {"data": 1})] @pytest.mark.asyncio async def test_wildcard_callback_receives_all(self): broadcaster = EventBroadcaster() received = [] def handler(event, payload): received.append(event) broadcaster.subscribe_callback("*", handler) await broadcaster.broadcast("event.a", {}) await broadcaster.broadcast("event.b", {}) assert received == ["event.a", "event.b"] @pytest.mark.asyncio async def test_unsubscribe_callback_stops_delivery(self): broadcaster = EventBroadcaster() received = [] def handler(event, payload): received.append(event) broadcaster.subscribe_callback("test.event", handler) broadcaster.unsubscribe_callback("test.event", handler) await broadcaster.broadcast("test.event", {}) assert received == [] def test_subscriber_count_includes_callbacks(self): broadcaster = EventBroadcaster() broadcaster.subscribe("e1") broadcaster.subscribe_callback("e1", lambda e, p: None) broadcaster.subscribe_callback("e2", lambda e, p: None) assert broadcaster.subscriber_count("e1") == 2 assert broadcaster.subscriber_count("e2") == 1 assert broadcaster.subscriber_count() == 3 @pytest.mark.asyncio async def test_callback_error_does_not_block_broadcast(self): broadcaster = EventBroadcaster() received = [] def good_handler(event, payload): received.append(event) def bad_handler(event, payload): raise RuntimeError("boom") broadcaster.subscribe_callback("test.event", bad_handler) broadcaster.subscribe_callback("test.event", good_handler) await broadcaster.broadcast("test.event", {}) assert received == ["test.event"] @pytest.mark.asyncio async def test_broadcast_without_payload(self): broadcaster = EventBroadcaster() queue = broadcaster.subscribe("test.event") await broadcaster.broadcast("test.event") received = await asyncio.wait_for(queue.get(), timeout=1.0) assert received == {"event": "test.event", "payload": None}