228 lines
9.9 KiB
Gherkin
228 lines
9.9 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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|