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
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
195 lines
5.8 KiB
Python
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()
|