forked from HAL9000/cleveragents-core
051ee7c290
Added 52 new .feature files and corresponding _steps.py files targeting previously uncovered code paths in the following areas: - TUI layer: app, commands, persona (state/schema/registry), widgets, input (shell_exec, reference_parser) - Application services: plan lifecycle/service/executor, session, project, repo indexing, correction, checkpoint, actor, llm_actors, strategy coordinator, resource file watcher, service retry wiring - CLI commands: session, resource, repl, plan, db, automation_profile - Domain models: retry_policy, resource_type, cost_budget, docker_compose_analyzer, detail_level, _sql_string_aware, _postgresql_helpers - Core: circuit_breaker, retry_service_patterns - Infrastructure: repositories, transaction_sandbox, strategy_registry, plugins/loader, container - Config: settings - Agents: plan_generation, context_analysis, auto_debug - A2A: facade All new tests follow the Behave/Gherkin BDD standard. Resolved step definition collisions with unique prefixes. Fixed Alembic fileConfig logger disabling issue (disable_existing_loggers=False). ISSUES CLOSED: #1068
348 lines
13 KiB
Python
348 lines
13 KiB
Python
"""Step definitions for plan_executor_coverage_boost.feature.
|
|
|
|
These steps target specific uncovered lines in plan_executor.py:
|
|
- Lines 328-330: _try_emit_metric success path (emitter configured)
|
|
- Lines 331-335: _try_emit_metric exception path (emitter raises)
|
|
- Line 384: _try_create_checkpoint returns None (no sandbox)
|
|
- Line 422: _try_rollback_to_last_checkpoint returns False (no sandbox)
|
|
- Line 458: _resolve_sandbox_for_checkpoint via execution_context
|
|
- Lines 598-599: _enforce_guardrails raises PlanError (step limit)
|
|
- Line 612: _enforce_guardrails_per_step returns early (guardrails None)
|
|
- Line 619: _enforce_guardrails_per_step raises PlanError (wall-clock)
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.autonomy_guardrail_service import (
|
|
AutonomyGuardrailService,
|
|
)
|
|
from cleveragents.application.services.plan_executor import (
|
|
PlanExecutor,
|
|
)
|
|
from cleveragents.core.exceptions import PlanError
|
|
from cleveragents.domain.models.core.autonomy_guardrails import AutonomyGuardrails
|
|
from cleveragents.domain.models.observability.metrics import OperationalMetricKey
|
|
from cleveragents.infrastructure.sandbox.checkpoint import CheckpointManager
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# A valid 26-char ULID for use as plan_id in metrics and other calls.
|
|
_VALID_PLAN_ID = str(ULID())
|
|
|
|
|
|
def _make_lifecycle_stub():
|
|
"""Return a minimal mock lifecycle service."""
|
|
return MagicMock(name="lifecycle_service")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the plan executor module is imported")
|
|
def step_plan_executor_module_imported(context):
|
|
"""Verify that PlanExecutor is importable."""
|
|
assert PlanExecutor is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _try_emit_metric success path (lines 328-330)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a mock metrics emitter")
|
|
def step_executor_with_mock_emitter(context):
|
|
"""Create a PlanExecutor with a mocked metrics_emitter."""
|
|
context.mock_emitter = MagicMock(name="metrics_emitter")
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
metrics_emitter=context.mock_emitter,
|
|
)
|
|
|
|
|
|
@when("I trigger metric emission with a valid key and value")
|
|
def step_trigger_metric_emission(context):
|
|
"""Call _try_emit_metric with a valid key, plan_id, and value."""
|
|
context.emit_error = None
|
|
try:
|
|
context.executor._try_emit_metric(
|
|
key=OperationalMetricKey.PLAN_DURATION_MS,
|
|
plan_id=_VALID_PLAN_ID,
|
|
value=42.5,
|
|
)
|
|
except Exception as exc:
|
|
context.emit_error = exc
|
|
|
|
|
|
@then("the metrics emitter should have received exactly one entry")
|
|
def step_verify_emitter_called(context):
|
|
"""Verify the mock emitter's emit() was called once."""
|
|
assert context.emit_error is None, f"Unexpected error: {context.emit_error!r}"
|
|
assert context.mock_emitter.emit.call_count == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _try_emit_metric exception path (lines 331-335)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a failing metrics emitter")
|
|
def step_executor_with_failing_emitter(context):
|
|
"""Create a PlanExecutor whose emitter.emit() raises."""
|
|
failing_emitter = MagicMock(name="failing_emitter")
|
|
failing_emitter.emit.side_effect = RuntimeError("emit boom")
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
metrics_emitter=failing_emitter,
|
|
)
|
|
|
|
|
|
@when("I trigger metric emission that causes an exception")
|
|
def step_trigger_failing_metric_emission(context):
|
|
"""Call _try_emit_metric knowing the emitter will fail."""
|
|
context.emit_error = None
|
|
try:
|
|
context.executor._try_emit_metric(
|
|
key=OperationalMetricKey.PLAN_DURATION_MS,
|
|
plan_id=_VALID_PLAN_ID,
|
|
value=99.0,
|
|
)
|
|
except Exception as exc:
|
|
context.emit_error = exc
|
|
|
|
|
|
@then("no exception should propagate from metric emission")
|
|
def step_verify_no_emission_exception(context):
|
|
"""Verify the exception was swallowed (best-effort emission)."""
|
|
assert context.emit_error is None, f"Expected no error, got {context.emit_error!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _try_create_checkpoint returns None when sandbox is None (line 384)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a checkpoint manager but no sandbox source")
|
|
def step_executor_with_checkpoint_manager_no_sandbox(context):
|
|
"""Create a PlanExecutor with a checkpoint manager but no sandbox_root
|
|
and no execution_context, so _resolve_sandbox_for_checkpoint returns None.
|
|
"""
|
|
mock_cp_manager = MagicMock(spec=CheckpointManager, name="cp_manager")
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
checkpoint_manager=mock_cp_manager,
|
|
sandbox_root=None,
|
|
execution_context=None,
|
|
)
|
|
|
|
|
|
@when("I attempt to create a plan executor checkpoint")
|
|
def step_attempt_create_checkpoint(context):
|
|
"""Call _try_create_checkpoint on the executor."""
|
|
context.checkpoint_result = context.executor._try_create_checkpoint(
|
|
plan_id=_VALID_PLAN_ID,
|
|
phase="pre_execute",
|
|
)
|
|
|
|
|
|
@then("the checkpoint result should be None")
|
|
def step_verify_checkpoint_none(context):
|
|
"""Verify the checkpoint result is None."""
|
|
assert context.checkpoint_result is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _try_rollback_to_last_checkpoint returns False (line 422)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I attempt to rollback to the last checkpoint")
|
|
def step_attempt_rollback(context):
|
|
"""Call _try_rollback_to_last_checkpoint on the executor."""
|
|
context.rollback_result = context.executor._try_rollback_to_last_checkpoint(
|
|
plan_id=_VALID_PLAN_ID,
|
|
)
|
|
|
|
|
|
@then("the rollback result should be False")
|
|
def step_verify_rollback_false(context):
|
|
"""Verify the rollback result is False."""
|
|
assert context.rollback_result is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _resolve_sandbox_for_checkpoint via execution_context (line 458)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with an execution context that has a sandbox manager")
|
|
def step_executor_with_execution_context_sandbox(context):
|
|
"""Create a PlanExecutor with an execution_context whose
|
|
sandbox_manager.list_sandboxes returns a sandbox object.
|
|
"""
|
|
mock_sandbox = SimpleNamespace(
|
|
sandbox_id="sandbox-from-ctx",
|
|
context=SimpleNamespace(sandbox_path="/tmp/sandbox-ctx"),
|
|
)
|
|
mock_sandbox_mgr = MagicMock(name="sandbox_manager")
|
|
mock_sandbox_mgr.list_sandboxes.return_value = [mock_sandbox]
|
|
|
|
mock_exec_ctx = SimpleNamespace(sandbox_manager=mock_sandbox_mgr)
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
execution_context=mock_exec_ctx,
|
|
)
|
|
context.expected_sandbox = mock_sandbox
|
|
|
|
|
|
@when("I resolve a sandbox for checkpointing")
|
|
def step_resolve_sandbox(context):
|
|
"""Call _resolve_sandbox_for_checkpoint."""
|
|
context.resolved_sandbox = context.executor._resolve_sandbox_for_checkpoint(
|
|
plan_id=_VALID_PLAN_ID,
|
|
)
|
|
|
|
|
|
@then("the resolved sandbox should come from the execution context")
|
|
def step_verify_resolved_sandbox(context):
|
|
"""Verify the returned sandbox matches the one from the execution context."""
|
|
assert context.resolved_sandbox is context.expected_sandbox
|
|
assert context.resolved_sandbox.sandbox_id == "sandbox-from-ctx"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _enforce_guardrails raises PlanError on step limit (lines 598-599)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a guardrail service that blocks step limit")
|
|
def step_executor_guardrail_blocks_step_limit(context):
|
|
"""Create a PlanExecutor with a guardrail service that:
|
|
- get_guardrails returns guardrails with start_time already set
|
|
- check_wall_clock returns True (ok)
|
|
- check_step_limit returns False (blocked)
|
|
"""
|
|
guardrail_svc = MagicMock(spec=AutonomyGuardrailService, name="guardrail_svc")
|
|
|
|
mock_guardrails = MagicMock(spec=AutonomyGuardrails)
|
|
mock_guardrails.start_time = "2025-01-01T00:00:00Z"
|
|
mock_guardrails.step_count = 100
|
|
|
|
guardrail_svc.get_guardrails.return_value = mock_guardrails
|
|
guardrail_svc.check_wall_clock.return_value = True
|
|
guardrail_svc.check_step_limit.return_value = False
|
|
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
guardrail_service=guardrail_svc,
|
|
)
|
|
|
|
|
|
@when("I enforce guardrails for a plan")
|
|
def step_enforce_guardrails(context):
|
|
"""Call _enforce_guardrails, expecting a PlanError."""
|
|
context.guardrail_error = None
|
|
try:
|
|
context.executor._enforce_guardrails("plan-step-limit-test")
|
|
except PlanError as exc:
|
|
context.guardrail_error = exc
|
|
|
|
|
|
@then("a PlanError about step limit should be raised")
|
|
def step_verify_step_limit_error(context):
|
|
"""Verify that a PlanError mentioning step limit was raised."""
|
|
assert context.guardrail_error is not None, "Expected PlanError but none raised"
|
|
assert "step limit already reached" in str(context.guardrail_error).lower(), (
|
|
f"Unexpected error message: {context.guardrail_error}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _enforce_guardrails_per_step returns early when guardrails is None (line 612)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a guardrail service that returns no guardrails")
|
|
def step_executor_guardrail_returns_none(context):
|
|
"""Create a PlanExecutor with a guardrail service that returns
|
|
None from get_guardrails (no guardrails configured for this plan).
|
|
"""
|
|
guardrail_svc = MagicMock(spec=AutonomyGuardrailService, name="guardrail_svc_none")
|
|
guardrail_svc.get_guardrails.return_value = None
|
|
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
guardrail_service=guardrail_svc,
|
|
)
|
|
|
|
|
|
@when("I enforce per-step guardrails")
|
|
def step_enforce_per_step_guardrails(context):
|
|
"""Call _enforce_guardrails_per_step with guardrails=None."""
|
|
context.per_step_error = None
|
|
try:
|
|
context.executor._enforce_guardrails_per_step("plan-no-guardrails")
|
|
except Exception as exc:
|
|
context.per_step_error = exc
|
|
|
|
|
|
@then("no exception should be raised from per-step enforcement")
|
|
def step_verify_no_per_step_error(context):
|
|
"""Verify no exception was raised (early return)."""
|
|
assert context.per_step_error is None, (
|
|
f"Expected no error, got {context.per_step_error!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _enforce_guardrails_per_step raises PlanError on wall-clock (line 619)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with a guardrail service that blocks wall-clock per step")
|
|
def step_executor_guardrail_blocks_wallclock_per_step(context):
|
|
"""Create a PlanExecutor with a guardrail service that:
|
|
- get_guardrails returns valid guardrails
|
|
- check_step_limit returns True (step OK)
|
|
- check_wall_clock returns False (wall-clock exceeded)
|
|
"""
|
|
guardrail_svc = MagicMock(spec=AutonomyGuardrailService, name="guardrail_svc_wc")
|
|
|
|
mock_guardrails = MagicMock(spec=AutonomyGuardrails)
|
|
mock_guardrails.step_count = 5
|
|
|
|
guardrail_svc.get_guardrails.return_value = mock_guardrails
|
|
guardrail_svc.check_step_limit.return_value = True
|
|
guardrail_svc.check_wall_clock.return_value = False
|
|
|
|
context.executor = PlanExecutor(
|
|
lifecycle_service=_make_lifecycle_stub(),
|
|
guardrail_service=guardrail_svc,
|
|
)
|
|
|
|
|
|
@when("I enforce per-step guardrails expecting wall-clock error")
|
|
def step_enforce_per_step_wallclock(context):
|
|
"""Call _enforce_guardrails_per_step, expecting a wall-clock PlanError."""
|
|
context.per_step_wc_error = None
|
|
try:
|
|
context.executor._enforce_guardrails_per_step("plan-wc-exceeded")
|
|
except PlanError as exc:
|
|
context.per_step_wc_error = exc
|
|
|
|
|
|
@then("a PlanError about wall-clock limit should be raised from per-step")
|
|
def step_verify_wallclock_per_step_error(context):
|
|
"""Verify that a PlanError about wall-clock was raised."""
|
|
assert context.per_step_wc_error is not None, "Expected PlanError but none raised"
|
|
assert "wall-clock limit exceeded" in str(context.per_step_wc_error).lower(), (
|
|
f"Unexpected error message: {context.per_step_wc_error}"
|
|
)
|