Files
cleveragents-core/features/steps/event_emission_wiring_steps.py
brent.edwards eb46f0ff54
CI / push-validation (push) Successful in 40s
CI / helm (push) Successful in 44s
CI / build (push) Successful in 1m14s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m36s
CI / e2e_tests (push) Successful in 1m19s
CI / typecheck (push) Successful in 2m22s
CI / security (push) Successful in 2m23s
CI / benchmark-regression (push) Failing after 41s
CI / integration_tests (push) Successful in 4m46s
CI / unit_tests (push) Failing after 20m13s
CI / benchmark-publish (push) Failing after 28m28s
CI / coverage (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
fix(plan): add tier hydration and improve architecture review output (#10938)
## Summary

This PR fixes issue #10878 where architecture reviews were truncated because the regex pattern for parsing file output would stop at the first ``` encountered in the Markdown report.

## Changes

- Change file delimiters from ``` to >>>>>>>/<<<<<<< to avoid Markdown conflicts
- Add tier hydration before strategize phase in plan_executor.py
- Increase max_tokens to 16384 in llm_actors.py for longer outputs
- Increase context_max_tokens_hot from 16000 to 32000 in settings.py
- Fix get_hot_view → get_hot_fragments in strategy_actor.py and plan_executor.py
- Add opencode to skip directories in context_tier_hydrator.py
- Change sandbox output location to plan-output/ directory in plan.py
- Add get_context_summary stub method to acms_service.py

## Testing

Run architecture review action and verify the output report is complete with all sections.

Reviewed-on: #10938
2026-05-19 12:43:34 +00:00

545 lines
17 KiB
Python

"""Step definitions for event_emission_wiring.feature.
Tests that domain services correctly emit events through the EventBus
for all 32 newly wired event types.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from ulid import ULID
from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.invariant_service import InvariantService
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.application.services.session_service import (
PersistentSessionService,
)
from cleveragents.application.services.validation_pipeline import (
ValidationCommand,
ValidationPipeline,
)
from cleveragents.domain.models.core.invariant import InvariantScope
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
from cleveragents.domain.models.core.session import MessageRole
from cleveragents.domain.models.core.tool import ValidationMode
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_settings() -> Any:
mock = MagicMock()
mock.async_enabled = False
mock.cleveragents_max_context_size = 50 * 1024 * 1024
return mock
def _get_events(ctx: Context) -> list[DomainEvent]:
return ctx.ew_event_bus.audit_log # type: ignore[no-any-return]
def _find_event(ctx: Context, event_type_value: str) -> DomainEvent | None:
for ev in _get_events(ctx):
if ev.event_type.value == event_type_value:
return ev
return None
# ---------------------------------------------------------------------------
# Plan Lifecycle steps
# ---------------------------------------------------------------------------
@given("an in-memory PlanLifecycleService with a tracking event bus")
def step_plan_service_with_bus(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
ctx.ew_plan_svc = PlanLifecycleService(
settings=_make_settings(),
event_bus=ctx.ew_event_bus,
)
def _create_plan_in_phase(
svc: PlanLifecycleService,
action_name: str,
phase: PlanPhase,
) -> str:
"""Helper to create a plan and set it to a specific phase/processing state."""
svc.create_action(
name=action_name,
description="Test action",
definition_of_done="Done",
strategy_actor="local/strategy",
execution_actor="local/execution",
)
plan = svc.use_action(action_name)
plan.processing_state = ProcessingState.PROCESSING
plan.phase = phase
svc.commit_plan(plan)
return plan.identity.plan_id
@given("a plan in Strategize/PROCESSING phase")
def step_plan_strategize_processing(ctx: Context) -> None:
svc: PlanLifecycleService = ctx.ew_plan_svc
ctx.ew_plan_id = _create_plan_in_phase(svc, "local/ew-action", PlanPhase.STRATEGIZE)
ctx.ew_event_bus.clear_audit_log()
@given("a plan in Execute/PROCESSING phase")
def step_plan_execute_processing(ctx: Context) -> None:
svc: PlanLifecycleService = ctx.ew_plan_svc
ctx.ew_plan_id = _create_plan_in_phase(
svc, "local/ew-exec-action", PlanPhase.EXECUTE
)
ctx.ew_event_bus.clear_audit_log()
@given("a plan in Apply/PROCESSING phase")
def step_plan_apply_processing(ctx: Context) -> None:
svc: PlanLifecycleService = ctx.ew_plan_svc
ctx.ew_plan_id = _create_plan_in_phase(
svc, "local/ew-apply-action", PlanPhase.APPLY
)
ctx.ew_event_bus.clear_audit_log()
@when("I call complete_strategize on that plan")
def step_complete_strategize(ctx: Context) -> None:
ctx.ew_plan_svc.complete_strategize(ctx.ew_plan_id)
@when('I call fail_strategize with error "{error}"')
def step_fail_strategize(ctx: Context, error: str) -> None:
ctx.ew_plan_svc.fail_strategize(ctx.ew_plan_id, error)
@when('I call fail_execute with error "{error}"')
def step_fail_execute(ctx: Context, error: str) -> None:
ctx.ew_plan_svc.fail_execute(ctx.ew_plan_id, error)
@when('I call fail_apply with error "{error}"')
def step_fail_apply(ctx: Context, error: str) -> None:
ctx.ew_plan_svc.fail_apply(ctx.ew_plan_id, error)
# ---------------------------------------------------------------------------
# Decision steps
# ---------------------------------------------------------------------------
@given("an in-memory DecisionService with a tracking event bus")
def step_decision_service_with_bus(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
ctx.ew_decision_svc = DecisionService(event_bus=ctx.ew_event_bus)
@when("I record a normal decision")
def step_record_normal_decision(ctx: Context) -> None:
plan_id = str(ULID())
ctx.ew_decision_svc.record_decision(
plan_id=plan_id,
decision_type="strategy_choice",
question="Which strategy?",
chosen_option="Option A",
)
@when("I record a correction decision")
def step_record_correction_decision(ctx: Context) -> None:
plan_id = str(ULID())
ctx.ew_decision_svc.record_decision(
plan_id=plan_id,
decision_type="strategy_choice",
question="Which strategy?",
chosen_option="Option B",
is_correction=True,
corrects_decision_id=str(ULID()),
correction_reason="Correcting previous",
)
@given("ew two decisions exist for the same plan")
def step_two_decisions(ctx: Context) -> None:
plan_id = str(ULID())
ctx.ew_plan_id_for_decisions = plan_id
d1 = ctx.ew_decision_svc.record_decision(
plan_id=plan_id,
decision_type="strategy_choice",
question="Q1",
chosen_option="O1",
)
d2 = ctx.ew_decision_svc.record_decision(
plan_id=plan_id,
decision_type="strategy_choice",
question="Q2",
chosen_option="O2",
)
ctx.ew_decision_id_1 = d1.decision_id
ctx.ew_decision_id_2 = d2.decision_id
ctx.ew_event_bus.clear_audit_log()
@when("ew I mark the first decision as superseded by the second")
def step_mark_superseded(ctx: Context) -> None:
ctx.ew_decision_svc.mark_superseded(ctx.ew_decision_id_1, ctx.ew_decision_id_2)
# ---------------------------------------------------------------------------
# Invariant steps
# ---------------------------------------------------------------------------
@given("an InvariantService with a tracking event bus")
def step_invariant_service_with_bus(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
ctx.ew_invariant_svc = InvariantService(event_bus=ctx.ew_event_bus)
@given("an active invariant exists")
def step_active_invariant(ctx: Context) -> None:
inv = ctx.ew_invariant_svc.add_invariant(
text="Must not delete files",
scope=InvariantScope.GLOBAL,
source_name="test",
)
ctx.ew_invariant = inv
@given("an active invariant exists with known ID")
def step_active_invariant_known_id(ctx: Context) -> None:
inv = ctx.ew_invariant_svc.add_invariant(
text="Must not modify production data",
scope=InvariantScope.GLOBAL,
source_name="test",
)
ctx.ew_invariant = inv
ctx.ew_invariant_id = inv.id
@when("I enforce invariants for a plan")
def step_enforce_invariants(ctx: Context) -> None:
ctx.ew_event_bus.clear_audit_log()
ctx.ew_invariant_svc.enforce_invariants(
plan_id=str(ULID()),
invariants=[ctx.ew_invariant],
)
@when("I enforce invariants with violations for that invariant")
def step_enforce_with_violations(ctx: Context) -> None:
ctx.ew_event_bus.clear_audit_log()
ctx.ew_invariant_svc.enforce_invariants(
plan_id=str(ULID()),
invariants=[ctx.ew_invariant],
violated_invariant_ids=[ctx.ew_invariant_id],
)
# ---------------------------------------------------------------------------
# Session steps
# ---------------------------------------------------------------------------
@given("a PersistentSessionService with a tracking event bus")
def step_session_service_with_bus(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
mock_session_repo = MagicMock()
mock_message_repo = MagicMock()
mock_session_repo.get_by_id.return_value = MagicMock(updated_at=None)
mock_message_repo.count_for_session.return_value = 0
ctx.ew_session_svc = PersistentSessionService(
session_repo=mock_session_repo,
message_repo=mock_message_repo,
event_bus=ctx.ew_event_bus,
)
@given("a session exists")
def step_session_exists(ctx: Context) -> None:
session = ctx.ew_session_svc.create()
ctx.ew_session_id = session.session_id
ctx.ew_event_bus.clear_audit_log()
@when("I append a message to the session")
def step_append_message(ctx: Context) -> None:
ctx.ew_session_svc.append_message(
ctx.ew_session_id,
MessageRole.USER,
"Hello, world!",
)
# ---------------------------------------------------------------------------
# Checkpoint / Sandbox steps
# ---------------------------------------------------------------------------
@given("a CheckpointService with a tracking event bus")
def step_checkpoint_service_with_bus(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
ctx.ew_checkpoint_svc = CheckpointService(event_bus=ctx.ew_event_bus)
@when("I create a checkpoint")
def step_create_checkpoint(ctx: Context) -> None:
ctx.ew_checkpoint_svc.create_checkpoint(
plan_id=str(ULID()),
sandbox_ref="abc123",
reason="Test checkpoint",
)
@when("I register a sandbox for a plan")
def step_register_sandbox(ctx: Context) -> None:
ctx.ew_checkpoint_svc.register_sandbox(
plan_id=str(ULID()),
sandbox_ref="sandbox-ref-abc",
)
@when("I mark a plan as applied")
def step_mark_applied(ctx: Context) -> None:
ctx.ew_checkpoint_svc.mark_plan_applied(str(ULID()))
# ---------------------------------------------------------------------------
# Validation steps
# ---------------------------------------------------------------------------
@given("a ValidationPipeline with a tracking event bus and a passing validation")
def step_validation_passing(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
def _executor(name: str, config: dict[str, Any]) -> dict[str, Any]:
return {"passed": True, "message": "OK"}
cmd = ValidationCommand(
validation_name="test-check",
resource_id="res-1",
resource_name="test-resource",
mode=ValidationMode.REQUIRED,
)
ctx.ew_pipeline = ValidationPipeline(
commands=[cmd],
executor=_executor,
event_bus=ctx.ew_event_bus,
)
@given("a ValidationPipeline with a tracking event bus and a failing validation")
def step_validation_failing(ctx: Context) -> None:
ctx.ew_event_bus = ReactiveEventBus()
def _executor(name: str, config: dict[str, Any]) -> dict[str, Any]:
return {"passed": False, "message": "FAIL"}
cmd = ValidationCommand(
validation_name="test-check-fail",
resource_id="res-2",
resource_name="test-resource-fail",
mode=ValidationMode.REQUIRED,
)
ctx.ew_pipeline = ValidationPipeline(
commands=[cmd],
executor=_executor,
event_bus=ctx.ew_event_bus,
)
@when("I run the pipeline")
def step_run_pipeline(ctx: Context) -> None:
ctx.ew_pipeline.run()
# ---------------------------------------------------------------------------
# Common assertion steps
# ---------------------------------------------------------------------------
@then('ew the audit log should contain a "{event_type}" event')
def step_ew_event_bus_received(ctx: Context, event_type: str) -> None:
ev = _find_event(ctx, event_type)
assert ev is not None, (
f"Expected event '{event_type}' not found. "
f"Got: {[e.event_type.value for e in _get_events(ctx)]}"
)
@then('ew the event details should contain key "{key}"')
def step_ew_event_details_key(ctx: Context, key: str) -> None:
events = _get_events(ctx)
found = False
for ev in events:
if key in ev.details:
found = True
break
assert found, (
f"Key '{key}' not found in any event details. "
f"Events: {[(e.event_type.value, list(e.details.keys())) for e in events]}"
)
# ---------------------------------------------------------------------------
# Tool Lifecycle steps
# ---------------------------------------------------------------------------
@given("a ToolRuntime with a tracking event bus and a registered tool")
def step_tool_runtime_with_bus(ctx: Context) -> None:
from cleveragents.tool.lifecycle import (
ToolDescriptor,
ToolExecutionContext,
ToolResult,
ToolRuntime,
)
ctx.ew_event_bus = ReactiveEventBus()
class _PassingTool:
def discover(self) -> ToolDescriptor:
return ToolDescriptor(name="test-tool", description="A test tool")
def activate(self, ctx: ToolExecutionContext) -> None:
pass
def execute(
self, params: dict[str, Any], ctx: ToolExecutionContext
) -> ToolResult:
return ToolResult(success=True, data={"result": "ok"})
def deactivate(self, ctx: ToolExecutionContext) -> None:
pass
from cleveragents.domain.models.core.tool import Tool, ToolCapability
tool = Tool(
name="local/test-tool",
description="Test",
source="builtin",
tool_type="tool",
timeout=30,
capability=ToolCapability(),
)
ctx.ew_tool_runtime = ToolRuntime(
tools={"local/test-tool": tool},
instances={"local/test-tool": _PassingTool()},
event_bus=ctx.ew_event_bus,
)
ctx.ew_tool_ctx = ToolExecutionContext(plan_id="test-plan-001")
@when("I execute the tool")
def step_execute_tool(ctx: Context) -> None:
ctx.ew_tool_runtime.execute("local/test-tool", {}, ctx.ew_tool_ctx)
@given("a ToolRuntime with a tracking event bus and a failing tool")
def step_tool_runtime_failing(ctx: Context) -> None:
from cleveragents.tool.lifecycle import (
ToolDescriptor,
ToolExecutionContext,
ToolResult,
ToolRuntime,
)
ctx.ew_event_bus = ReactiveEventBus()
class _FailingTool:
def discover(self) -> ToolDescriptor:
return ToolDescriptor(name="fail-tool", description="A failing tool")
def activate(self, ctx: ToolExecutionContext) -> None:
pass
def execute(
self, params: dict[str, Any], ctx: ToolExecutionContext
) -> ToolResult:
raise RuntimeError("Tool execution failed!")
def deactivate(self, ctx: ToolExecutionContext) -> None:
pass
from cleveragents.domain.models.core.tool import Tool, ToolCapability
tool = Tool(
name="local/fail-tool",
description="Failing test",
source="builtin",
tool_type="tool",
timeout=30,
capability=ToolCapability(),
)
ctx.ew_tool_runtime = ToolRuntime(
tools={"local/fail-tool": tool},
instances={"local/fail-tool": _FailingTool()},
event_bus=ctx.ew_event_bus,
)
ctx.ew_tool_ctx = ToolExecutionContext(plan_id="test-plan-002")
@when("I execute the failing tool expecting an error")
def step_execute_failing_tool(ctx: Context) -> None:
import contextlib
from cleveragents.tool.lifecycle import ToolExecutionError
with contextlib.suppress(ToolExecutionError):
ctx.ew_tool_runtime.execute("local/fail-tool", {}, ctx.ew_tool_ctx)
# ---------------------------------------------------------------------------
# Actor Runtime steps
# ---------------------------------------------------------------------------
@given("a ToolCallingRuntime with a tracking event bus and a simple LLM")
def step_actor_runtime_with_bus(ctx: Context) -> None:
from cleveragents.tool.actor_runtime import (
LLMResponse,
ToolCallingRuntime,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner
ctx.ew_event_bus = ReactiveEventBus()
registry = ToolRegistry()
runner = ToolRunner(registry=registry)
class _SimpleLLM:
def invoke(
self,
prompt: str,
tool_schemas: list[dict[str, Any]],
tool_results: list[dict[str, Any]] | None = None,
actor_config: dict[str, Any] | None = None,
) -> LLMResponse:
return LLMResponse(content="Hello!")
ctx.ew_actor_runtime = ToolCallingRuntime(
registry=registry,
runner=runner,
llm_caller=_SimpleLLM(),
event_bus=ctx.ew_event_bus,
)
@when("I run the tool loop with a simple prompt")
def step_run_tool_loop(ctx: Context) -> None:
ctx.ew_actor_runtime.run_tool_loop("Say hello")