Files
cleveragents-core/robot/helper_resource_handler_sandbox.py
T
hamza.khyari 7bb77aee5c
CI / build (pull_request) Successful in 14s
CI / lint (pull_request) Successful in 4m3s
CI / quality (pull_request) Successful in 4m1s
CI / typecheck (pull_request) Successful in 4m30s
CI / security (pull_request) Successful in 5m12s
CI / unit_tests (pull_request) Successful in 8m45s
CI / integration_tests (pull_request) Successful in 8m43s
CI / docker (pull_request) Successful in 1m13s
CI / e2e_tests (pull_request) Successful in 13m7s
CI / coverage (pull_request) Successful in 12m43s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 1h9m12s
feat(resource): implement ResourceHandler sandbox and checkpoint methods
Extend the ResourceHandler protocol with four sandbox/lifecycle methods:
create_sandbox (idempotent), create_checkpoint, rollback_to, project_access.

Fixes from self-review:
- Instance-level checkpoint dict instead of ClassVar (no shared state)
- Git rollback uses 'git reset --hard' + 'git clean -fd' (not just checkout)
- FsDirectory rollback skips .git to preserve git metadata
- project_access separates ImportError (local mode) from ValueError (reject)
- Added discard_checkpoints() cleanup method
- Git rollback test verifies file content restored

ISSUES CLOSED: #836
2026-03-27 10:50:15 +00:00

196 lines
6.0 KiB
Python

"""Robot Framework helper for resource handler sandbox/checkpoint tests.
Exercises the create_sandbox -> create_checkpoint -> modify -> rollback
cycle on real temp directories and git repos.
Issue #836: ResourceHandler sandbox and checkpoint methods.
"""
from __future__ import annotations
import subprocess
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import NoReturn
_ROOT = Path(__file__).resolve().parents[1]
_SRC = str(_ROOT / "src")
sys.path.insert(0, _SRC)
for _mod in list(sys.modules):
if _mod.startswith("cleveragents"):
del sys.modules[_mod]
from cleveragents.domain.models.core.resource import ( # noqa: E402
PhysVirt,
Resource,
ResourceCapabilities,
)
from cleveragents.infrastructure.sandbox.factory import SandboxFactory # noqa: E402
from cleveragents.infrastructure.sandbox.manager import SandboxManager # noqa: E402
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler # noqa: E402
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler # noqa: E402
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
_ID_CTR = 0
def _next_id() -> str:
global _ID_CTR
_ID_CTR += 1
n = _ID_CTR + 0x836000
chars: list[str] = []
for _ in range(26):
chars.append(_CB32[n % 32])
n //= 32
return "".join(reversed(chars))
def _fail(msg: str) -> NoReturn:
print(msg, file=sys.stderr)
sys.exit(1)
def _make_resource(rtype: str, location: str) -> Resource:
return Resource(
resource_id=_next_id(),
resource_type_name=rtype,
classification=PhysVirt.PHYSICAL,
location=location,
capabilities=ResourceCapabilities(
readable=True, writable=True, sandboxable=True
),
)
def fs_checkpoint_rollback() -> None:
"""FsDirectory: sandbox -> checkpoint -> modify -> rollback cycle."""
handler = FsDirectoryHandler()
factory = SandboxFactory()
mgr = SandboxManager(factory=factory, cleanup_on_exit=False)
tmpdir = tempfile.mkdtemp(prefix="rh836_robot_fs_")
Path(tmpdir, "file.txt").write_text("original\n", encoding="utf-8")
resource = _make_resource("fs-directory", tmpdir)
plan_id = _next_id()
# 1. Create sandbox
sb = handler.create_sandbox(resource=resource, plan_id=plan_id, sandbox_manager=mgr)
if not sb.created:
_fail("Expected sandbox to be created")
if not Path(sb.sandbox_path).exists():
_fail(f"Sandbox path does not exist: {sb.sandbox_path}")
# 2. Create checkpoint
ckpt = handler.create_checkpoint(
resource=resource, plan_id=plan_id, sandbox_manager=mgr
)
if not ckpt.checkpoint_id.startswith("checkpoint-"):
_fail(f"Unexpected checkpoint_id: {ckpt.checkpoint_id}")
# 3. Modify
Path(sb.sandbox_path, "file.txt").write_text("modified\n", encoding="utf-8")
actual = Path(sb.sandbox_path, "file.txt").read_text(encoding="utf-8").strip()
if actual != "modified":
_fail(f"Modification failed: got '{actual}'")
# 4. Rollback
rb = handler.rollback_to(
resource=resource,
plan_id=plan_id,
checkpoint_id=ckpt.checkpoint_id,
sandbox_manager=mgr,
)
if not rb.success:
_fail(f"Rollback failed: {rb.message}")
# 5. Verify restored
restored = Path(sb.sandbox_path, "file.txt").read_text(encoding="utf-8").strip()
if restored != "original":
_fail(f"Rollback did not restore: expected 'original', got '{restored}'")
print("fs-checkpoint-rollback-ok")
def git_checkpoint_rollback() -> None:
"""GitCheckout: sandbox -> checkpoint -> modify -> rollback cycle."""
handler = GitCheckoutHandler()
factory = SandboxFactory()
mgr = SandboxManager(factory=factory, cleanup_on_exit=False)
repo = tempfile.mkdtemp(prefix="rh836_robot_git_")
subprocess.run(["git", "init"], cwd=repo, capture_output=True, check=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=repo,
capture_output=True,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=repo,
capture_output=True,
check=True,
)
Path(repo, "code.py").write_text("# hello\n", encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
subprocess.run(
["git", "commit", "-m", "init"],
cwd=repo,
capture_output=True,
check=True,
)
resource = _make_resource("git-checkout", repo)
plan_id = _next_id()
# 1. Create sandbox
sb = handler.create_sandbox(resource=resource, plan_id=plan_id, sandbox_manager=mgr)
if not sb.created:
_fail("Expected sandbox to be created")
# 2. Create checkpoint (git tag)
ckpt = handler.create_checkpoint(
resource=resource, plan_id=plan_id, sandbox_manager=mgr
)
if not ckpt.checkpoint_id.startswith("checkpoint-"):
_fail(f"Unexpected checkpoint_id: {ckpt.checkpoint_id}")
# 3. Modify file in sandbox
Path(sb.sandbox_path, "code.py").write_text("# changed\n", encoding="utf-8")
subprocess.run(
["git", "add", "."], cwd=sb.sandbox_path, capture_output=True, check=True
)
subprocess.run(
["git", "commit", "-m", "modify"],
cwd=sb.sandbox_path,
capture_output=True,
check=True,
)
# 4. Rollback
rb = handler.rollback_to(
resource=resource,
plan_id=plan_id,
checkpoint_id=ckpt.checkpoint_id,
sandbox_manager=mgr,
)
if not rb.success:
_fail(f"Rollback failed: {rb.message}")
print("git-checkpoint-rollback-ok")
_COMMANDS: dict[str, Callable[[], None]] = {
"fs-checkpoint-rollback": fs_checkpoint_rollback,
"git-checkpoint-rollback": git_checkpoint_rollback,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(1)
_COMMANDS[sys.argv[1]]()