Files
cleveragents-core/features/steps/checkpoint_manager_coverage_steps.py
T
freemo 2eb31a598c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 19s
CI / security (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 38s
CI / integration_tests (pull_request) Successful in 2m44s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 23s
CI / typecheck (push) Successful in 31s
CI / security (push) Successful in 32s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 3m25s
CI / unit_tests (pull_request) Successful in 13m3s
CI / docker (pull_request) Successful in 1m2s
CI / benchmark-publish (push) Successful in 16m35s
CI / benchmark-regression (pull_request) Successful in 22m21s
CI / unit_tests (push) Successful in 24m45s
CI / docker (push) Successful in 8s
CI / coverage (pull_request) Successful in 1h1m47s
CI / coverage (push) Successful in 1h11m37s
test(coverage): add Behave BDD tests for 8 under-covered modules
Added targeted Behave BDD feature files and step definitions to improve
unit test coverage for:

- decision_service.py: Full coverage of all 7 service methods (18 scenarios)
- plan_apply_service.py: Branch coverage for handle_merge_failure (2 scenarios)
- plan_executor.py: Edge cases for rollback, checkpoint, and parse_steps (15 scenarios)
- cli/commands/plan.py: Uncovered region lines 1950-2273 (23 scenarios)
- repositories.py: Remaining missed branches and lines (14 scenarios)
- sandbox/checkpoint.py: Full coverage of CheckpointManager (26 scenarios)
- langgraph/bridge.py: Remaining uncovered lines and branches (10 scenarios)
- cli/commands/config.py: Safety net to maintain 100% coverage (42 scenarios)

Total: 150 new scenarios, 596 steps, all passing.

Also fixed a step definition collision in plan_lifecycle_coverage by renaming
"the delete result should be false" to "the plan delete result should be false".

ISSUES CLOSED: #475
2026-03-01 03:09:51 +00:00

653 lines
23 KiB
Python

"""Step definitions for checkpoint_manager_coverage.feature.
Targets every method, branch, and edge case in checkpoint.py to achieve
full line-rate and branch-rate coverage.
"""
from __future__ import annotations
import os
import shutil
import tempfile
import threading
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.infrastructure.sandbox.checkpoint import (
Checkpointable,
CheckpointManager,
SandboxCheckpoint,
)
from cleveragents.infrastructure.sandbox.protocol import SandboxError
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_mock_sandbox(
sandbox_id: str = "sb-mock-001",
context: Any = None,
) -> MagicMock:
"""Build a mock that satisfies the Checkpointable protocol."""
sb = MagicMock()
sb.sandbox_id = sandbox_id
sb.context = context
return sb
def _make_temp_sandbox_dir() -> str:
"""Create a temp dir with sample files and a subdirectory."""
tmpdir = tempfile.mkdtemp(prefix="ca-cpmgr-test-")
os.makedirs(os.path.join(tmpdir, "subdir"), exist_ok=True)
with open(os.path.join(tmpdir, "file.txt"), "w") as f:
f.write("original\n")
with open(os.path.join(tmpdir, "subdir", "nested.txt"), "w") as f:
f.write("nested-original\n")
return tmpdir
def _register_cleanup(context: Context, path: str) -> None:
"""Schedule a directory for cleanup after the scenario."""
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(lambda: shutil.rmtree(path, ignore_errors=True))
# ===========================================================================
# SandboxCheckpoint Pydantic model
# ===========================================================================
@given("I build a SandboxCheckpoint with valid fields")
def step_build_checkpoint_model(context: Context) -> None:
context.cp_model = SandboxCheckpoint(
checkpoint_id="cp-aaa",
sandbox_id="sb-bbb",
plan_id="plan-ccc",
phase="pre_execute",
created_at=datetime(2025, 1, 1, 12, 0, 0),
metadata={"key": "val"},
snapshot_path="/tmp/snap",
)
@then("the constructed checkpoint model should expose all field values")
def step_assert_checkpoint_fields(context: Context) -> None:
cp = context.cp_model
assert cp.checkpoint_id == "cp-aaa"
assert cp.sandbox_id == "sb-bbb"
assert cp.plan_id == "plan-ccc"
assert cp.phase == "pre_execute"
assert cp.created_at == datetime(2025, 1, 1, 12, 0, 0)
assert cp.metadata == {"key": "val"}
assert cp.snapshot_path == "/tmp/snap"
@then("the constructed checkpoint model should be frozen")
def step_assert_checkpoint_frozen(context: Context) -> None:
try:
context.cp_model.phase = "modified"
raise AssertionError("Expected ValidationError for frozen model")
except Exception:
pass # pydantic frozen model rejects mutation
@given("I build a SandboxCheckpoint without explicit metadata")
def step_build_checkpoint_no_metadata(context: Context) -> None:
context.cp_model = SandboxCheckpoint(
checkpoint_id="cp-ddd",
sandbox_id="sb-eee",
plan_id="plan-fff",
phase="post_execute",
created_at=datetime.now(),
snapshot_path="/tmp/snap2",
)
@then("the constructed checkpoint metadata should be an empty dict")
def step_assert_empty_metadata_default(context: Context) -> None:
assert context.cp_model.metadata == {}
# ===========================================================================
# Checkpointable protocol
# ===========================================================================
@given("a mock object with sandbox_id and context properties")
def step_mock_checkpointable(context: Context) -> None:
class _Good:
@property
def sandbox_id(self) -> str:
return "sb-proto"
@property
def context(self) -> Any:
return None
context.proto_obj = _Good()
@then("the mock object should satisfy the Checkpointable protocol")
def step_assert_satisfies_checkpointable(context: Context) -> None:
assert isinstance(context.proto_obj, Checkpointable)
@given("a mock object without a sandbox_id property")
def step_mock_not_checkpointable(context: Context) -> None:
class _Bad:
@property
def context(self) -> Any:
return None
context.proto_obj = _Bad()
@then("the mock object should not satisfy the Checkpointable protocol")
def step_assert_not_checkpointable(context: Context) -> None:
assert not isinstance(context.proto_obj, Checkpointable)
# ===========================================================================
# CheckpointManager.__init__
# ===========================================================================
@given("a freshly created CheckpointManager")
def step_fresh_manager(context: Context) -> None:
context.mgr = CheckpointManager()
context.crafted_cp = None
context.cp_result = None
context.rollback_result = None
context.delete_result = None
context.cp_list = None
context.snapshot_result = None
context.raised_error = None
context.cleanup_error = None
@then("the manager internal checkpoints dict should be empty")
def step_assert_empty_checkpoints(context: Context) -> None:
assert context.mgr._checkpoints == {}
@then("the manager should have a threading lock")
def step_assert_has_lock(context: Context) -> None:
assert isinstance(context.mgr._lock, type(threading.RLock()))
# ===========================================================================
# create_checkpoint - context is None
# ===========================================================================
@given("a mock sandbox whose context is None")
def step_mock_sandbox_no_context(context: Context) -> None:
context.mock_sb = _make_mock_sandbox(sandbox_id="sb-nocontext")
@when(
'I invoke create_checkpoint with plan "{plan_id}" phase "{phase}" and no metadata'
)
def step_create_cp_no_metadata(context: Context, plan_id: str, phase: str) -> None:
context.cp_result = context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id=plan_id,
phase=phase,
)
@then("the returned checkpoint should have a non-empty snapshot_path directory")
def step_assert_snapshot_path_dir(context: Context) -> None:
sp = context.cp_result.snapshot_path
assert sp, "snapshot_path should not be empty"
assert os.path.isdir(sp), f"Expected directory at {sp}"
_register_cleanup(context, os.path.dirname(sp))
@then("the returned checkpoint sandbox_id should equal the mock sandbox id")
def step_assert_cp_sandbox_id(context: Context) -> None:
assert context.cp_result.sandbox_id == context.mock_sb.sandbox_id
@when(
'I invoke create_checkpoint with plan "{plan_id}" phase "{phase}" and numeric metadata'
)
def step_create_cp_numeric_metadata(context: Context, plan_id: str, phase: str) -> None:
context.cp_result = context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id=plan_id,
phase=phase,
metadata={"count": 42, "flag": True},
)
_register_cleanup(context, os.path.dirname(context.cp_result.snapshot_path))
@then("the returned checkpoint metadata values should all be strings")
def step_assert_metadata_strings(context: Context) -> None:
for k, v in context.cp_result.metadata.items():
assert isinstance(v, str), f"metadata[{k!r}] = {v!r} is not a string"
@when(
'I invoke create_checkpoint with plan "{plan_id}" phase "{phase}" and None metadata'
)
def step_create_cp_none_metadata(context: Context, plan_id: str, phase: str) -> None:
context.cp_result = context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id=plan_id,
phase=phase,
metadata=None,
)
_register_cleanup(context, os.path.dirname(context.cp_result.snapshot_path))
@then("the returned checkpoint metadata should be an empty dict")
def step_assert_cp_metadata_empty(context: Context) -> None:
assert context.cp_result.metadata == {}
# ===========================================================================
# create_checkpoint - context with sandbox_path
# ===========================================================================
@given("a temporary sandbox directory with sample files")
def step_temp_sandbox_dir(context: Context) -> None:
context.temp_sandbox = _make_temp_sandbox_dir()
_register_cleanup(context, context.temp_sandbox)
@given("a mock sandbox whose context points to the temporary directory")
def step_mock_sandbox_with_context(context: Context) -> None:
ctx = MagicMock()
ctx.sandbox_path = context.temp_sandbox
context.mock_sb = _make_mock_sandbox(sandbox_id="sb-withctx", context=ctx)
@then("the snapshot directory should contain copies of the sample files")
def step_assert_snapshot_has_files(context: Context) -> None:
snap = context.cp_result.snapshot_path
_register_cleanup(context, os.path.dirname(snap))
assert os.path.isfile(os.path.join(snap, "file.txt"))
assert os.path.isfile(os.path.join(snap, "subdir", "nested.txt"))
with open(os.path.join(snap, "file.txt")) as f:
assert f.read() == "original\n"
# ===========================================================================
# _snapshot_directory - branches
# ===========================================================================
@when("I call _snapshot_directory with a None sandbox_path")
def step_snapshot_none_path(context: Context) -> None:
context.snapshot_result = context.mgr._snapshot_directory(None, "cp-test-none")
_register_cleanup(context, os.path.dirname(context.snapshot_result))
@then("the returned snapshot should be an existing empty directory")
def step_assert_snapshot_empty_dir(context: Context) -> None:
sp = context.snapshot_result
assert os.path.isdir(sp), f"Expected directory at {sp}"
assert os.listdir(sp) == [], f"Expected empty dir, got {os.listdir(sp)}"
@when("I call _snapshot_directory with a non-existent sandbox_path")
def step_snapshot_nonexistent_path(context: Context) -> None:
context.snapshot_result = context.mgr._snapshot_directory(
"/nonexistent/path/xyz", "cp-test-noexist"
)
_register_cleanup(context, os.path.dirname(context.snapshot_result))
@when("I call _snapshot_directory with the temporary sandbox_path")
def step_snapshot_valid_path(context: Context) -> None:
context.snapshot_result = context.mgr._snapshot_directory(
context.temp_sandbox, "cp-test-valid"
)
_register_cleanup(context, os.path.dirname(context.snapshot_result))
@then("the returned snapshot should contain the sample files")
def step_assert_snapshot_has_sample_files(context: Context) -> None:
sp = context.snapshot_result
assert os.path.isfile(os.path.join(sp, "file.txt"))
assert os.path.isfile(os.path.join(sp, "subdir", "nested.txt"))
@when("I call _snapshot_directory with a path that triggers OSError")
def step_snapshot_oserror(context: Context) -> None:
context.raised_error = None
with patch("shutil.copytree", side_effect=OSError("mock copy failure")):
try:
context.mgr._snapshot_directory(context.temp_sandbox, "cp-test-err")
except SandboxError as exc:
context.raised_error = exc
@then("a SandboxError should have been raised")
def step_assert_sandbox_error(context: Context) -> None:
assert context.raised_error is not None, "Expected SandboxError to be raised"
assert isinstance(context.raised_error, SandboxError)
assert "Failed to snapshot" in str(context.raised_error)
# ===========================================================================
# _cleanup_snapshot - branches
# ===========================================================================
@when("I call _cleanup_snapshot with an empty string")
def step_cleanup_empty_string(context: Context) -> None:
context.cleanup_error = None
try:
CheckpointManager._cleanup_snapshot("")
except Exception as exc:
context.cleanup_error = exc
@then("no error should occur from cleanup_snapshot")
def step_assert_no_cleanup_error(context: Context) -> None:
assert context.cleanup_error is None, (
f"Expected no error, got {context.cleanup_error!r}"
)
@given("a temporary snapshot directory tree for cleanup")
def step_temp_snapshot_for_cleanup(context: Context) -> None:
parent = tempfile.mkdtemp(prefix="ca-cleanup-test-")
snap = os.path.join(parent, "snapshot")
os.makedirs(snap, exist_ok=True)
with open(os.path.join(snap, "data.txt"), "w") as f:
f.write("to-be-cleaned\n")
context.cleanup_parent = parent
context.cleanup_snapshot = snap
@when("I call _cleanup_snapshot with the temporary snapshot path")
def step_cleanup_real_snapshot(context: Context) -> None:
context.cleanup_error = None
try:
CheckpointManager._cleanup_snapshot(context.cleanup_snapshot)
except Exception as exc:
context.cleanup_error = exc
@then("the temporary snapshot parent should no longer exist")
def step_assert_parent_removed(context: Context) -> None:
assert not os.path.exists(context.cleanup_parent), (
f"Parent should have been removed: {context.cleanup_parent}"
)
@when("I call _cleanup_snapshot with a non-existent path")
def step_cleanup_nonexistent(context: Context) -> None:
context.cleanup_error = None
try:
CheckpointManager._cleanup_snapshot("/nonexistent/snapshot/path")
except Exception as exc:
context.cleanup_error = exc
# ===========================================================================
# rollback_to - all branches
# ===========================================================================
@given("a SandboxCheckpoint with empty snapshot_path")
def step_cp_empty_snapshot(context: Context) -> None:
context.crafted_cp = SandboxCheckpoint(
checkpoint_id="cp-empty-snap",
sandbox_id="sb-rb1",
plan_id="plan-rb1",
phase="pre_execute",
created_at=datetime.now(),
metadata={},
snapshot_path="",
)
@when("I invoke rollback_to on the crafted checkpoint")
def step_rollback_crafted(context: Context) -> None:
if getattr(context, "rollback_should_patch_oserror", False):
# Patch os.listdir to raise on the second call (copy phase)
orig_listdir = os.listdir
call_count = {"n": 0}
def _failing_listdir(path: str) -> list[str]:
call_count["n"] += 1
if call_count["n"] >= 2:
raise OSError("simulated listdir failure")
return orig_listdir(path)
with patch("os.listdir", side_effect=_failing_listdir):
context.rollback_result = context.mgr.rollback_to(context.crafted_cp)
else:
context.rollback_result = context.mgr.rollback_to(context.crafted_cp)
@then("the rollback result should be false")
def step_assert_rollback_false(context: Context) -> None:
assert context.rollback_result is False, "Expected rollback to return False"
@given("a SandboxCheckpoint with a non-existent snapshot_path")
def step_cp_nonexistent_snapshot(context: Context) -> None:
context.crafted_cp = SandboxCheckpoint(
checkpoint_id="cp-nosnap",
sandbox_id="sb-rb2",
plan_id="plan-rb2",
phase="pre_execute",
created_at=datetime.now(),
metadata={},
snapshot_path="/nonexistent/snapshot/dir",
)
@given("a SandboxCheckpoint whose snapshot exists but metadata has no sandbox_path")
def step_cp_no_sandbox_path_meta(context: Context) -> None:
# Create a real snapshot dir so the first guard passes
snap_parent = tempfile.mkdtemp(prefix="ca-rb-test-")
snap = os.path.join(snap_parent, "snapshot")
os.makedirs(snap, exist_ok=True)
_register_cleanup(context, snap_parent)
context.crafted_cp = SandboxCheckpoint(
checkpoint_id="cp-nometa",
sandbox_id="sb-rb3",
plan_id="plan-rb3",
phase="pre_execute",
created_at=datetime.now(),
metadata={}, # no sandbox_path key
snapshot_path=snap,
)
@given("a SandboxCheckpoint whose snapshot exists but sandbox_path points nowhere")
def step_cp_sandbox_path_missing(context: Context) -> None:
snap_parent = tempfile.mkdtemp(prefix="ca-rb-test2-")
snap = os.path.join(snap_parent, "snapshot")
os.makedirs(snap, exist_ok=True)
_register_cleanup(context, snap_parent)
context.crafted_cp = SandboxCheckpoint(
checkpoint_id="cp-badpath",
sandbox_id="sb-rb4",
plan_id="plan-rb4",
phase="pre_execute",
created_at=datetime.now(),
metadata={"sandbox_path": "/nonexistent/sandbox/dir"},
snapshot_path=snap,
)
@given("a checkpoint was created for the temporary sandbox")
def step_create_cp_for_temp_sandbox(context: Context) -> None:
context.cp_result = context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id="plan-rb-ok",
phase="pre_execute",
metadata={"sandbox_path": context.temp_sandbox},
)
_register_cleanup(context, os.path.dirname(context.cp_result.snapshot_path))
context.crafted_cp = context.cp_result
@given("the temporary sandbox files are then modified")
def step_modify_temp_sandbox(context: Context) -> None:
# Overwrite existing file
with open(os.path.join(context.temp_sandbox, "file.txt"), "w") as f:
f.write("modified!\n")
# Add a new file
with open(os.path.join(context.temp_sandbox, "extra.txt"), "w") as f:
f.write("extra\n")
# Add a new subdirectory with file
new_sub = os.path.join(context.temp_sandbox, "newdir")
os.makedirs(new_sub, exist_ok=True)
with open(os.path.join(new_sub, "added.txt"), "w") as f:
f.write("added\n")
@when("I invoke rollback_to on the created checkpoint")
def step_rollback_created_cp(context: Context) -> None:
context.rollback_result = context.mgr.rollback_to(context.cp_result)
@then("the rollback result should be true")
def step_assert_rollback_true(context: Context) -> None:
assert context.rollback_result is True, "Expected rollback to return True"
@then("the temporary sandbox should contain the original files")
def step_assert_temp_sandbox_restored(context: Context) -> None:
with open(os.path.join(context.temp_sandbox, "file.txt")) as f:
assert f.read() == "original\n", "file.txt should be restored"
with open(os.path.join(context.temp_sandbox, "subdir", "nested.txt")) as f:
assert f.read() == "nested-original\n", "nested.txt should be restored"
assert not os.path.exists(os.path.join(context.temp_sandbox, "extra.txt")), (
"extra.txt should not exist after rollback"
)
assert not os.path.exists(os.path.join(context.temp_sandbox, "newdir")), (
"newdir should not exist after rollback"
)
@given("a SandboxCheckpoint whose snapshot and sandbox exist but restore will fail")
def step_cp_rollback_oserror(context: Context) -> None:
# Create a real snapshot dir with a file
snap_parent = tempfile.mkdtemp(prefix="ca-rb-err-")
snap = os.path.join(snap_parent, "snapshot")
os.makedirs(snap, exist_ok=True)
with open(os.path.join(snap, "ok.txt"), "w") as f:
f.write("ok\n")
# Create a real sandbox dir
sandbox_dir = tempfile.mkdtemp(prefix="ca-rb-err-sb-")
with open(os.path.join(sandbox_dir, "existing.txt"), "w") as f:
f.write("existing\n")
_register_cleanup(context, snap_parent)
_register_cleanup(context, sandbox_dir)
context.crafted_cp = SandboxCheckpoint(
checkpoint_id="cp-oserr",
sandbox_id="sb-rb-err",
plan_id="plan-rb-err",
phase="pre_execute",
created_at=datetime.now(),
metadata={"sandbox_path": sandbox_dir},
snapshot_path=snap,
)
# Flag the OSError scenario so the when step knows to patch
context.rollback_should_patch_oserror = True
# ===========================================================================
# list_checkpoints - branches
# ===========================================================================
@when('I invoke list_checkpoints for sandbox "{sandbox_id}"')
def step_list_checkpoints_by_id(context: Context, sandbox_id: str) -> None:
context.cp_list = context.mgr.list_checkpoints(sandbox_id)
@then("the checkpoint list should be empty")
def step_assert_list_empty(context: Context) -> None:
assert context.cp_list == [], f"Expected empty list, got {context.cp_list}"
@given("I create two checkpoints for the mock sandbox")
def step_create_two_checkpoints(context: Context) -> None:
context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id="plan-list",
phase="pre_execute",
)
context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id="plan-list",
phase="post_execute",
)
@when("I invoke list_checkpoints for the mock sandbox")
def step_list_checkpoints_mock_sb(context: Context) -> None:
context.cp_list = context.mgr.list_checkpoints(context.mock_sb.sandbox_id)
@then("the checkpoint list should have {count:d} entries in creation order")
def step_assert_list_count_ordered(context: Context, count: int) -> None:
assert len(context.cp_list) == count, (
f"Expected {count} entries, got {len(context.cp_list)}"
)
for i in range(1, len(context.cp_list)):
assert context.cp_list[i].created_at >= context.cp_list[i - 1].created_at
# ===========================================================================
# delete_checkpoint - branches
# ===========================================================================
@given("a single checkpoint is created for the mock sandbox")
def step_create_single_cp(context: Context) -> None:
context.cp_result = context.mgr.create_checkpoint(
sandbox=context.mock_sb,
plan_id="plan-del",
phase="pre_execute",
)
_register_cleanup(context, os.path.dirname(context.cp_result.snapshot_path))
@when("I invoke delete_checkpoint with the created checkpoint id")
def step_delete_created_cp(context: Context) -> None:
context.delete_result = context.mgr.delete_checkpoint(
context.cp_result.checkpoint_id
)
@then("the checkpoint delete result should be true")
def step_assert_delete_true(context: Context) -> None:
assert context.delete_result is True, "Expected delete to return True"
@then("listing checkpoints for the mock sandbox should return {count:d} entries")
def step_assert_list_after_delete(context: Context, count: int) -> None:
result = context.mgr.list_checkpoints(context.mock_sb.sandbox_id)
assert len(result) == count, f"Expected {count} entries, got {len(result)}"
@when('I invoke delete_checkpoint with id "{cp_id}"')
def step_delete_unknown_cp(context: Context, cp_id: str) -> None:
context.delete_result = context.mgr.delete_checkpoint(cp_id)
@then("the checkpoint delete result should be false")
def step_assert_delete_false(context: Context) -> None:
assert context.delete_result is False, "Expected delete to return False"