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
## 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
545 lines
17 KiB
Python
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")
|