00881a3e5f
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3m26s
CI / build (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 4m2s
CI / quality (pull_request) Successful in 4m2s
CI / security (pull_request) Successful in 4m23s
CI / integration_tests (pull_request) Successful in 9m35s
CI / unit_tests (pull_request) Successful in 9m39s
CI / docker (pull_request) Successful in 2m15s
CI / e2e_tests (pull_request) Successful in 12m32s
CI / coverage (pull_request) Successful in 11m41s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 23s
CI / lint (push) Successful in 3m17s
CI / typecheck (push) Successful in 4m11s
CI / quality (push) Successful in 4m11s
CI / security (push) Successful in 4m28s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 9m13s
CI / unit_tests (push) Successful in 9m26s
CI / e2e_tests (push) Successful in 10m20s
CI / docker (push) Successful in 1m16s
CI / coverage (push) Successful in 12m2s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 33m17s
CI / benchmark-regression (pull_request) Successful in 57m23s
Verified and completed the Event System Domain Event Taxonomy implementation. Gap analysis: - EventType enum (38 types across 12 domains): ALREADY EXISTED, verified complete - DomainEvent Pydantic model (all 9 fields): ALREADY EXISTED, verified complete - EventBus Protocol (emit + subscribe): ALREADY EXISTED, verified complete - ReactiveEventBus (RxPY Subject, stream property): ALREADY EXISTED, verified - LoggingEventBus (structlog-based): ALREADY EXISTED, verified complete Added: - In-memory audit_log on ReactiveEventBus (list[DomainEvent] with defensive copy) - Emit ordering aligned with specification: RxPY stream push, then audit append, then handler dispatch (§Event System) - Clarified audit_log docstring: volatile in-memory log, not durable SQLite persistence (durable persistence wired separately via audit service layer) - Behave feature: features/observability/event_system_taxonomy.feature (36 scenarios) - Step definitions: features/steps/event_system_taxonomy_steps.py - Robot integration test: robot/event_system_taxonomy_integration.robot (13 test cases) - Robot helper: robot/helper_event_system_taxonomy.py (13 subcommands) - ASV benchmark: benchmarks/bench_event_bus.py (5 benchmark suites) - vulture_whitelist.py: added audit_log property Code review fixes applied: - Reordered emit() to match spec: on_next → audit_log → handlers (was: audit → on_next → handlers) - Clarified audit_log docstring to distinguish volatile in-memory log from spec SQLite table - Fixed benchmark TaxonomyAuditLogSuite: parameterized setup instead of inline construction - Added teardown to TaxonomyLoggingEmitSuite to restore logging.disable(NOTSET) - Clear _received list between benchmark iterations to reduce noise - Catch specific pydantic.ValidationError in frozen model mutation tests - Consolidated duplicate singular/plural step definitions - Tightened EventType count threshold from >=30 to >=38 Quality gates: - lint: PASSED - typecheck: PASSED (0 errors) - unit_tests: 36/36 new scenarios PASSED (pre-existing 12 flaky failures unchanged) - integration_tests: 1483/1483 PASSED - security_scan: PASSED - dead_code: PASSED ISSUES CLOSED: #587
428 lines
14 KiB
Python
428 lines
14 KiB
Python
"""Helper script for event_system_taxonomy_integration.robot.
|
|
|
|
Each subcommand is a self-contained check that prints a sentinel on success.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from pydantic import ValidationError
|
|
from rx.core.observable.observable import Observable
|
|
from ulid import ULID
|
|
|
|
# Ensure local source tree is importable
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.infrastructure.events import ( # noqa: E402
|
|
DomainEvent,
|
|
EventBus,
|
|
EventType,
|
|
LoggingEventBus,
|
|
ReactiveEventBus,
|
|
)
|
|
|
|
_DOT_PATTERN = re.compile(r"^[a-z_]+\.[a-z_]+$")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def event_type_count() -> None:
|
|
"""Verify EventType has at least 43 members."""
|
|
count = len(EventType)
|
|
if count < 43:
|
|
print(f"FAIL: EventType has {count} members, expected >= 43", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("event-type-count-ok")
|
|
|
|
|
|
def event_type_domains() -> None:
|
|
"""Verify EventType covers all required domains."""
|
|
required_values = [
|
|
"plan.created",
|
|
"decision.created",
|
|
"invariant.reconciled",
|
|
"actor.invoked",
|
|
"tool.invoked",
|
|
"resource.accessed",
|
|
"sandbox.created",
|
|
"checkpoint.created",
|
|
"context.built",
|
|
"validation.started",
|
|
"session.created",
|
|
"budget.warning",
|
|
"correction.applied",
|
|
"config.changed",
|
|
"entity.deleted",
|
|
"auth.success",
|
|
"auth.failure",
|
|
]
|
|
actual = {e.value for e in EventType}
|
|
missing = [v for v in required_values if v not in actual]
|
|
if missing:
|
|
print(f"FAIL: missing domain values: {missing}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("event-type-domains-ok")
|
|
|
|
|
|
def event_type_dot_convention() -> None:
|
|
"""Verify every EventType value matches <domain>.<action>."""
|
|
for member in EventType:
|
|
if not _DOT_PATTERN.match(member.value):
|
|
print(
|
|
f"FAIL: {member.name}={member.value!r} violates convention",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
print("event-type-dot-convention-ok")
|
|
|
|
|
|
def domain_event_fields() -> None:
|
|
"""Verify DomainEvent has all required fields."""
|
|
required = {
|
|
"event_type",
|
|
"timestamp",
|
|
"correlation_id",
|
|
"plan_id",
|
|
"root_plan_id",
|
|
"session_id",
|
|
"actor_name",
|
|
"project_name",
|
|
"details",
|
|
}
|
|
actual = set(DomainEvent.model_fields)
|
|
missing = required - actual
|
|
if missing:
|
|
print(f"FAIL: DomainEvent missing fields: {missing}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("domain-event-fields-ok")
|
|
|
|
|
|
def domain_event_json_roundtrip() -> None:
|
|
"""Serialize and deserialize a fully populated DomainEvent."""
|
|
event = DomainEvent(
|
|
event_type=EventType.PLAN_CREATED,
|
|
plan_id=str(ULID()),
|
|
root_plan_id=str(ULID()),
|
|
session_id=str(ULID()),
|
|
actor_name="local/strategist",
|
|
project_name="test-project",
|
|
details={"phase": "strategize"},
|
|
)
|
|
json_str = event.model_dump_json()
|
|
restored = DomainEvent.model_validate_json(json_str)
|
|
if restored != event:
|
|
print("FAIL: JSON round-trip mismatch", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("domain-event-json-roundtrip-ok")
|
|
|
|
|
|
def domain_event_immutable() -> None:
|
|
"""Verify DomainEvent is frozen."""
|
|
event = DomainEvent(event_type=EventType.PLAN_CREATED)
|
|
try:
|
|
event.event_type = EventType.PLAN_CANCELLED # type: ignore[misc]
|
|
except ValidationError:
|
|
print("domain-event-immutable-ok")
|
|
return
|
|
print("FAIL: mutation was not rejected", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def reactive_emit_subscribe_audit() -> None:
|
|
"""Emit dispatches to handlers and records audit_log event details."""
|
|
received: list[DomainEvent] = []
|
|
bus = ReactiveEventBus()
|
|
bus.subscribe(EventType.PLAN_CREATED, received.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
|
|
if len(received) != 1:
|
|
print(f"FAIL: handler got {len(received)} events", file=sys.stderr)
|
|
sys.exit(1)
|
|
if len(bus.audit_log) != 1:
|
|
print(f"FAIL: audit_log has {len(bus.audit_log)} events", file=sys.stderr)
|
|
sys.exit(1)
|
|
if received[0].event_type != EventType.PLAN_CREATED:
|
|
print("FAIL: handler received wrong event type", file=sys.stderr)
|
|
sys.exit(1)
|
|
if bus.audit_log[0].event_type != EventType.PLAN_CREATED:
|
|
print("FAIL: audit_log captured wrong event type", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("reactive-emit-subscribe-audit-ok")
|
|
|
|
|
|
def reactive_handler_exception_isolation() -> None:
|
|
"""Verify failing handlers do not block ReactiveEventBus dispatch."""
|
|
received: list[DomainEvent] = []
|
|
bus = ReactiveEventBus()
|
|
|
|
def failing_handler(event: DomainEvent) -> None:
|
|
raise RuntimeError(f"intentional failure for {event.event_type.value}")
|
|
|
|
bus.subscribe(EventType.PLAN_CREATED, failing_handler)
|
|
bus.subscribe(EventType.PLAN_CREATED, received.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
|
|
if len(received) != 1:
|
|
print("FAIL: normal handler was not called", file=sys.stderr)
|
|
sys.exit(1)
|
|
if len(bus.audit_log) != 1:
|
|
print("FAIL: audit_log missing emitted event", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("reactive-handler-exception-isolation-ok")
|
|
|
|
|
|
def reactive_stream_exception_isolation() -> None:
|
|
"""Verify failing stream observers do not block bus dispatch."""
|
|
received: list[DomainEvent] = []
|
|
bus = ReactiveEventBus()
|
|
bus.subscribe(EventType.PLAN_CREATED, received.append)
|
|
|
|
def failing_stream_observer(event: DomainEvent) -> None:
|
|
raise RuntimeError(f"intentional stream failure for {event.event_type.value}")
|
|
|
|
bus.stream.subscribe(on_next=failing_stream_observer)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
|
|
if len(received) != 1:
|
|
print("FAIL: stream failure blocked typed handler dispatch", file=sys.stderr)
|
|
sys.exit(1)
|
|
if len(bus.audit_log) != 1:
|
|
print("FAIL: stream failure blocked audit recording", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("reactive-stream-exception-isolation-ok")
|
|
|
|
|
|
def reactive_stream_observable() -> None:
|
|
"""stream property exposes rx.Observable that receives events."""
|
|
bus = ReactiveEventBus()
|
|
if not isinstance(bus.stream, Observable):
|
|
print("FAIL: stream is not Observable", file=sys.stderr)
|
|
sys.exit(1)
|
|
captured: list[DomainEvent] = []
|
|
bus.stream.subscribe(on_next=captured.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
if len(captured) != 1:
|
|
print(f"FAIL: stream captured {len(captured)} events", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("reactive-stream-observable-ok")
|
|
|
|
|
|
def reactive_stream_read_only() -> None:
|
|
"""stream property returns read-only observable view."""
|
|
bus = ReactiveEventBus()
|
|
stream = bus.stream
|
|
|
|
if hasattr(stream, "on_next"):
|
|
print("FAIL: stream unexpectedly exposes on_next", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
captured: list[DomainEvent] = []
|
|
stream.subscribe(on_next=captured.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
if len(captured) != 1:
|
|
print("FAIL: read-only stream did not receive emitted event", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("reactive-stream-read-only-ok")
|
|
|
|
|
|
def audit_log_order() -> None:
|
|
"""audit_log preserves emission order."""
|
|
bus = ReactiveEventBus()
|
|
types = [EventType.PLAN_CREATED, EventType.DECISION_CREATED, EventType.TOOL_INVOKED]
|
|
for et in types:
|
|
bus.emit(DomainEvent(event_type=et))
|
|
log_types = [e.event_type.value for e in bus.audit_log]
|
|
expected = ["plan.created", "decision.created", "tool.invoked"]
|
|
if log_types != expected:
|
|
print(f"FAIL: order {log_types} != {expected}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("audit-log-order-ok")
|
|
|
|
|
|
def audit_log_defensive_copy() -> None:
|
|
"""audit_log returns a copy — external mutation is safe."""
|
|
bus = ReactiveEventBus()
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
log1 = bus.audit_log
|
|
log1.clear()
|
|
if len(bus.audit_log) != 1:
|
|
print("FAIL: audit_log was externally mutated", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("audit-log-defensive-copy-ok")
|
|
|
|
|
|
def audit_log_retention_cap() -> None:
|
|
"""Configured max_audit_log_size evicts oldest events FIFO."""
|
|
bus = ReactiveEventBus(max_audit_log_size=2)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
bus.emit(DomainEvent(event_type=EventType.DECISION_CREATED))
|
|
bus.emit(DomainEvent(event_type=EventType.TOOL_INVOKED))
|
|
|
|
log = bus.audit_log
|
|
if len(log) != 2:
|
|
print(
|
|
f"FAIL: expected capped audit_log size 2, got {len(log)}", file=sys.stderr
|
|
)
|
|
sys.exit(1)
|
|
|
|
actual = [event.event_type.value for event in log]
|
|
expected = ["decision.created", "tool.invoked"]
|
|
if actual != expected:
|
|
print(f"FAIL: capped audit_log order {actual} != {expected}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
bus.clear_audit_log()
|
|
if bus.audit_log:
|
|
print("FAIL: clear_audit_log did not empty audit_log", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print("audit-log-retention-cap-ok")
|
|
|
|
|
|
def audit_log_invalid_retention_cap() -> None:
|
|
"""Invalid retention cap should be rejected."""
|
|
try:
|
|
ReactiveEventBus(max_audit_log_size=0)
|
|
except ValueError:
|
|
print("audit-log-invalid-retention-cap-ok")
|
|
return
|
|
print("FAIL: expected ValueError for max_audit_log_size=0", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def log_correlation() -> None:
|
|
"""Events sharing a correlation_id are traceable in audit_log."""
|
|
bus = ReactiveEventBus()
|
|
shared_cid = str(ULID())
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED, correlation_id=shared_cid))
|
|
bus.emit(
|
|
DomainEvent(event_type=EventType.PLAN_PHASE_CHANGED, correlation_id=shared_cid)
|
|
)
|
|
log = bus.audit_log
|
|
if len(log) != 2:
|
|
print(f"FAIL: expected 2 events, got {len(log)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if log[0].correlation_id != shared_cid or log[1].correlation_id != shared_cid:
|
|
print("FAIL: correlation_id mismatch", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("log-correlation-ok")
|
|
|
|
|
|
def protocol_conformance() -> None:
|
|
"""Both bus implementations satisfy EventBus Protocol."""
|
|
for cls in (ReactiveEventBus, LoggingEventBus):
|
|
bus = cls()
|
|
if not isinstance(bus, EventBus):
|
|
print(f"FAIL: {cls.__name__} != EventBus", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("protocol-conformance-ok")
|
|
|
|
|
|
def logging_emit_subscribe() -> None:
|
|
"""Verify LoggingEventBus emits to subscribed handlers."""
|
|
received: list[DomainEvent] = []
|
|
bus = LoggingEventBus()
|
|
bus.subscribe(EventType.PLAN_CREATED, received.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
if len(received) != 1:
|
|
print("FAIL: logging handler did not receive event", file=sys.stderr)
|
|
sys.exit(1)
|
|
if received[0].event_type != EventType.PLAN_CREATED:
|
|
print("FAIL: logging handler received wrong event type", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("logging-emit-subscribe-ok")
|
|
|
|
|
|
def logging_handler_exception_isolation() -> None:
|
|
"""Verify LoggingEventBus isolates handler failures."""
|
|
received: list[DomainEvent] = []
|
|
bus = LoggingEventBus()
|
|
|
|
def failing_handler(event: DomainEvent) -> None:
|
|
raise RuntimeError(f"intentional failure for {event.event_type.value}")
|
|
|
|
bus.subscribe(EventType.PLAN_CREATED, failing_handler)
|
|
bus.subscribe(EventType.PLAN_CREATED, received.append)
|
|
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
|
|
|
|
if len(received) != 1:
|
|
print("FAIL: normal logging handler was not called", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("logging-handler-exception-isolation-ok")
|
|
|
|
|
|
def type_validation() -> None:
|
|
"""Emit/subscribe reject invalid argument types."""
|
|
bus = ReactiveEventBus()
|
|
|
|
# emit non-DomainEvent
|
|
try:
|
|
bus.emit("not-an-event") # type: ignore[arg-type]
|
|
except TypeError:
|
|
pass
|
|
else:
|
|
print("FAIL: emit did not reject non-DomainEvent", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# subscribe non-EventType
|
|
try:
|
|
bus.subscribe("plan.created", lambda e: None) # type: ignore[arg-type]
|
|
except TypeError:
|
|
pass
|
|
else:
|
|
print("FAIL: subscribe did not reject non-EventType", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# subscribe non-callable
|
|
try:
|
|
bus.subscribe(EventType.PLAN_CREATED, "not-callable") # type: ignore[arg-type]
|
|
except TypeError:
|
|
pass
|
|
else:
|
|
print("FAIL: subscribe did not reject non-callable", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print("type-validation-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS = {
|
|
"event_type_count": event_type_count,
|
|
"event_type_domains": event_type_domains,
|
|
"event_type_dot_convention": event_type_dot_convention,
|
|
"domain_event_fields": domain_event_fields,
|
|
"domain_event_json_roundtrip": domain_event_json_roundtrip,
|
|
"domain_event_immutable": domain_event_immutable,
|
|
"reactive_emit_subscribe_audit": reactive_emit_subscribe_audit,
|
|
"reactive_handler_exception_isolation": reactive_handler_exception_isolation,
|
|
"reactive_stream_exception_isolation": reactive_stream_exception_isolation,
|
|
"reactive_stream_observable": reactive_stream_observable,
|
|
"reactive_stream_read_only": reactive_stream_read_only,
|
|
"audit_log_order": audit_log_order,
|
|
"audit_log_defensive_copy": audit_log_defensive_copy,
|
|
"audit_log_retention_cap": audit_log_retention_cap,
|
|
"audit_log_invalid_retention_cap": audit_log_invalid_retention_cap,
|
|
"log_correlation": log_correlation,
|
|
"protocol_conformance": protocol_conformance,
|
|
"logging_emit_subscribe": logging_emit_subscribe,
|
|
"logging_handler_exception_isolation": logging_handler_exception_isolation,
|
|
"type_validation": type_validation,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
|
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
|
|
print(f"Commands: {list(_COMMANDS)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
_COMMANDS[sys.argv[1]]()
|