From 1cf41ec6e20dc88ba6a5efd3dadde109e66d6402 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Sat, 13 Jun 2026 14:04:03 -0400 Subject: [PATCH] fix(events): wire test mocks + steps for EventBus.unsubscribe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-05-12 reviewer flagged blockers in the unsubscribe test scaffolding and CI now reproduces them as ruff format mismatches plus six errored BDD scenarios: - features/event_bus.feature:213 "Multiple handlers — unsubscribe removes only the target" referenced an undefined "I unsubscribe the first handler" step, and step_remaining_handlers_received iterated over every collector including the one just unsubscribed (BLOCKER 4). - features/fix_then_revalidate_coverage_boost.feature:{191,201,212,329} and fix_then_revalidate_coverage_r3.feature:49 errored because the _MockEventBus and _FailingEventBus stubs in the step files satisfied the old EventBus Protocol (emit + subscribe) but not the new unsubscribe member, and FixThenRevalidateOrchestrator's runtime isinstance(event_bus, EventBus) check (the EventBus Protocol is @runtime_checkable) rejected them as invalid. - features/steps/event_bus_steps.py and infrastructure/events/logging_bus.py needed `ruff format` cleanup; both were one-line whitespace adjustments. Fixes applied: 1. Add unsubscribe() no-op to the three mock EventBus classes so the runtime protocol check accepts them. 2. Add an explicit "I unsubscribe the first handler" step that records which collector index was removed, plus track last_subscribed_type from the multi-subscribe steps so the unsubscribe step can locate the event_type. 3. Update step_remaining_handlers_received to skip the recorded index when verifying remaining-handler counts. 4. Apply ruff format to the two files CI flagged. Local gates: lint PASS, unit_tests on all three affected feature files PASS (88 scenarios, 349 steps, 0 errors). --- features/steps/event_bus_steps.py | 33 ++++++++++++++++--- ...ix_then_revalidate_coverage_boost_steps.py | 14 ++++++++ .../fix_then_revalidate_coverage_r3_steps.py | 7 ++++ .../infrastructure/events/logging_bus.py | 1 + 4 files changed, 51 insertions(+), 4 deletions(-) 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"]