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
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:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user