Files
temp/features/steps/checkpoint_service_coverage_boost_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
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
2026-03-20 21:22:10 +00:00

250 lines
10 KiB
Python

"""Step definitions for checkpoint_service_coverage_boost.feature.
These steps target specific uncovered lines in checkpoint_service.py:
- Lines 484-487: subprocess.CalledProcessError handler in _run_git
- Lines 488-491: subprocess.TimeoutExpired handler in _run_git
- Line 417: ResourceNotFoundError when lifecycle service returns None plan
"""
import os
import subprocess
import tempfile
from unittest.mock import MagicMock, patch
from behave import given, then, when
from ulid import ULID
from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.core.exceptions import BusinessRuleViolation, ResourceNotFoundError
# Generate a stable ULID for the rollback test plan
_ROLLBACK_PLAN_ID = str(ULID())
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the checkpoint service module is imported for coverage boost")
def step_checkpoint_module_imported(context):
"""Ensure the checkpoint service module is importable."""
assert CheckpointService is not None
# ---------------------------------------------------------------------------
# Shared: create a checkpoint service with in-memory storage
# ---------------------------------------------------------------------------
@given("a checkpoint service with in-memory storage for coverage boost")
def step_create_in_memory_service(context):
"""Create a CheckpointService with no repository (in-memory mode)."""
context.cp_service = CheckpointService()
# ---------------------------------------------------------------------------
# Scenario: Git CalledProcessError → BusinessRuleViolation (lines 484-487)
# ---------------------------------------------------------------------------
@given("subprocess.run is mocked to raise CalledProcessError")
def step_mock_subprocess_called_process_error(context):
"""Prepare a mock that raises CalledProcessError."""
context.subprocess_side_effect = subprocess.CalledProcessError(
returncode=128,
cmd=["git", "status"],
stderr="fatal: not a git repository",
)
@when('I invoke _run_git with args "{git_args}"')
def step_invoke_run_git(context, git_args):
"""Call _run_git with a mocked subprocess.run."""
args = git_args.split()
context.run_git_error = None
with patch("subprocess.run", side_effect=context.subprocess_side_effect):
try:
context.cp_service._run_git(args, cwd="/tmp/fake-sandbox")
except (BusinessRuleViolation, Exception) as exc:
context.run_git_error = exc
@then('a BusinessRuleViolation should be raised mentioning "{fragment}"')
def step_verify_business_rule_violation(context, fragment):
"""Verify a BusinessRuleViolation was raised containing the fragment."""
error = getattr(context, "run_git_error", None) or getattr(
context, "rollback_error", None
)
assert error is not None, "Expected an exception but none was raised"
assert isinstance(error, BusinessRuleViolation), (
f"Expected BusinessRuleViolation, got {type(error).__name__}: {error}"
)
assert fragment in str(error), (
f"Expected '{fragment}' in error message, got: {error}"
)
@then('the error message should contain the git command "{cmd}"')
def step_verify_error_contains_git_command(context, cmd):
"""Verify the error message includes the git subcommand."""
assert cmd in str(context.run_git_error), (
f"Expected '{cmd}' in error message, got: {context.run_git_error}"
)
@then("the error message should include the stderr output")
def step_verify_error_contains_stderr(context):
"""Verify the CalledProcessError handler includes stderr in the message."""
msg = str(context.run_git_error)
assert "fatal:" in msg or "not a git repository" in msg, (
f"Expected stderr content in error message, got: {msg}"
)
# ---------------------------------------------------------------------------
# Scenario: Git TimeoutExpired → BusinessRuleViolation (lines 488-491)
# ---------------------------------------------------------------------------
@given("subprocess.run is mocked to raise TimeoutExpired")
def step_mock_subprocess_timeout(context):
"""Prepare a mock that raises TimeoutExpired."""
context.subprocess_side_effect = subprocess.TimeoutExpired(
cmd=["git", "log"],
timeout=60,
)
@then('the timeout error message should contain the git command "{cmd}"')
def step_verify_timeout_error_contains_git_command(context, cmd):
"""Verify the timeout error message includes the git subcommand."""
assert context.run_git_error is not None, (
"Expected an exception but none was raised"
)
assert isinstance(context.run_git_error, BusinessRuleViolation), (
f"Expected BusinessRuleViolation, got {type(context.run_git_error).__name__}"
)
assert "timed out" in str(context.run_git_error), (
f"Expected 'timed out' in error message, got: {context.run_git_error}"
)
assert cmd in str(context.run_git_error), (
f"Expected '{cmd}' in error message, got: {context.run_git_error}"
)
# ---------------------------------------------------------------------------
# Scenario: CalledProcessError during rollback (lines 484-487 via rollback)
# ---------------------------------------------------------------------------
@given("a sandbox directory exists at a temporary path")
def step_create_temp_sandbox(context):
"""Create a temporary directory with a .git subdirectory to act as sandbox."""
context.sandbox_tmpdir = tempfile.mkdtemp(prefix="sandbox_test_")
os.makedirs(os.path.join(context.sandbox_tmpdir, ".git"), exist_ok=True)
context.rollback_plan_id = _ROLLBACK_PLAN_ID
context.cp_service.register_sandbox(
context.rollback_plan_id, context.sandbox_tmpdir
)
def cleanup():
import shutil
shutil.rmtree(context.sandbox_tmpdir, ignore_errors=True)
context.add_cleanup(cleanup)
@given("a checkpoint was created for the rollback test plan")
def step_create_checkpoint_for_rollback(context):
"""Create a checkpoint for the rollback test plan."""
context.rollback_checkpoint = context.cp_service.create_checkpoint(
plan_id=context.rollback_plan_id,
sandbox_ref="abc123deadbeef",
reason="test checkpoint for rollback",
)
@given("subprocess.run is mocked to raise CalledProcessError for git diff")
def step_mock_called_process_error_for_diff(context):
"""Mock subprocess.run to raise CalledProcessError."""
context.subprocess_patch = patch(
"subprocess.run",
side_effect=subprocess.CalledProcessError(
returncode=1,
cmd=["git", "diff", "--name-only"],
stderr="fatal: bad revision",
),
)
context.subprocess_mock = context.subprocess_patch.start()
context.add_cleanup(context.subprocess_patch.stop)
@when("I attempt to rollback to the checkpoint")
def step_attempt_rollback(context):
"""Attempt rollback and capture any error."""
context.rollback_error = None
context.run_git_error = None
try:
context.cp_service.rollback_to_checkpoint(
plan_id=context.rollback_plan_id,
checkpoint_id=context.rollback_checkpoint.checkpoint_id,
)
except Exception as exc:
context.rollback_error = exc
context.run_git_error = exc
# ---------------------------------------------------------------------------
# Scenario: TimeoutExpired during rollback (lines 488-491 via rollback)
# ---------------------------------------------------------------------------
@given("subprocess.run is mocked to raise TimeoutExpired for git reset")
def step_mock_timeout_for_reset(context):
"""Mock subprocess.run to raise TimeoutExpired."""
context.subprocess_patch = patch(
"subprocess.run",
side_effect=subprocess.TimeoutExpired(
cmd=["git", "reset", "--hard"],
timeout=60,
),
)
context.subprocess_mock = context.subprocess_patch.start()
context.add_cleanup(context.subprocess_patch.stop)
# ---------------------------------------------------------------------------
# Scenario: _resolve_sandbox_path with lifecycle service returning None
# (line 417)
# ---------------------------------------------------------------------------
@given("a checkpoint service with a lifecycle service that returns None for any plan")
def step_create_service_with_null_lifecycle(context):
"""Create a CheckpointService with a mocked lifecycle service that returns None."""
mock_lifecycle = MagicMock()
mock_lifecycle.get_plan.return_value = None
context.cp_service = CheckpointService(plan_lifecycle_service=mock_lifecycle)
@when('I attempt to resolve the sandbox path for plan "{plan_id}"')
def step_attempt_resolve_sandbox(context, plan_id):
"""Attempt to call _resolve_sandbox_path and capture the error."""
context.resolve_error = None
try:
context.cp_service._resolve_sandbox_path(plan_id)
except Exception as exc:
context.resolve_error = exc
@then('a ResourceNotFoundError should be raised for resource type "{resource_type}"')
def step_verify_resource_not_found(context, resource_type):
"""Verify a ResourceNotFoundError was raised with the expected resource type."""
assert context.resolve_error is not None, (
"Expected an exception but none was raised"
)
assert isinstance(context.resolve_error, ResourceNotFoundError), (
f"Expected ResourceNotFoundError, got {type(context.resolve_error).__name__}: "
f"{context.resolve_error}"
)
assert context.resolve_error.resource_type == resource_type, (
f"Expected resource_type='{resource_type}', "
f"got '{context.resolve_error.resource_type}'"
)
@then('the ResourceNotFoundError should reference plan id "{plan_id}"')
def step_verify_resource_not_found_plan_id(context, plan_id):
"""Verify the error references the correct plan ID."""
assert context.resolve_error.resource_id == plan_id, (
f"Expected resource_id='{plan_id}', got '{context.resolve_error.resource_id}'"
)