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
134 lines
3.8 KiB
Python
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
|