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
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
196 lines
6.0 KiB
Python
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]]()
|