diff --git a/features/steps/event_bus_steps.py b/features/steps/event_bus_steps.py index 51fe6ed33..b18c442ad 100644 --- a/features/steps/event_bus_steps.py +++ b/features/steps/event_bus_steps.py @@ -188,6 +188,7 @@ def step_subscribe_two(ctx: Context, et: str) -> None: collector = _EventCollector() ctx.collectors.append(collector) ctx.bus.subscribe(EventType(et), collector) + ctx.last_subscribed_type = et # type: ignore[attr-defined] @when('I subscribe ten unique handlers to "{et}" events') @@ -196,6 +197,7 @@ def step_subscribe_ten(ctx: Context, et: str) -> None: collector = _EventCollector() ctx.collectors.append(collector) ctx.bus.subscribe(EventType(et), collector) + ctx.last_subscribed_type = et # type: ignore[attr-defined] @when('I emit a "{et}" DomainEvent') @@ -304,6 +306,24 @@ def step_unsubscribe_handler(ctx: Context) -> None: EventType(ctx.last_subscribed_type), # type: ignore[attr-defined] collector, ) + ctx._unsubscribed_index: int = 0 # type: ignore[attr-defined] + + +@when("I unsubscribe the first handler") +def step_unsubscribe_first_handler(ctx: Context) -> None: + """Unsubscribe ``ctx.collectors[0]`` from the most-recently-used event type. + + Mirrors :func:`step_unsubscribe_handler` but reads naturally in scenarios + that subscribe multiple handlers before removing one. + """ + if not ctx.collectors: + raise AssertionError("No collectors registered to unsubscribe") + collector = ctx.collectors[0] + ctx.bus.unsubscribe( # type: ignore[attr-defined] + EventType(ctx.last_subscribed_type), # type: ignore[attr-defined] + collector, + ) + ctx._unsubscribed_index = 0 @when('an unknown handler is unsubscribed for "{et}" events') @@ -340,9 +360,16 @@ def step_unsub_handler_received_zero(ctx: Context) -> None: @then("each remaining handler should have received {n:d} event") def step_remaining_handlers_received(ctx: Context, n: int) -> None: - """Verify that non-unsubscribed handlers still receive events.""" + """Verify that non-unsubscribed handlers still receive events. + + Skips the collector at ``ctx._unsubscribed_index`` (set by an + ``unsubscribe`` step) so we don't check the handler we removed. + """ + skip_idx = getattr(ctx, "_unsubscribed_index", None) all_passed = True for i, col in enumerate(ctx.collectors): + if i == skip_idx: + continue count = len(col.received) if count != n: ctx._handler_failure_count = i @@ -354,9 +381,7 @@ def step_remaining_handlers_received(ctx: Context, n: int) -> None: idx = ctx._handler_failure_count # type: ignore[attr-defined] exp = ctx._handler_failure_expected # type: ignore[attr-defined] got = ctx._handler_failure_actual # type: ignore[attr-defined] - raise AssertionError( - f"Handler {idx} received {got}, expected {exp}" - ) + raise AssertionError(f"Handler {idx} received {got}, expected {exp}") # --------------------------------------------------------------------------- diff --git a/features/steps/fix_then_revalidate_coverage_boost_steps.py b/features/steps/fix_then_revalidate_coverage_boost_steps.py index c900cafa1..e3665be0c 100644 --- a/features/steps/fix_then_revalidate_coverage_boost_steps.py +++ b/features/steps/fix_then_revalidate_coverage_boost_steps.py @@ -547,6 +547,13 @@ class _MockEventBus: ) -> None: pass # Not needed for this test + def unsubscribe( + self, + event_type: EventType, + handler: Any, + ) -> None: + pass # Not needed for this test + class _FailingEventBus: """Mock EventBus whose emit always raises.""" @@ -561,6 +568,13 @@ class _FailingEventBus: ) -> None: pass + def unsubscribe( + self, + event_type: EventType, + handler: Any, + ) -> None: + pass + # --------------------------------------------------------------------------- # Given steps for new review-fix scenarios diff --git a/features/steps/fix_then_revalidate_coverage_r3_steps.py b/features/steps/fix_then_revalidate_coverage_r3_steps.py index d6ec44448..b0a9fb61f 100644 --- a/features/steps/fix_then_revalidate_coverage_r3_steps.py +++ b/features/steps/fix_then_revalidate_coverage_r3_steps.py @@ -118,6 +118,13 @@ class _FailingEventBus: ) -> None: pass + def unsubscribe( + self, + event_type: EventType, + handler: Any, + ) -> None: + pass + # --------------------------------------------------------------------------- # Given steps diff --git a/src/cleveragents/infrastructure/events/logging_bus.py b/src/cleveragents/infrastructure/events/logging_bus.py index 2c6c7a8f2..7911b4e12 100644 --- a/src/cleveragents/infrastructure/events/logging_bus.py +++ b/src/cleveragents/infrastructure/events/logging_bus.py @@ -123,4 +123,5 @@ class LoggingEventBus: with contextlib.suppress(ValueError): handlers.remove(handler) + __all__ = ["LoggingEventBus"]