Files
brent.edwards 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
feat(events): add user_identity field to DomainEvent and propagate through event pipeline (#1257)
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
2026-04-02 16:59:07 +00:00

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