mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
A standalone, domain-agnostic pub/sub seam: an EventBus that owns its subscriber registry and streams Event values from producers to listeners in process. Boundary-crossing (Celery/DB/workers) is left to subscribers, keeping the bus single-responsibility. Includes the immutable Event value object and full unit coverage.
181 lines
4.5 KiB
Python
181 lines
4.5 KiB
Python
"""``EventBus`` contract: subscribe, publish (stamp + fan out), dispatch.
|
|
|
|
Each test uses a fresh ``EventBus`` — no shared global state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.event_bus import Event, EventBus
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def _event() -> Event:
|
|
return Event(event_type="x.happened", payload={"k": "v"}, search_space_id=1)
|
|
|
|
|
|
async def _noop(_event: Event) -> None:
|
|
return None
|
|
|
|
|
|
async def _other(_event: Event) -> None:
|
|
return None
|
|
|
|
|
|
# --- registry -------------------------------------------------------------
|
|
|
|
|
|
def test_subscribe_then_subscribers_returns_the_handler() -> None:
|
|
bus = EventBus()
|
|
bus.subscribe(_noop)
|
|
|
|
assert _noop in bus.subscribers()
|
|
|
|
|
|
def test_subscribe_is_idempotent_for_the_same_handler() -> None:
|
|
"""Registering the same handler twice must not make it fire twice."""
|
|
bus = EventBus()
|
|
bus.subscribe(_noop)
|
|
bus.subscribe(_noop)
|
|
|
|
assert bus.subscribers().count(_noop) == 1
|
|
|
|
|
|
def test_distinct_handlers_both_register() -> None:
|
|
bus = EventBus()
|
|
bus.subscribe(_noop)
|
|
bus.subscribe(_other)
|
|
|
|
registered = bus.subscribers()
|
|
assert _noop in registered
|
|
assert _other in registered
|
|
|
|
|
|
def test_subscribers_returns_a_defensive_snapshot() -> None:
|
|
"""Mutating the returned list must not corrupt the registry."""
|
|
bus = EventBus()
|
|
bus.subscribe(_noop)
|
|
|
|
snapshot = bus.subscribers()
|
|
snapshot.clear()
|
|
|
|
assert _noop in bus.subscribers()
|
|
|
|
|
|
def test_subscribe_returns_handler_so_it_can_be_used_as_a_decorator() -> None:
|
|
bus = EventBus()
|
|
returned = bus.subscribe(_other)
|
|
|
|
assert returned is _other
|
|
|
|
|
|
def test_two_buses_do_not_share_subscribers() -> None:
|
|
"""The registry is per-instance, not global."""
|
|
a = EventBus()
|
|
b = EventBus()
|
|
a.subscribe(_noop)
|
|
|
|
assert _noop in a.subscribers()
|
|
assert _noop not in b.subscribers()
|
|
|
|
|
|
# --- dispatch -------------------------------------------------------------
|
|
|
|
|
|
async def test_dispatch_delivers_event_to_every_subscriber() -> None:
|
|
bus = EventBus()
|
|
seen: list[tuple[str, Event]] = []
|
|
|
|
async def first(event: Event) -> None:
|
|
seen.append(("first", event))
|
|
|
|
async def second(event: Event) -> None:
|
|
seen.append(("second", event))
|
|
|
|
bus.subscribe(first)
|
|
bus.subscribe(second)
|
|
|
|
event = _event()
|
|
await bus.dispatch(event)
|
|
|
|
assert ("first", event) in seen
|
|
assert ("second", event) in seen
|
|
|
|
|
|
async def test_dispatch_isolates_a_failing_subscriber() -> None:
|
|
"""A subscriber that raises must not stop a healthy one from running."""
|
|
bus = EventBus()
|
|
healthy_ran = False
|
|
|
|
async def boom(_event: Event) -> None:
|
|
raise RuntimeError("subscriber blew up")
|
|
|
|
async def healthy(_event: Event) -> None:
|
|
nonlocal healthy_ran
|
|
healthy_ran = True
|
|
|
|
bus.subscribe(boom)
|
|
bus.subscribe(healthy)
|
|
|
|
await bus.dispatch(_event())
|
|
|
|
assert healthy_ran is True
|
|
|
|
|
|
async def test_dispatch_never_propagates_subscriber_errors() -> None:
|
|
"""``dispatch`` itself must not raise even if every subscriber fails."""
|
|
bus = EventBus()
|
|
|
|
async def boom(_event: Event) -> None:
|
|
raise ValueError("nope")
|
|
|
|
bus.subscribe(boom)
|
|
|
|
await bus.dispatch(_event()) # must not raise
|
|
|
|
|
|
async def test_dispatch_with_no_subscribers_is_a_noop() -> None:
|
|
bus = EventBus()
|
|
await bus.dispatch(_event()) # must not raise
|
|
|
|
|
|
# --- publish --------------------------------------------------------------
|
|
|
|
|
|
async def test_publish_builds_a_stamped_event_and_fans_it_out() -> None:
|
|
bus = EventBus()
|
|
received: list[Event] = []
|
|
|
|
async def handler(event: Event) -> None:
|
|
received.append(event)
|
|
|
|
bus.subscribe(handler)
|
|
await bus.publish("document.indexed", {"document_id": 42}, search_space_id=7)
|
|
|
|
assert len(received) == 1
|
|
event = received[0]
|
|
assert event.event_type == "document.indexed"
|
|
assert event.payload == {"document_id": 42}
|
|
assert event.search_space_id == 7
|
|
# Engine-stamped identity/time on the way through.
|
|
assert event.event_id
|
|
assert event.occurred_at
|
|
|
|
|
|
async def test_publish_defaults_payload_to_empty_dict() -> None:
|
|
bus = EventBus()
|
|
received: list[Event] = []
|
|
|
|
async def handler(event: Event) -> None:
|
|
received.append(event)
|
|
|
|
bus.subscribe(handler)
|
|
await bus.publish("x.happened", search_space_id=1)
|
|
|
|
assert received[0].payload == {}
|
|
|
|
|
|
async def test_publish_with_no_subscribers_is_a_noop() -> None:
|
|
await EventBus().publish("x.happened", search_space_id=1) # must not raise
|