66850665b7
CI / push-validation (pull_request) Successful in 10s
CI / helm (pull_request) Successful in 33s
CI / lint (pull_request) Successful in 44s
CI / quality (pull_request) Successful in 48s
CI / security (pull_request) Successful in 54s
CI / e2e_tests (pull_request) Successful in 3m14s
CI / build (pull_request) Successful in 3m25s
CI / typecheck (pull_request) Successful in 4m1s
CI / integration_tests (pull_request) Successful in 6m33s
CI / unit_tests (pull_request) Successful in 7m48s
CI / docker (pull_request) Successful in 55s
CI / coverage (pull_request) Successful in 5m58s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (push) Failing after 0s
CI / benchmark-publish (push) Failing after 0s
CI / push-validation (push) Successful in 13s
CI / lint (push) Successful in 19s
CI / helm (push) Successful in 24s
CI / build (push) Successful in 31s
CI / quality (push) Successful in 39s
CI / typecheck (push) Successful in 42s
CI / security (push) Successful in 43s
CI / e2e_tests (push) Successful in 3m20s
CI / unit_tests (push) Successful in 3m36s
CI / integration_tests (push) Successful in 4m34s
CI / docker (push) Successful in 1m38s
CI / coverage (push) Successful in 9m25s
CI / status-check (push) Successful in 1s
Split checkpoint_manager_coverage_steps.py (692 lines) into three focused modules to comply with the 500-line file limit: - checkpoint_manager_coverage_steps.py: model, protocol, manager init, create_checkpoint, _snapshot_directory, and _cleanup_snapshot steps (408 lines) - checkpoint_manager_rollback_steps.py: rollback_to, list_checkpoints, and delete_checkpoint branch steps (283 lines) - checkpoint_manager_bug7488_steps.py: Bug #7488 sandbox_path auto-stored-in-metadata steps (59 lines) Also updated CONTRIBUTORS.md to document HAL 9000's contribution for bug fix #7488 as required by CONTRIBUTING.md guidelines. ISSUES CLOSED: #7488
284 lines
10 KiB
Python
284 lines
10 KiB
Python
"""Step definitions for rollback_to, list_checkpoints, and delete_checkpoint branches.
|
|
|
|
Split from checkpoint_manager_coverage_steps.py to keep files under 500 lines.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.infrastructure.sandbox.checkpoint import SandboxCheckpoint
|
|
|
|
|
|
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 p=path: shutil.rmtree(p, ignore_errors=True)
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# 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"
|