Files
cleveragents-core/features/event_bus.feature
T
HAL9000 69c283c5a4
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 41s
CI / push-validation (pull_request) Successful in 36s
CI / lint (pull_request) Failing after 1m34s
CI / build (pull_request) Successful in 1m4s
CI / typecheck (pull_request) Successful in 1m43s
CI / security (pull_request) Successful in 1m49s
CI / quality (pull_request) Successful in 1m53s
CI / e2e_tests (pull_request) Successful in 4m9s
CI / unit_tests (pull_request) Failing after 4m18s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 4m51s
CI / status-check (pull_request) Failing after 3s
fix(events): add close() method to ReactiveEventBus to complete RxPY subject
Address all blocking reviewer feedback on PR #10937:
- Remove accidentally committed add_close_steps.py patch script
- Fix single-quote style in step decorators (ruff format compliance)
- Add _closed flag and is_closed public property to ReactiveEventBus
- Guard emit() against calls after close() - raises RuntimeError
- Add __enter__/__exit__ context manager protocol for automatic cleanup
- Add BDD scenarios: emit-after-close raises RuntimeError, context manager
- Fix step_subscriptions_cleared to use public is_closed property
- Add changelog entry for all changes

ISSUES CLOSED: #10916
2026-05-04 19:57:37 +00:00

197 lines
8.5 KiB
Gherkin

Feature: EventBus protocol and domain event emission
As a domain service
I want to emit typed domain events through an EventBus
So that audit logging, observability, and reactive subscribers work correctly
# ---------------------------------------------------------------------------
# EventType enum
# ---------------------------------------------------------------------------
Scenario: EventType enum covers required lifecycle values
Then EventType should include "plan.created"
And EventType should include "decision.created"
And EventType should include "tool.invoked"
And EventType should include "actor.completed"
And EventType should include "validation.passed"
And EventType should include "budget.exceeded"
Scenario: EventType string values compare equal to their members
Then EventType member PLAN_CREATED should equal string "plan.created"
And EventType member DECISION_CREATED should equal string "decision.created"
# ---------------------------------------------------------------------------
# DomainEvent model
# ---------------------------------------------------------------------------
Scenario: DomainEvent is created with required event_type
When I create a minimal DomainEvent with event_type "plan.created"
Then the DomainEvent event_type should be "plan.created"
And the DomainEvent should have a non-empty correlation_id
And the DomainEvent should have a timestamp
Scenario: DomainEvent defaults are applied for optional fields
When I create a minimal DomainEvent with event_type "decision.created"
Then the DomainEvent plan_id should be None
And the DomainEvent actor_name should be None
And the DomainEvent details should be empty
Scenario: DomainEvent accepts plan_id and details
When I create a DomainEvent with event_type "plan.created" plan_id "01ARZ3NDEKTSV4RRFFQ69G5FAV" and details key "phase" value "strategize"
Then the DomainEvent plan_id should be "01ARZ3NDEKTSV4RRFFQ69G5FAV"
And the DomainEvent details should contain key "phase"
Scenario: DomainEvent is immutable (frozen)
When I create a minimal DomainEvent with event_type "plan.created"
Then modifying the DomainEvent should raise an error
# ---------------------------------------------------------------------------
# EventBus Protocol
# ---------------------------------------------------------------------------
Scenario: ReactiveEventBus satisfies the EventBus protocol
Then ReactiveEventBus should satisfy the EventBus protocol
Scenario: LoggingEventBus satisfies the EventBus protocol
Then LoggingEventBus should satisfy the EventBus protocol
# ---------------------------------------------------------------------------
# ReactiveEventBus
# ---------------------------------------------------------------------------
Scenario: ReactiveEventBus emits events to subscribed handlers
Given a ReactiveEventBus
When I subscribe to "plan.created" events
And I emit a "plan.created" DomainEvent
Then the handler should have received 1 event
And the received event type should be "plan.created"
Scenario: ReactiveEventBus dispatches only to matching event type
Given a ReactiveEventBus
When I subscribe to "plan.created" events
And I emit a "decision.created" DomainEvent
Then the handler should have received 0 event
Scenario: ReactiveEventBus supports multiple handlers per event type
Given a ReactiveEventBus
When I subscribe two handlers to "plan.created" events
And I emit a "plan.created" DomainEvent
Then each handler should have received 1 event
Scenario: ReactiveEventBus exposes an observable stream
Given a ReactiveEventBus
Then the bus should expose an observable stream
Scenario: ReactiveEventBus rejects non-DomainEvent inputs
Given a ReactiveEventBus
Then emitting a non-DomainEvent should raise TypeError
Scenario: ReactiveEventBus rejects invalid event_type in subscribe
Given a ReactiveEventBus
Then subscribing with a non-EventType should raise TypeError
# ---------------------------------------------------------------------------
# LoggingEventBus
# ---------------------------------------------------------------------------
Scenario: LoggingEventBus emits events to subscribed handlers
Given a LoggingEventBus
When I subscribe to "decision.created" events
And I emit a "decision.created" DomainEvent
Then the handler should have received 1 event
Scenario: LoggingEventBus rejects non-DomainEvent inputs
Given a LoggingEventBus
Then emitting a non-DomainEvent should raise TypeError
Scenario: LoggingEventBus rejects invalid event_type in subscribe
Given a LoggingEventBus
Then subscribing with a non-EventType should raise TypeError
Scenario: LoggingEventBus rejects non-callable handler in subscribe
Given a LoggingEventBus
Then subscribing with a non-callable handler should raise TypeError
Scenario: ReactiveEventBus rejects non-callable handler in subscribe
Given a ReactiveEventBus
Then subscribing with a non-callable handler should raise TypeError
Scenario: EventBus Protocol emit and subscribe stubs are executable
Then the EventBus Protocol stubs should be callable
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ReactiveEventBus close() - RxPY subject lifecycle
# ---------------------------------------------------------------------------
Scenario: ReactiveEventBus close marks bus as closed and clears subscriptions
Given a ReactiveEventBus
When I subscribe to "plan.created" events
And I emit a "plan.created" DomainEvent
And I close the ReactiveEventBus
Then the subscribed handler should have received 1 event
And the subscriptions should be cleared
And the bus should be closed
Scenario: ReactiveEventBus close clears the audit log
Given a ReactiveEventBus
When I emit a "plan.created" DomainEvent
And I emit a "decision.created" DomainEvent
And I close the ReactiveEventBus
Then the audit log should be empty
Scenario: ReactiveEventBus emit raises RuntimeError after close
Given a ReactiveEventBus
When I close the ReactiveEventBus
And I emit a "plan.created" DomainEvent after close
Then a RuntimeError should be raised
Scenario: ReactiveEventBus supports context manager protocol
When I use the ReactiveEventBus as a context manager
Then the bus should be closed after the context manager exits
# DecisionService event emission
# ---------------------------------------------------------------------------
Scenario: DecisionService emits DECISION_CREATED when recording a decision
Given a DecisionService with an injected event bus
When I call record_decision
Then the event bus should have received a "decision.created" event
And the emitted event plan_id should match the decision plan_id
Scenario: DecisionService works without an event bus (no-op)
Given a DecisionService without an event bus
When I call record_decision
Then no event bus exception should be raised
# ---------------------------------------------------------------------------
# PlanLifecycleService event emission
# ---------------------------------------------------------------------------
Scenario: PlanLifecycleService emits PLAN_CREATED on use_action
Given a PlanLifecycleService with an injected event bus
When I call use_action to create a plan
Then the event bus should have received a "plan.created" event
Scenario: PlanLifecycleService emits PLAN_PHASE_CHANGED on execute_plan
Given a PlanLifecycleService with an injected event bus and a strategized plan
When I call execute_plan
Then the event bus should have received a "plan.phase_changed" event
Scenario: PlanLifecycleService works without an event bus (no-op)
Given a PlanLifecycleService without an event bus
When I call use_action to create a plan
Then no event bus exception should be raised
# ---------------------------------------------------------------------------
# DI container
# ---------------------------------------------------------------------------
Scenario: Container provides ReactiveEventBus as a Singleton
When I resolve event_bus from the DI container twice
Then both resolutions should return the same instance
Scenario: Container wires event_bus into DecisionService
When I resolve decision_service from the DI container
Then the decision_service should have an event_bus attribute