"""Unit tests for InstanceEventBus.""" import asyncio import uuid from typing import Any import pytest from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload @pytest.fixture def event_bus() -> InstanceEventBus: """Provide a fresh EventBus instance with reset singleton state.""" bus = InstanceEventBus() bus._reset_for_testing() return bus @pytest.fixture def sample_payload() -> InstanceEventPayload: """Provide a sample event payload.""" return { "event": "instance.started", "instance_id": str(uuid.uuid4()), "status": "starting", "message": "Container starting...", "metadata": {}, "timestamp": "2026-05-28T12:00:00Z", "correlation_id": str(uuid.uuid4()), } @pytest.mark.unit async def test_publish_delivers_to_all_subscribers( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """All subscribed callbacks should receive the published payload.""" received: list[Any] = [] def callback_1(payload: InstanceEventPayload) -> None: received.append(("callback_1", payload)) def callback_2(payload: InstanceEventPayload) -> None: received.append(("callback_2", payload)) def callback_3(payload: InstanceEventPayload) -> None: received.append(("callback_3", payload)) event_bus.subscribe("instance.started", callback_1) event_bus.subscribe("instance.started", callback_2) event_bus.subscribe("instance.started", callback_3) await event_bus.publish("instance.started", sample_payload) assert len(received) == 3 assert received[0][0] == "callback_1" assert received[1][0] == "callback_2" assert received[2][0] == "callback_3" @pytest.mark.unit async def test_subscriber_exception_isolation( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """If one subscriber raises, others should still receive the event.""" received: list[str] = [] def bad_callback(_payload: InstanceEventPayload) -> None: raise RuntimeError("boom") def good_callback(_payload: InstanceEventPayload) -> None: received.append("good_callback") event_bus.subscribe("instance.started", bad_callback) event_bus.subscribe("instance.started", good_callback) # Should not raise await event_bus.publish("instance.started", sample_payload) assert received == ["good_callback"] @pytest.mark.unit async def test_unsubscribe_removes_callback( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """After unsubscribing, the callback should not be called.""" received: list[str] = [] def callback(_payload: InstanceEventPayload) -> None: received.append("callback") unsubscribe = event_bus.subscribe("instance.started", callback) unsubscribe() await event_bus.publish("instance.started", sample_payload) assert received == [] @pytest.mark.unit async def test_publish_to_empty_subscriber_list( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """Publishing to an event type with no subscribers should not raise.""" await event_bus.publish("instance.started", sample_payload) @pytest.mark.unit async def test_async_subscriber_supported( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """Async callbacks should be awaited correctly.""" received: list[str] = [] async def async_callback(_payload: InstanceEventPayload) -> None: await asyncio.sleep(0) received.append("async_callback") event_bus.subscribe("instance.started", async_callback) await event_bus.publish("instance.started", sample_payload) assert received == ["async_callback"] @pytest.mark.unit async def test_unsubscribe_all_clears_subscribers( event_bus: InstanceEventBus, sample_payload: InstanceEventPayload, ) -> None: """unsubscribe_all should remove all callbacks for an event type.""" received: list[str] = [] def callback(_payload: InstanceEventPayload) -> None: received.append("callback") event_bus.subscribe("instance.started", callback) event_bus.unsubscribe_all("instance.started") await event_bus.publish("instance.started", sample_payload) assert received == []