Files
temp/features/steps/checkpoint_service_coverage_r3_steps.py
freemo 31472b5413 test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

314 lines
12 KiB
Python

"""Step definitions for checkpoint_service_coverage_r3.feature.
Targets uncovered lines in checkpoint_service.py:
- Lines 425-426: selective_rollback except when rev-parse HEAD fails
- Lines 453-458, 460: selective_rollback recovery failure logging
- Line 487: archive_artifacts with explicit archive_dir
- Line 541: _compute_diff_snapshot returns [] when refs match
- Lines 546, 548: _compute_diff_snapshot returns [] when sandbox missing
- Lines 556-562, 564, 566: _compute_diff_snapshot returns [] when git diff fails
"""
import os
import shutil
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
# Pre-generate valid ULID strings for use as plan IDs
_SANDBOX_PLAN_ID = str(ULID())
_DIFF_PLAN_ID = str(ULID())
_NO_SANDBOX_PLAN_ID = str(ULID())
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("chkcov3 the checkpoint service module is available")
def step_chkcov3_module_available(context):
"""Verify the checkpoint service module can be imported."""
assert CheckpointService is not None
# ---------------------------------------------------------------------------
# Shared: create a checkpoint service with in-memory storage
# ---------------------------------------------------------------------------
@given("chkcov3 a checkpoint service with in-memory storage")
def step_chkcov3_create_service(context):
"""Create a CheckpointService with no repository (in-memory)."""
context.chkcov3_service = CheckpointService()
# ---------------------------------------------------------------------------
# Shared: create a temporary sandbox directory with .git
# ---------------------------------------------------------------------------
@given("chkcov3 a sandbox directory with .git exists")
def step_chkcov3_create_sandbox(context):
"""Create a temp dir with a .git subdir to act as a sandbox."""
tmpdir = tempfile.mkdtemp(prefix="chkcov3_sandbox_")
os.makedirs(os.path.join(tmpdir, ".git"), exist_ok=True)
context.chkcov3_sandbox_path = tmpdir
context.chkcov3_plan_id = _SANDBOX_PLAN_ID
context.chkcov3_service.register_sandbox(context.chkcov3_plan_id, tmpdir)
def _cleanup():
shutil.rmtree(tmpdir, ignore_errors=True)
context.add_cleanup(_cleanup)
# ---------------------------------------------------------------------------
# Shared: create a checkpoint for the sandbox plan
# ---------------------------------------------------------------------------
@given("chkcov3 a checkpoint exists for the sandbox plan")
def step_chkcov3_create_checkpoint(context):
"""Create a checkpoint under the sandbox plan."""
cp = context.chkcov3_service.create_checkpoint(
plan_id=context.chkcov3_plan_id,
sandbox_ref="deadbeef1234",
reason="chkcov3 test checkpoint",
)
context.chkcov3_checkpoint_id = cp.checkpoint_id
# ===================================================================
# Scenario: selective_rollback when rev-parse HEAD fails (425-426)
# ===================================================================
@given("chkcov3 _run_git is mocked to always fail")
def step_chkcov3_mock_run_git_always_fail(context):
"""Patch _run_git on the service to always raise."""
patcher = patch.object(
context.chkcov3_service,
"_run_git",
side_effect=BusinessRuleViolation("Git operation failed: mocked failure"),
)
patcher.start()
context.add_cleanup(patcher.stop)
@when("chkcov3 I call selective_rollback for the plan and checkpoint")
def step_chkcov3_call_selective_rollback(context):
"""Call selective_rollback and capture any exception."""
context.chkcov3_error = None
try:
context.chkcov3_service.selective_rollback(
plan_id=context.chkcov3_plan_id,
checkpoint_id=context.chkcov3_checkpoint_id,
)
except Exception as exc:
context.chkcov3_error = exc
@then("chkcov3 an exception should be stored on context")
def step_chkcov3_exception_stored(context):
"""Verify an exception was captured."""
assert context.chkcov3_error is not None, (
"Expected an exception but none was raised"
)
@then("chkcov3 the stored exception should be a BusinessRuleViolation")
def step_chkcov3_exception_is_brv(context):
"""Verify the captured exception is a BusinessRuleViolation."""
assert isinstance(context.chkcov3_error, BusinessRuleViolation), (
f"Expected BusinessRuleViolation, got {type(context.chkcov3_error).__name__}: "
f"{context.chkcov3_error}"
)
# ===================================================================
# Scenario: selective_rollback recovery failure (453-458, 460)
# ===================================================================
@given("chkcov3 _run_git is mocked to succeed once then fail thereafter")
def step_chkcov3_mock_run_git_succeed_then_fail(context):
"""Patch _run_git to succeed on the first call (rev-parse HEAD),
then raise on all subsequent calls (rollback + recovery)."""
call_count = {"n": 0}
original_error = BusinessRuleViolation("Git operation failed: mocked failure")
def _side_effect(args, cwd):
call_count["n"] += 1
if call_count["n"] == 1:
# First call is rev-parse HEAD — return a fake result
mock_result = MagicMock()
mock_result.stdout = "abc123fakeheadref\n"
mock_result.stderr = ""
mock_result.returncode = 0
return mock_result
# All subsequent calls fail
raise original_error
patcher = patch.object(
context.chkcov3_service,
"_run_git",
side_effect=_side_effect,
)
patcher.start()
context.add_cleanup(patcher.stop)
# ===================================================================
# Scenario: archive_artifacts with explicit archive_dir (line 487)
# ===================================================================
@given("chkcov3 a temporary sandbox with artifact files")
def step_chkcov3_sandbox_with_artifacts(context):
"""Create a temp sandbox dir with some artifact files."""
tmpdir = tempfile.mkdtemp(prefix="chkcov3_archive_sandbox_")
context.chkcov3_archive_sandbox = tmpdir
# Create artifact files
artifact_dir = os.path.join(tmpdir, "output")
os.makedirs(artifact_dir, exist_ok=True)
context.chkcov3_artifact_paths = ["output/result.txt", "output/data.json"]
for rel_path in context.chkcov3_artifact_paths:
full_path = os.path.join(tmpdir, rel_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(f"content of {rel_path}")
def _cleanup():
shutil.rmtree(tmpdir, ignore_errors=True)
context.add_cleanup(_cleanup)
@given("chkcov3 an explicit archive directory")
def step_chkcov3_explicit_archive_dir(context):
"""Create a separate temp directory for the archive."""
tmpdir = tempfile.mkdtemp(prefix="chkcov3_archive_dest_")
context.chkcov3_archive_dir = tmpdir
def _cleanup():
shutil.rmtree(tmpdir, ignore_errors=True)
context.add_cleanup(_cleanup)
@when("chkcov3 I call archive_artifacts with the explicit archive_dir")
def step_chkcov3_call_archive_artifacts(context):
"""Call archive_artifacts with the explicit archive_dir parameter."""
context.chkcov3_archived = context.chkcov3_service.archive_artifacts(
sandbox_path=context.chkcov3_archive_sandbox,
artifact_paths=context.chkcov3_artifact_paths,
archive_dir=context.chkcov3_archive_dir,
)
@then("chkcov3 the archived list should contain the artifact paths")
def step_chkcov3_archived_list_check(context):
"""Verify the returned list contains the artifact relative paths."""
for rel_path in context.chkcov3_artifact_paths:
assert rel_path in context.chkcov3_archived, (
f"Expected '{rel_path}' in archived list, got {context.chkcov3_archived}"
)
@then("chkcov3 the artifacts should exist in the explicit archive directory")
def step_chkcov3_artifacts_in_archive(context):
"""Verify the artifact files were moved to the explicit archive dir."""
for rel_path in context.chkcov3_artifact_paths:
dest = os.path.join(context.chkcov3_archive_dir, rel_path)
assert os.path.exists(dest), (
f"Expected archived file at {dest} but it does not exist"
)
# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when refs match (541)
# ===================================================================
@given('chkcov3 a diff-plan checkpoint with ref "{ref}"')
def step_chkcov3_create_diff_plan_checkpoint(context, ref):
"""Create a checkpoint with a specific sandbox_ref for a diff test plan."""
context.chkcov3_diff_plan_id = _DIFF_PLAN_ID
context.chkcov3_service.create_checkpoint(
plan_id=context.chkcov3_diff_plan_id,
sandbox_ref=ref,
reason="chkcov3 diff test checkpoint",
)
@when('chkcov3 I call _compute_diff_snapshot with current_ref "{ref}"')
def step_chkcov3_call_compute_diff_same_ref(context, ref):
"""Call _compute_diff_snapshot with a ref that matches the existing checkpoint."""
context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
plan_id=context.chkcov3_diff_plan_id,
current_ref=ref,
)
@then("chkcov3 the diff result should be an empty list")
def step_chkcov3_diff_result_empty(context):
"""Verify the diff result is an empty list."""
assert context.chkcov3_diff_result == [], (
f"Expected empty list, got {context.chkcov3_diff_result}"
)
# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when sandbox missing (546, 548)
# ===================================================================
@given('chkcov3 an unregistered plan "{plan_id}" has a checkpoint with ref "{ref}"')
def step_chkcov3_create_checkpoint_for_unregistered_plan(context, plan_id, ref):
"""Create a checkpoint for a plan that has no sandbox registered.
The plan_id from the feature is ignored; we use a valid ULID instead."""
context.chkcov3_no_sandbox_plan_id = _NO_SANDBOX_PLAN_ID
context.chkcov3_service.create_checkpoint(
plan_id=_NO_SANDBOX_PLAN_ID,
sandbox_ref=ref,
reason="chkcov3 no-sandbox test",
)
@when(
'chkcov3 I call _compute_diff_snapshot for plan "{plan_id}" with current_ref "{ref}"'
)
def step_chkcov3_call_compute_diff_for_plan(context, plan_id, ref):
"""Call _compute_diff_snapshot for a plan that has no sandbox registered.
Uses the ULID plan stored on context rather than the feature string."""
actual_plan_id = getattr(context, "chkcov3_no_sandbox_plan_id", _NO_SANDBOX_PLAN_ID)
context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
plan_id=actual_plan_id,
current_ref=ref,
)
# ===================================================================
# Scenario: _compute_diff_snapshot returns [] when git diff fails (556-566)
# ===================================================================
@given('chkcov3 the sandbox plan has a checkpoint with ref "{ref}"')
def step_chkcov3_create_checkpoint_for_sandbox_plan(context, ref):
"""Create a checkpoint for the sandbox plan with a specific ref."""
context.chkcov3_service.create_checkpoint(
plan_id=context.chkcov3_plan_id,
sandbox_ref=ref,
reason="chkcov3 diff-fail test",
)
@given("chkcov3 _run_git is mocked to raise on diff command")
def step_chkcov3_mock_run_git_diff_fail(context):
"""Patch _run_git so it raises when called (for the diff in _compute_diff_snapshot)."""
patcher = patch.object(
context.chkcov3_service,
"_run_git",
side_effect=BusinessRuleViolation("Git operation failed: diff error"),
)
patcher.start()
context.add_cleanup(patcher.stop)
@when(
'chkcov3 I call _compute_diff_snapshot for the sandbox plan with current_ref "{ref}"'
)
def step_chkcov3_call_compute_diff_sandbox_plan(context, ref):
"""Call _compute_diff_snapshot for the sandbox plan with a different ref."""
context.chkcov3_diff_result = context.chkcov3_service._compute_diff_snapshot(
plan_id=context.chkcov3_plan_id,
current_ref=ref,
)