"""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, )