from __future__ import annotations from collections.abc import Callable from cleveragents.a2a.events import A2aEventQueue, EventBusBridge from cleveragents.application.services.fix_then_revalidate import ( FixThenRevalidateOrchestrator, ) from cleveragents.application.services.validation_pipeline import ValidationPipeline from cleveragents.infrastructure.events.models import DomainEvent from cleveragents.infrastructure.events.reactive import ReactiveEventBus from cleveragents.infrastructure.events.types import EventType def _event(event_type: EventType = EventType.PLAN_CREATED) -> DomainEvent: return DomainEvent(event_type=event_type) def _pipeline() -> ValidationPipeline: return ValidationPipeline(commands=[], executor=lambda _name, _args: {}) def test_reactive_event_bus_close_completes_and_rejects_later_emit() -> None: bus = ReactiveEventBus() completed: list[bool] = [] bus.stream.subscribe(on_completed=lambda: completed.append(True)) bus.close() assert completed == [True] try: bus.emit(_event()) except RuntimeError: pass else: raise AssertionError("emit after close should raise RuntimeError") def test_reactive_event_bus_context_manager_closes_bus() -> None: bus = ReactiveEventBus() completed: list[bool] = [] bus.stream.subscribe(on_completed=lambda: completed.append(True)) with bus as entered: assert entered is bus assert bus._closed is True assert completed == [True] class _TypedEventBus: def __init__(self) -> None: self.handlers: dict[EventType, list[Callable[[DomainEvent], None]]] = {} def emit(self, event: DomainEvent) -> None: for handler in self.handlers.get(event.event_type, []): handler(event) def subscribe( self, event_type: EventType, handler: Callable[[DomainEvent], None] ) -> None: self.handlers.setdefault(event_type, []).append(handler) def unsubscribe( self, event_type: EventType, handler: Callable[[DomainEvent], None] ) -> bool: handlers = self.handlers.get(event_type, []) try: handlers.remove(handler) except ValueError: return False return True def test_event_bus_bridge_unsubscribes_from_typed_event_bus() -> None: bus = _TypedEventBus() queue = A2aEventQueue() bridge = EventBusBridge(bus, queue) bridge.start() bus.emit(_event()) bridge.stop() bus.emit(_event()) assert len(queue.get_events()) == 1 assert all(not handlers for handlers in bus.handlers.values()) class _DisposableSubscription: def __init__(self) -> None: self.disposed = False def dispose(self) -> None: self.disposed = True class _LegacyEventBus: def __init__(self) -> None: self.callback: Callable[[DomainEvent], None] | None = None self.subscription = _DisposableSubscription() def subscribe(self, callback: Callable[[DomainEvent], None]) -> object: self.callback = callback return self.subscription def test_event_bus_bridge_still_supports_disposable_subscription_bus() -> None: bus = _LegacyEventBus() queue = A2aEventQueue() bridge = EventBusBridge(bus, queue) bridge.start() assert bus.callback is not None bus.callback(_event()) bridge.stop() assert len(queue.get_events()) == 1 assert bus.subscription.disposed is True assert bridge._subscription is None class _EmitOnlyBus: def emit(self, event: DomainEvent) -> None: pass def test_fix_then_revalidate_accepts_emit_only_event_sink() -> None: orchestrator = FixThenRevalidateOrchestrator( validation_pipeline=_pipeline(), event_bus=_EmitOnlyBus(), # type: ignore[arg-type] ) assert orchestrator.event_bus is not None