Files
cleveragents-core/robot/helper_sandbox_checkpoint.py
freemo 0ca1303927
CI / lint (pull_request) Successful in 23s
CI / quality (pull_request) Successful in 29s
CI / security (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 59s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 25s
CI / integration_tests (pull_request) Successful in 4m33s
CI / benchmark-regression (pull_request) Successful in 22m32s
CI / unit_tests (pull_request) Successful in 30m28s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 1h39m42s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 20s
CI / build (push) Successful in 24s
CI / security (push) Successful in 28s
CI / typecheck (push) Successful in 1m0s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m35s
CI / benchmark-publish (push) Successful in 13m36s
CI / coverage (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / docker (push) Has been cancelled
feat(sandbox): add checkpoint and rollback hooks
Introduce a lightweight checkpoint/rollback system for sandbox state
during plan execute and apply flows.  CheckpointManager snapshots
the sandbox working directory before each phase and can restore it
on failure, giving the execution engine a reliable undo mechanism.

Key changes:
- SandboxCheckpoint model, Checkpointable protocol, and
  CheckpointManager in infrastructure/sandbox/checkpoint.py
- PlanExecutor gains optional checkpoint_manager with pre/post
  execute hooks and automatic rollback on failure
- PlanApplyService gains optional checkpoint_manager with pre-apply
  checkpoint and rollback helper
- 12 BDD scenarios (features/sandbox_checkpoints.feature)
- 5 Robot Framework smoke tests (robot/sandbox_checkpoint_smoke.robot)
- ASV benchmarks for creation, rollback, and listing operations
- Reference documentation in docs/reference/sandbox.md

ISSUES CLOSED: #183
2026-02-27 23:08:55 +00:00

195 lines
5.8 KiB
Python

"""Helper utilities for sandbox checkpoint Robot integration tests.
Covers the M4 checkpoint infrastructure:
- CheckpointManager create/rollback/list/delete
- PlanExecutor checkpoint integration
"""
from __future__ import annotations
import os
import sys
import tempfile
from unittest.mock import MagicMock
from cleveragents.infrastructure.sandbox.checkpoint import (
CheckpointManager,
SandboxCheckpoint,
)
from cleveragents.infrastructure.sandbox.no_sandbox import NoSandbox
def _checkpoint_create() -> None:
"""Integration test: create a checkpoint and verify its fields."""
mgr = CheckpointManager()
with tempfile.TemporaryDirectory() as tmpdir:
# Create a file in the directory
with open(os.path.join(tmpdir, "file.txt"), "w") as f:
f.write("original content\n")
sandbox = NoSandbox(resource_id="res-robot-01", original_path=tmpdir)
sandbox.create(plan_id="plan-robot-01")
cp = mgr.create_checkpoint(
sandbox=sandbox,
plan_id="plan-robot-01",
phase="pre_execute",
metadata={"sandbox_path": tmpdir},
)
assert isinstance(cp, SandboxCheckpoint)
assert cp.phase == "pre_execute"
assert cp.plan_id == "plan-robot-01"
assert cp.sandbox_id == sandbox.sandbox_id
assert len(cp.checkpoint_id) == 26 # ULID length
assert cp.created_at is not None
assert os.path.isdir(cp.snapshot_path)
print("checkpoint-create-ok")
def _checkpoint_rollback() -> None:
"""Integration test: create checkpoint, modify, rollback, verify."""
mgr = CheckpointManager()
with tempfile.TemporaryDirectory() as tmpdir:
# Create original file
file_path = os.path.join(tmpdir, "data.txt")
with open(file_path, "w") as f:
f.write("original\n")
sandbox = NoSandbox(resource_id="res-robot-02", original_path=tmpdir)
sandbox.create(plan_id="plan-robot-02")
# Checkpoint
cp = mgr.create_checkpoint(
sandbox=sandbox,
plan_id="plan-robot-02",
phase="pre_execute",
metadata={"sandbox_path": tmpdir},
)
# Modify the file
with open(file_path, "w") as f:
f.write("modified\n")
with open(file_path) as f:
assert f.read() == "modified\n"
# Add a new file
new_file = os.path.join(tmpdir, "extra.txt")
with open(new_file, "w") as f:
f.write("extra\n")
# Rollback
result = mgr.rollback_to(cp)
assert result is True
# Verify original state restored
with open(file_path) as f:
content = f.read()
assert content == "original\n", f"Expected 'original\\n', got {content!r}"
# New file should be gone
assert not os.path.exists(new_file), "extra.txt should not exist"
print("checkpoint-rollback-ok")
def _checkpoint_listing() -> None:
"""Integration test: multiple checkpoints maintain creation order."""
mgr = CheckpointManager()
with tempfile.TemporaryDirectory() as tmpdir:
sandbox = NoSandbox(resource_id="res-robot-03", original_path=tmpdir)
sandbox.create(plan_id="plan-robot-03")
phases = ["pre_execute", "post_execute", "pre_apply"]
for phase in phases:
mgr.create_checkpoint(
sandbox=sandbox,
plan_id="plan-robot-03",
phase=phase,
)
checkpoints = mgr.list_checkpoints(sandbox.sandbox_id)
assert len(checkpoints) == 3
for i, phase in enumerate(phases):
assert checkpoints[i].phase == phase
# Verify chronological order
for i in range(1, len(checkpoints)):
assert checkpoints[i].created_at >= checkpoints[i - 1].created_at
print("checkpoint-listing-ok")
def _checkpoint_delete() -> None:
"""Integration test: delete a checkpoint and verify removal."""
mgr = CheckpointManager()
with tempfile.TemporaryDirectory() as tmpdir:
sandbox = NoSandbox(resource_id="res-robot-04", original_path=tmpdir)
sandbox.create(plan_id="plan-robot-04")
cp = mgr.create_checkpoint(
sandbox=sandbox,
plan_id="plan-robot-04",
phase="pre_execute",
)
assert len(mgr.list_checkpoints(sandbox.sandbox_id)) == 1
result = mgr.delete_checkpoint(cp.checkpoint_id)
assert result is True
assert len(mgr.list_checkpoints(sandbox.sandbox_id)) == 0
# Deleting again should return False
result2 = mgr.delete_checkpoint(cp.checkpoint_id)
assert result2 is False
print("checkpoint-delete-ok")
def _executor_integration() -> None:
"""Integration test: PlanExecutor accepts optional checkpoint_manager."""
from cleveragents.application.services.plan_executor import PlanExecutor
lifecycle = MagicMock()
# Without checkpoint manager
executor_no_cp = PlanExecutor(lifecycle_service=lifecycle)
assert executor_no_cp.checkpoint_manager is None
# With checkpoint manager
mgr = CheckpointManager()
executor_with_cp = PlanExecutor(
lifecycle_service=lifecycle,
checkpoint_manager=mgr,
)
assert executor_with_cp.checkpoint_manager is mgr
print("executor-integration-ok")
def main() -> None:
if len(sys.argv) < 2:
raise SystemExit("Expected command argument")
command = sys.argv[1]
commands = {
"checkpoint-create": _checkpoint_create,
"checkpoint-rollback": _checkpoint_rollback,
"checkpoint-listing": _checkpoint_listing,
"checkpoint-delete": _checkpoint_delete,
"executor-integration": _executor_integration,
}
if command not in commands:
raise SystemExit(f"Unknown command: {command}")
commands[command]()
if __name__ == "__main__":
main()