Files
cleveragents-core/tests/events/test_event_bus_semantics.py
T
CleverAgents Bot 556de5f6f9
CI / load-versions (push) Successful in 16s
CI / push-validation (push) Successful in 24s
CI / lint (push) Successful in 41s
CI / quality (push) Successful in 52s
CI / build (push) Successful in 39s
CI / security (push) Successful in 1m5s
CI / typecheck (push) Successful in 1m16s
CI / helm (push) Successful in 57s
CI / unit_tests (push) Successful in 5m54s
CI / integration_tests (push) Successful in 8m32s
CI / docker (push) Successful in 2m4s
CI / coverage (push) Successful in 11m53s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
fix(events): restore event bus close and bridge semantics
2026-06-18 10:38:44 +00:00

134 lines
3.8 KiB
Python

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