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
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
653 lines
23 KiB
Python
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"
|