e3d3c7926d
CI / integration_tests (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / security (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / docker (push) Has been cancelled
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
297 lines
14 KiB
Gherkin
297 lines
14 KiB
Gherkin
Feature: Event System Domain Event Taxonomy
|
|
As a platform operator
|
|
I want a complete domain event taxonomy with typed event types and a reactive bus
|
|
So that I can audit, observe, and react to all system events consistently
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EventType enum completeness
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: EventType enum has at least 43 members
|
|
Then the EventType enum should have at least 43 members
|
|
|
|
Scenario: EventType covers the Plan lifecycle domain
|
|
Then EventType should contain "plan.created"
|
|
And EventType should contain "plan.phase_changed"
|
|
And EventType should contain "plan.state_changed"
|
|
And EventType should contain "plan.applied"
|
|
And EventType should contain "plan.cancelled"
|
|
And EventType should contain "plan.errored"
|
|
|
|
Scenario: EventType covers the Decision domain
|
|
Then EventType should contain "decision.created"
|
|
And EventType should contain "decision.approved"
|
|
And EventType should contain "decision.corrected"
|
|
And EventType should contain "decision.superseded"
|
|
|
|
Scenario: EventType covers the Invariant domain
|
|
Then EventType should contain "invariant.reconciled"
|
|
And EventType should contain "invariant.violated"
|
|
And EventType should contain "invariant.enforced"
|
|
|
|
Scenario: EventType covers the Actor domain
|
|
Then EventType should contain "actor.invoked"
|
|
And EventType should contain "actor.completed"
|
|
And EventType should contain "actor.errored"
|
|
And EventType should contain "actor.escalated"
|
|
|
|
Scenario: EventType covers the Tool domain
|
|
Then EventType should contain "tool.invoked"
|
|
And EventType should contain "tool.completed"
|
|
And EventType should contain "tool.errored"
|
|
And EventType should contain "tool.retried"
|
|
|
|
Scenario: EventType covers the Resource domain
|
|
Then EventType should contain "resource.accessed"
|
|
And EventType should contain "resource.modified"
|
|
And EventType should contain "resource.indexed"
|
|
|
|
Scenario: EventType covers the Correction domain
|
|
Then EventType should contain "correction.applied"
|
|
|
|
Scenario: EventType covers the Configuration domain
|
|
Then EventType should contain "config.changed"
|
|
|
|
Scenario: EventType covers the Entity lifecycle domain
|
|
Then EventType should contain "entity.deleted"
|
|
|
|
Scenario: EventType covers the Authentication domain
|
|
Then EventType should contain "auth.success"
|
|
And EventType should contain "auth.failure"
|
|
|
|
Scenario: EventType covers the Sandbox domain
|
|
Then EventType should contain "sandbox.created"
|
|
And EventType should contain "sandbox.committed"
|
|
And EventType should contain "sandbox.rolled_back"
|
|
|
|
Scenario: EventType covers the Checkpoint domain
|
|
Then EventType should contain "checkpoint.created"
|
|
And EventType should contain "checkpoint.restored"
|
|
|
|
Scenario: EventType covers the Context domain
|
|
Then EventType should contain "context.built"
|
|
And EventType should contain "context.query_executed"
|
|
|
|
Scenario: EventType covers the Validation domain
|
|
Then EventType should contain "validation.started"
|
|
And EventType should contain "validation.passed"
|
|
And EventType should contain "validation.failed"
|
|
|
|
Scenario: EventType covers the Session domain
|
|
Then EventType should contain "session.created"
|
|
And EventType should contain "session.message_sent"
|
|
|
|
Scenario: EventType covers the Budget domain
|
|
Then EventType should contain "budget.warning"
|
|
And EventType should contain "budget.exceeded"
|
|
|
|
Scenario: EventType uses StrEnum with dot-separated values
|
|
Then every EventType value should match the pattern "<domain>.<action>"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DomainEvent Pydantic model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: DomainEvent has all required fields
|
|
When I create a DomainEvent for taxonomy with event_type "plan.created"
|
|
Then the taxonomy event should have field "event_type"
|
|
And the taxonomy event should have field "timestamp"
|
|
And the taxonomy event should have field "correlation_id"
|
|
And the taxonomy event should have field "plan_id"
|
|
And the taxonomy event should have field "root_plan_id"
|
|
And the taxonomy event should have field "session_id"
|
|
And the taxonomy event should have field "actor_name"
|
|
And the taxonomy event should have field "project_name"
|
|
And the taxonomy event should have field "user_identity"
|
|
And the taxonomy event should have field "details"
|
|
|
|
Scenario: DomainEvent correlation_id is a 26-char ULID string
|
|
When I create a DomainEvent for taxonomy with event_type "plan.created"
|
|
Then the taxonomy event correlation_id should be 26 characters long
|
|
|
|
Scenario: DomainEvent timestamp is timezone-aware UTC
|
|
When I create a DomainEvent for taxonomy with event_type "plan.created"
|
|
Then the taxonomy event timestamp should be UTC
|
|
|
|
Scenario: DomainEvent is frozen (immutable)
|
|
When I create a DomainEvent for taxonomy with event_type "plan.created"
|
|
Then mutating the taxonomy event should raise a validation error
|
|
|
|
Scenario: DomainEvent serializes to JSON round-trip
|
|
When I create a DomainEvent for taxonomy with all fields populated
|
|
Then the taxonomy event should serialize to JSON and deserialize back identically
|
|
|
|
Scenario: DomainEvent correlation fields accept ULID strings
|
|
When I create a DomainEvent for taxonomy with correlation fields
|
|
Then the taxonomy event plan_id should be set
|
|
And the taxonomy event root_plan_id should be set
|
|
And the taxonomy event session_id should be set
|
|
|
|
Scenario: DomainEvent details dict preserves custom keys
|
|
When I create a DomainEvent for taxonomy with typed details
|
|
Then the taxonomy event details should contain the expected keys
|
|
|
|
Scenario: DomainEvent user_identity defaults to None
|
|
When I create a DomainEvent for taxonomy with event_type "plan.created"
|
|
Then the taxonomy event user_identity should be None
|
|
|
|
Scenario: DomainEvent accepts explicit user_identity
|
|
When I create a DomainEvent for taxonomy with user_identity "alice@example.com"
|
|
Then the taxonomy event user_identity should be "alice@example.com"
|
|
|
|
Scenario: DomainEvent with user_identity serializes to JSON round-trip
|
|
When I create a DomainEvent for taxonomy with user_identity "bob@example.com"
|
|
Then the taxonomy event should serialize to JSON and deserialize back identically
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EventBus Protocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: EventBus Protocol defines emit and subscribe
|
|
Then the EventBus Protocol should define method "emit"
|
|
And the EventBus Protocol should define method "subscribe"
|
|
|
|
Scenario: ReactiveEventBus satisfies the EventBus Protocol
|
|
Then a ReactiveEventBus instance should satisfy the EventBus Protocol
|
|
|
|
Scenario: LoggingEventBus satisfies the EventBus Protocol
|
|
Then a LoggingEventBus instance should satisfy the EventBus Protocol
|
|
|
|
Scenario: LoggingEventBus emits to subscribed handlers
|
|
Given a fresh LoggingEventBus for taxonomy tests
|
|
When I logging-taxonomy-subscribe to "plan.created" events
|
|
And I logging-taxonomy-emit a "plan.created" DomainEvent
|
|
Then the logging taxonomy handler should have received 1 event(s)
|
|
|
|
Scenario: LoggingEventBus continues dispatching when a handler raises
|
|
Given a fresh LoggingEventBus for taxonomy tests
|
|
When I logging-taxonomy-subscribe a failing handler and a normal handler to "plan.created" events
|
|
And I logging-taxonomy-emit a "plan.created" DomainEvent
|
|
Then the logging taxonomy normal handler should have received 1 event(s)
|
|
|
|
Scenario: LoggingEventBus rejects non-DomainEvent on emit
|
|
Given a fresh LoggingEventBus for taxonomy tests
|
|
Then logging-taxonomy-emitting a non-DomainEvent should raise TypeError
|
|
|
|
Scenario: LoggingEventBus rejects non-EventType on subscribe
|
|
Given a fresh LoggingEventBus for taxonomy tests
|
|
Then logging-taxonomy-subscribing with a non-EventType should raise TypeError
|
|
|
|
Scenario: LoggingEventBus rejects non-callable handler on subscribe
|
|
Given a fresh LoggingEventBus for taxonomy tests
|
|
Then logging-taxonomy-subscribing with a non-callable should raise TypeError
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ReactiveEventBus — emit / subscribe / stream
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: ReactiveEventBus emits to subscribed handlers
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-subscribe to "plan.created" events
|
|
And I taxonomy-emit a "plan.created" DomainEvent
|
|
Then the taxonomy handler should have received 1 event(s)
|
|
|
|
Scenario: ReactiveEventBus does not dispatch to non-matching types
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-subscribe to "plan.created" events
|
|
And I taxonomy-emit a "decision.created" DomainEvent
|
|
Then the taxonomy handler should have received 0 event(s)
|
|
|
|
Scenario: ReactiveEventBus supports multiple handlers per type
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-subscribe two handlers to "tool.invoked" events
|
|
And I taxonomy-emit a "tool.invoked" DomainEvent
|
|
Then each taxonomy handler should have received 1 event
|
|
|
|
Scenario: ReactiveEventBus continues dispatching when a handler raises
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-subscribe a failing handler and a normal handler to "plan.created" events
|
|
And I taxonomy-emit a "plan.created" DomainEvent
|
|
Then the taxonomy normal handler should have received 1 event(s)
|
|
And the audit_log should contain 1 events
|
|
|
|
Scenario: ReactiveEventBus exposes an rx.Observable stream
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
Then the taxonomy bus stream should be an Observable
|
|
|
|
Scenario: ReactiveEventBus stream is read-only
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
Then the taxonomy bus stream should not expose method "on_next"
|
|
|
|
Scenario: ReactiveEventBus observable stream receives emitted events
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I observe the taxonomy bus stream and emit a "plan.created" event
|
|
Then the stream observer should have captured the event
|
|
|
|
Scenario: ReactiveEventBus continues when a stream observer raises
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-subscribe to "plan.created" events
|
|
And I observe taxonomy stream with a failing observer and emit a "plan.created" event
|
|
Then the taxonomy handler should have received 1 event(s)
|
|
And the audit_log should contain 1 events
|
|
|
|
Scenario: ReactiveEventBus rejects non-DomainEvent on emit
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
Then taxonomy-emitting a non-DomainEvent should raise TypeError
|
|
|
|
Scenario: ReactiveEventBus rejects non-EventType on subscribe
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
Then taxonomy-subscribing with a non-EventType should raise TypeError
|
|
|
|
Scenario: ReactiveEventBus rejects non-callable handler on subscribe
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
Then taxonomy-subscribing with a non-callable should raise TypeError
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# In-memory event trail (audit_log)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: ReactiveEventBus audit_log captures all emitted events
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-emit a "plan.created" DomainEvent
|
|
And I taxonomy-emit a "decision.created" DomainEvent
|
|
Then the audit_log should contain 2 events
|
|
And the audit_log should include event types "plan.created,decision.created"
|
|
|
|
Scenario: ReactiveEventBus audit_log preserves event order
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-emit a "plan.created" DomainEvent
|
|
And I taxonomy-emit a "decision.created" DomainEvent
|
|
And I taxonomy-emit a "tool.invoked" DomainEvent
|
|
Then the audit_log events should be in order "plan.created,decision.created,tool.invoked"
|
|
|
|
Scenario: ReactiveEventBus audit_log returns a defensive copy
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-emit a "plan.created" DomainEvent
|
|
Then mutating the returned audit_log should not affect the bus
|
|
|
|
Scenario: ReactiveEventBus can cap in-memory audit_log retention
|
|
Given a ReactiveEventBus for taxonomy tests with max_audit_log_size 2
|
|
When I taxonomy-emit a "plan.created" DomainEvent
|
|
And I taxonomy-emit a "decision.created" DomainEvent
|
|
And I taxonomy-emit a "tool.invoked" DomainEvent
|
|
Then the audit_log should contain 2 events
|
|
And the audit_log should include event types "decision.created,tool.invoked"
|
|
|
|
Scenario: ReactiveEventBus clear_audit_log removes retained events
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I taxonomy-emit a "plan.created" DomainEvent
|
|
And I clear the taxonomy bus audit_log
|
|
Then the audit_log should contain 0 events
|
|
|
|
Scenario: ReactiveEventBus rejects invalid max_audit_log_size
|
|
Then creating a ReactiveEventBus with max_audit_log_size 0 should raise ValueError
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Log correlation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Scenario: Events sharing a correlation_id are traceable
|
|
Given a fresh ReactiveEventBus for taxonomy tests
|
|
When I emit two taxonomy events with the same correlation_id
|
|
Then both events in the audit_log should share the same correlation_id
|
|
|
|
Scenario: Independent DomainEvents get unique correlation_ids by default
|
|
When I create two independent DomainEvents for taxonomy
|
|
Then the two taxonomy events should have different correlation_ids
|