"""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]]()