fix(events): wire test mocks + steps for EventBus.unsubscribe
CI / lint (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 1m10s
CI / quality (pull_request) Successful in 49s
CI / build (pull_request) Successful in 58s
CI / helm (pull_request) Successful in 59s
CI / push-validation (pull_request) Successful in 26s
CI / unit_tests (pull_request) Successful in 6m15s
CI / integration_tests (pull_request) Successful in 10m19s
CI / security (pull_request) Failing after 12m42s
CI / coverage (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / status-check (pull_request) Has been cancelled

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).
This commit is contained in:
2026-06-13 14:04:03 -04:00
committed by Forgejo
parent 76360c0e30
commit 1cf41ec6e2
4 changed files with 51 additions and 4 deletions
+29 -4
View File
@@ -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}")
# ---------------------------------------------------------------------------
@@ -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
@@ -118,6 +118,13 @@ class _FailingEventBus:
) -> None:
pass
def unsubscribe(
self,
event_type: EventType,
handler: Any,
) -> None:
pass
# ---------------------------------------------------------------------------
# Given steps
@@ -123,4 +123,5 @@ class LoggingEventBus:
with contextlib.suppress(ValueError):
handlers.remove(handler)
__all__ = ["LoggingEventBus"]