Files
cleveragents-core/robot/helper_checkpoint_rollback.py
freemo ec4c39aecf
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 17s
CI / build (pull_request) Successful in 28s
CI / quality (pull_request) Successful in 34s
CI / security (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 45s
CI / unit_tests (pull_request) Successful in 3m11s
CI / integration_tests (pull_request) Successful in 3m37s
CI / e2e_tests (pull_request) Successful in 4m1s
CI / docker (pull_request) Successful in 56s
CI / coverage (pull_request) Successful in 6m37s
CI / lint (push) Successful in 11s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 32s
CI / typecheck (push) Successful in 44s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 56s
CI / unit_tests (push) Successful in 2m55s
CI / integration_tests (push) Successful in 3m34s
CI / docker (push) Successful in 59s
CI / e2e_tests (push) Successful in 3m59s
CI / coverage (push) Successful in 6m55s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 37m40s
fix(plan): implement real checkpoint rollback via git reset
Replace the simulated rollback in CheckpointService.rollback_to_checkpoint()
with real git operations. The method now executes git reset --hard <sandbox_ref>
followed by git clean -fd inside the sandbox working directory, reverting all
tracked file changes and removing untracked files added after the checkpoint.

Key changes:
- CheckpointService.rollback_to_checkpoint() now calls _git_reset_hard() and
  _git_clean() via subprocess against the sandbox path, enforcing sandbox
  boundary by confining all git operations to cwd=sandbox_path.
- Added _resolve_sandbox_path() to extract sandbox validation into a dedicated
  method, supporting both lifecycle-service and in-memory fallback paths.
- Added _validate_sandbox() to verify the sandbox path is a directory containing
  a .git subdirectory before executing git operations.
- Added _git_changed_paths() to compute the diff between HEAD and the target ref
  before reset, providing accurate restored file counts in the result.
- Domain event emission (CHECKPOINT_RESTORED) added via the optional event_bus,
  following the same EventBus protocol pattern used by other services.
- Updated existing Robot Framework helper (helper_checkpoint_rollback.py) to use
  a real temporary git workspace instead of a fake sandbox path, since
  _validate_sandbox() now enforces that the path is a real git repository.
- Removed @tdd_expected_fail tags from TDD test files since the bug is now fixed.
- Added new Behave scenarios and Robot integration tests for real file reversion,
  file removal after rollback, domain event emission, and sandbox boundary
  enforcement.

ISSUES CLOSED: #822
2026-03-18 16:51:14 +00:00

260 lines
8.1 KiB
Python

"""Helper script for checkpoint/rollback Robot Framework smoke tests.
Exercises CheckpointService operations: create, list, rollback,
prune, delete, guards, metadata, and correction-service integration.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.core.exceptions import BusinessRuleViolation, ResourceNotFoundError
from cleveragents.domain.models.core.checkpoint import CheckpointRetentionPolicy
def _create_temp_git_workspace() -> str:
"""Create a temporary git workspace with an initial commit."""
tmpdir = tempfile.mkdtemp(prefix="checkpoint_rollback_test_")
subprocess.run(
["git", "init", "--initial-branch=main", tmpdir],
check=True,
capture_output=True,
)
subprocess.run(
["git", "config", "user.email", "test@example.com"],
cwd=tmpdir,
check=True,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmpdir,
check=True,
capture_output=True,
)
tracked = os.path.join(tmpdir, "tracked.txt")
Path(tracked).write_text("initial content\n")
subprocess.run(
["git", "add", "tracked.txt"],
cwd=tmpdir,
check=True,
capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "Initial commit"],
cwd=tmpdir,
check=True,
capture_output=True,
)
return tmpdir
def _get_head_sha(cwd: str) -> str:
"""Return HEAD commit SHA."""
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=cwd,
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
def _create_checkpoint() -> None:
svc = CheckpointService()
cp = svc.create_checkpoint(_PLAN_ID, "commit-abc")
assert cp.checkpoint_id
assert cp.plan_id == _PLAN_ID
print("create-checkpoint-ok")
def _list_checkpoints() -> None:
svc = CheckpointService()
svc.create_checkpoint(_PLAN_ID, "commit-1")
svc.create_checkpoint(_PLAN_ID, "commit-2")
cps = svc.list_checkpoints(_PLAN_ID)
assert len(cps) == 2
print("list-checkpoints-ok")
def _rollback_checkpoint() -> None:
tmpdir = _create_temp_git_workspace()
try:
initial_sha = _get_head_sha(tmpdir)
svc = CheckpointService()
svc.register_sandbox(_PLAN_ID, tmpdir)
cp = svc.create_checkpoint(_PLAN_ID, initial_sha)
# Modify the tracked file and commit
Path(os.path.join(tmpdir, "tracked.txt")).write_text("modified\n")
subprocess.run(
["git", "add", "tracked.txt"],
cwd=tmpdir,
check=True,
capture_output=True,
)
subprocess.run(
["git", "commit", "-m", "Modify tracked file"],
cwd=tmpdir,
check=True,
capture_output=True,
)
result = svc.rollback_to_checkpoint(_PLAN_ID, cp.checkpoint_id)
assert result.restored_files_count > 0
assert result.from_checkpoint_id == cp.checkpoint_id
# Verify actual file reversion
content = Path(os.path.join(tmpdir, "tracked.txt")).read_text()
assert content == "initial content\n", (
f"Expected file reverted, got: {content!r}"
)
print("rollback-checkpoint-ok")
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def _rollback_applied_guard() -> None:
svc = CheckpointService()
svc.register_sandbox(_PLAN_ID, "sandbox-123")
svc.mark_plan_applied(_PLAN_ID)
cp = svc.create_checkpoint(_PLAN_ID, "commit-abc")
try:
svc.rollback_to_checkpoint(_PLAN_ID, cp.checkpoint_id)
msg = "Should have raised"
raise AssertionError(msg)
except BusinessRuleViolation as e:
assert "applied" in str(e)
print("rollback-applied-guard-ok")
def _rollback_sandbox_guard() -> None:
svc = CheckpointService()
cp = svc.create_checkpoint(_PLAN_ID, "commit-abc")
try:
svc.rollback_to_checkpoint(_PLAN_ID, cp.checkpoint_id)
msg = "Should have raised"
raise AssertionError(msg)
except BusinessRuleViolation as e:
assert "sandbox" in str(e)
print("rollback-sandbox-guard-ok")
def _prune_checkpoints() -> None:
svc = CheckpointService()
for i in range(5):
svc.create_checkpoint(_PLAN_ID, f"commit-{i}")
policy = CheckpointRetentionPolicy(max_checkpoints=3, auto_prune=True)
pruned = svc.prune_checkpoints(_PLAN_ID, policy)
assert len(pruned) == 2
remaining = svc.list_checkpoints(_PLAN_ID)
assert len(remaining) == 3
# Verify first and most recent are preserved
refs = [cp.sandbox_ref for cp in remaining]
assert "commit-0" in refs, "First checkpoint must be preserved"
assert "commit-4" in refs, "Most recent checkpoint must be preserved"
print("prune-checkpoints-ok")
def _delete_checkpoint() -> None:
svc = CheckpointService()
cp = svc.create_checkpoint(_PLAN_ID, "commit-abc")
svc.delete_checkpoint(cp.checkpoint_id)
try:
svc.get_checkpoint(cp.checkpoint_id)
msg = "Should have raised"
raise AssertionError(msg)
except ResourceNotFoundError:
pass
print("delete-checkpoint-ok")
def _checkpoint_metadata() -> None:
svc = CheckpointService()
cp = svc.create_checkpoint(
_PLAN_ID,
"commit-abc",
reason="pre-execution",
source_tool="lint-check",
phase="execute",
)
stored = svc.get_checkpoint(cp.checkpoint_id)
assert stored.metadata.reason == "pre-execution"
assert stored.metadata.source_tool == "lint-check"
assert stored.metadata.phase == "execute"
print("checkpoint-metadata-ok")
def _auto_prune_create() -> None:
svc = CheckpointService()
for i in range(10):
svc.create_checkpoint(_PLAN_ID, f"commit-{i}")
policy = CheckpointRetentionPolicy(max_checkpoints=5, auto_prune=True)
svc.create_checkpoint(_PLAN_ID, "commit-new", retention_policy=policy)
remaining = svc.list_checkpoints(_PLAN_ID)
assert len(remaining) == 5
print("auto-prune-create-ok")
def _checkpoint_spec_fields() -> None:
svc = CheckpointService()
cp = svc.create_checkpoint(
_PLAN_ID,
"commit-spec",
decision_id="01DEC1S10N0000000000000000",
checkpoint_type="pre_write",
resource_id="01RES0URCE0000000000000000",
filesystem_path="snapshots/cp001",
size_bytes=4096,
)
stored = svc.get_checkpoint(cp.checkpoint_id)
assert stored.decision_id == "01DEC1S10N0000000000000000"
assert stored.checkpoint_type == "pre_write"
assert stored.resource_id == "01RES0URCE0000000000000000"
assert stored.filesystem_path == "snapshots/cp001"
assert stored.size_bytes == 4096
print("checkpoint-spec-fields-ok")
def _correction_checkpoint() -> None:
cp_svc = CheckpointService()
corr_svc = CorrectionService(checkpoint_service=cp_svc)
assert corr_svc._checkpoint_service is not None
print("correction-checkpoint-ok")
_COMMANDS: dict[str, object] = {
"create-checkpoint": _create_checkpoint,
"list-checkpoints": _list_checkpoints,
"rollback-checkpoint": _rollback_checkpoint,
"rollback-applied-guard": _rollback_applied_guard,
"rollback-sandbox-guard": _rollback_sandbox_guard,
"prune-checkpoints": _prune_checkpoints,
"delete-checkpoint": _delete_checkpoint,
"checkpoint-metadata": _checkpoint_metadata,
"auto-prune-create": _auto_prune_create,
"checkpoint-spec-fields": _checkpoint_spec_fields,
"correction-checkpoint": _correction_checkpoint,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
print(f"Commands: {', '.join(sorted(_COMMANDS))}", file=sys.stderr)
sys.exit(1)
cmd = _COMMANDS[sys.argv[1]]
assert callable(cmd)
cmd()