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 # --------------------------------------------------------------------------- # EventBus unsubscribe # --------------------------------------------------------------------------- Scenario: ReactiveEventBus.unsubscribe() removes a handler Given a ReactiveEventBus When I subscribe to "plan.created" events And I unsubscribe from "plan.created" with the same handler Then the handler count for "plan.created" should be 0 Scenario: ReactiveEventBus.unsubscribe() returns True when found Given a ReactiveEventBus When I subscribe to "plan.created" events And I unsubscribe from "plan.created" with the same handler Then unsubscribing should return True Scenario: ReactiveEventBus.unsubscribe() returns False when not found Given a ReactiveEventBus Then unsubscribing a non-existent handler should return False Scenario: ReactiveEventBus.unsubscribe() rejects invalid event_type Given a ReactiveEventBus Then unsubscribing with a non-EventType should raise TypeError Scenario: ReactiveEventBus.unsubscribe() rejects non-callable handler Given a ReactiveEventBus Then unsubscribing with a non-callable handler should raise TypeError Scenario: LoggingEventBus.unsubscribe() removes a handler Given a LoggingEventBus When I subscribe to "decision.created" events And I unsubscribe from "decision.created" with the same handler Then the handler count for "decision.created" should be 0 Scenario: LoggingEventBus.unsubscribe() returns True when found Given a LoggingEventBus When I subscribe to "decision.created" events And I unsubscribe from "decision.created" with the same handler Then unsubscribing should return True Scenario: LoggingEventBus.unsubscribe() returns False when not found Given a LoggingEventBus Then unsubscribing a non-existent handler should return False Scenario: LoggingEventBus.unsubscribe() rejects invalid event_type Given a LoggingEventBus Then unsubscribing with a non-EventType should raise TypeError Scenario: LoggingEventBus.unsubscribe() rejects non-callable handler Given a LoggingEventBus Then unsubscribing with a non-callable handler should raise TypeError Scenario: EventBus Protocol unsubscribe stub is callable Then the EventBus Protocol unsubscribe stub should be callable Scenario: Event bus supports selective unsubscription (only specific handler removed) Given a ReactiveEventBus When I subscribe two handlers to "plan.created" events And I unsubscribe only the first handler from "plan.created" And I emit a "plan.created" DomainEvent Then exactly one handler should have received 1 event # --------------------------------------------------------------------------- # 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