forked from HAL9000/cleveragents-core
31472b5413
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
314 lines
12 KiB
Python
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,
|
|
)
|