Files
cleveragents-core/robot/helper_custom_sandbox_strategy.py
freemo 2688c85769
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 19s
CI / lint (pull_request) Successful in 23s
CI / quality (pull_request) Successful in 28s
CI / security (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 1m25s
CI / e2e_tests (pull_request) Successful in 2m33s
CI / unit_tests (pull_request) Successful in 3m4s
CI / integration_tests (pull_request) Successful in 3m33s
CI / docker (pull_request) Successful in 1m7s
CI / coverage (pull_request) Successful in 5m50s
CI / lint (push) Successful in 18s
CI / build (push) Successful in 33s
CI / quality (push) Successful in 47s
CI / typecheck (push) Successful in 49s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 52s
CI / e2e_tests (push) Successful in 2m9s
CI / unit_tests (push) Successful in 3m32s
CI / integration_tests (push) Successful in 3m41s
CI / docker (push) Successful in 56s
CI / coverage (push) Successful in 6m10s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 40m21s
feat(extensibility): implement Custom Sandbox Strategy Registration via SandboxStrategy Protocol
Implement SandboxStrategyProtocol, a 9-method @runtime_checkable Protocol enabling
third-party sandbox strategy registration. Includes:

- SandboxStrategyProtocol with create/read/write/diff/commit/rollback/checkpoint/
  restore_checkpoint/cleanup methods
- SandboxRef (frozen dataclass) and DiffView/DiffEntry (Pydantic models)
- SandboxStrategyRegistry with config-driven registration, Protocol validation,
  thread safety, and clear/list/has operations
- BuiltInSandboxStrategyAdapter wrapping existing Sandbox implementations to conform
  to the new Protocol
- CustomStrategyConfig for YAML/dict-based strategy registration
- SandboxFactory integration with custom_registry parameter,
  has_custom_strategy() and get_custom_strategy_class()
- 25 Behave BDD scenarios (85 steps) covering protocol, registry, adapter, config,
  and factory integration
- 8 Robot Framework integration tests with real filesystem operations
- ASV benchmarks for registry and adapter operations
- Developer documentation

ISSUES CLOSED: #586
2026-03-16 22:50:27 +00:00

440 lines
12 KiB
Python

"""Helper utilities for custom sandbox strategy Robot integration tests.
Covers end-to-end integration of:
- SandboxStrategyProtocol and runtime checking
- SandboxStrategyRegistry registration and lookup
- BuiltInSandboxStrategyAdapter full lifecycle
- CustomStrategyConfig validation
- SandboxFactory custom strategy integration
Based on issue #586.
"""
from __future__ import annotations
import os
import sys
import tempfile
from datetime import datetime
from cleveragents.domain.models.core.resource import (
PhysVirt,
Resource,
ResourceCapabilities,
)
from cleveragents.domain.models.core.resource import (
SandboxStrategy as SandboxStrategyEnum,
)
from cleveragents.domain.models.core.sandbox_strategy import (
DiffEntry,
DiffView,
SandboxRef,
SandboxStrategyProtocol,
)
from cleveragents.infrastructure.plugins.exceptions import (
ProtocolMismatchError,
)
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
from cleveragents.infrastructure.sandbox.strategy_adapter import (
BuiltInSandboxStrategyAdapter,
)
from cleveragents.infrastructure.sandbox.strategy_registry import (
CustomStrategyConfig,
SandboxStrategyRegistry,
)
# ---------------------------------------------------------------------------
# ULID helper
# ---------------------------------------------------------------------------
_CTR = 0
def _ulid(name: str) -> str:
global _CTR
_CTR += 1
base = abs(hash(name)) % (10**20)
raw = f"{_CTR:06d}{base:020d}"
charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
result = ""
for ch in raw:
result += charset[int(ch)]
return (result + "0" * 26)[:26]
def _make_resource(name: str, location: str) -> Resource:
return Resource(
resource_id=_ulid(name),
name=name,
resource_type_name="fs-directory",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategyEnum.NONE,
location=location,
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=True,
checkpointable=False,
),
)
# ---------------------------------------------------------------------------
# Test: SandboxRef creation and immutability
# ---------------------------------------------------------------------------
def _sandbox_ref_lifecycle() -> None:
"""Integration test: SandboxRef creation and properties."""
ref = SandboxRef(
sandbox_id="ref-001",
plan_id="plan-001",
resource_id="res-001",
created_at=datetime.now(),
metadata={"backend": "test"},
)
assert ref.sandbox_id == "ref-001"
assert ref.plan_id == "plan-001"
assert ref.metadata["backend"] == "test"
# Frozen check
try:
ref.sandbox_id = "mutated" # type: ignore[misc]
print("FAIL: Expected FrozenInstanceError")
return
except (AttributeError, Exception):
pass
print("sandbox-ref-lifecycle-ok")
# ---------------------------------------------------------------------------
# Test: DiffView and DiffEntry
# ---------------------------------------------------------------------------
def _diff_view_lifecycle() -> None:
"""Integration test: DiffView and DiffEntry models."""
entry = DiffEntry(path="test.py", operation="added", after=b"content")
assert entry.path == "test.py"
assert entry.operation == "added"
view = DiffView(
sandbox_id="dv-001",
entries=[entry],
summary="1 file added",
)
assert len(view.entries) == 1
assert view.sandbox_id == "dv-001"
# Empty path should fail
try:
DiffEntry(path="", operation="modified")
print("FAIL: Expected ValidationError")
return
except Exception:
pass
print("diff-view-lifecycle-ok")
# ---------------------------------------------------------------------------
# Test: SandboxStrategyProtocol runtime check
# ---------------------------------------------------------------------------
def _protocol_check() -> None:
"""Integration test: Protocol runtime checking."""
class _Good:
def create(self, plan_id, resource):
return None
def read(self, ref, path):
return b""
def write(self, ref, path, content):
return None
def diff(self, ref):
return None
def commit(self, ref):
pass
def rollback(self, ref):
pass
def checkpoint(self, ref, checkpoint_id):
pass
def restore_checkpoint(self, ref, checkpoint_id):
pass
def cleanup(self, ref):
pass
class _Bad:
def create(self, plan_id, resource):
return None
good = _Good()
bad = _Bad()
assert isinstance(good, SandboxStrategyProtocol)
assert not isinstance(bad, SandboxStrategyProtocol)
print("protocol-check-ok")
# ---------------------------------------------------------------------------
# Test: SandboxStrategyRegistry
# ---------------------------------------------------------------------------
def _registry_lifecycle() -> None:
"""Integration test: Registry registration, lookup, config, clear."""
registry = SandboxStrategyRegistry(allowed_prefixes=("cleveragents.",))
# Register
cls = registry.register(
"adapter",
"cleveragents.infrastructure.sandbox.strategy_adapter",
"BuiltInSandboxStrategyAdapter",
)
assert cls is BuiltInSandboxStrategyAdapter
assert registry.has("adapter")
assert "adapter" in registry.list_strategies()
# Get
assert registry.get("adapter") is BuiltInSandboxStrategyAdapter
assert registry.get("nonexistent") is None
# Non-protocol rejection
try:
registry.register(
"bad",
"cleveragents.infrastructure.sandbox.protocol",
"SandboxError",
)
print("FAIL: Expected ProtocolMismatchError")
return
except ProtocolMismatchError:
pass
# Config-driven registration
configs = {
"cfg1": {
"module": "cleveragents.infrastructure.sandbox.strategy_adapter",
"class": "BuiltInSandboxStrategyAdapter",
},
}
registered = registry.register_all_from_config(configs)
assert len(registered) == 1
# Clear
registry.clear()
assert len(registry.list_strategies()) == 0
print("registry-lifecycle-ok")
# ---------------------------------------------------------------------------
# Test: BuiltInSandboxStrategyAdapter full lifecycle
# ---------------------------------------------------------------------------
def _adapter_lifecycle() -> None:
"""Integration test: Adapter create/read/write/diff/commit/cleanup."""
with tempfile.TemporaryDirectory() as tmpdir:
# Seed a file
with open(os.path.join(tmpdir, "seed.txt"), "w") as f:
f.write("seed")
adapter = BuiltInSandboxStrategyAdapter(strategy_name="copy_on_write")
resource = Resource(
resource_id=_ulid("adapter-res"),
name="adapter-res",
resource_type_name="fs-directory",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategyEnum.COPY_ON_WRITE,
location=tmpdir,
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=True,
checkpointable=False,
),
)
# Create
ref = adapter.create("plan-adapter", resource)
assert isinstance(ref, SandboxRef)
assert ref.plan_id == "plan-adapter"
# Write
entry = adapter.write(ref, "new_file.txt", b"hello")
assert isinstance(entry, DiffEntry)
assert entry.operation == "added"
# Read
data = adapter.read(ref, "new_file.txt")
assert data == b"hello"
# Diff
view = adapter.diff(ref)
assert isinstance(view, DiffView)
# Cleanup
adapter.cleanup(ref)
assert ref.sandbox_id not in adapter._sandboxes
print("adapter-lifecycle-ok")
# ---------------------------------------------------------------------------
# Test: Adapter checkpoint/restore
# ---------------------------------------------------------------------------
def _adapter_checkpoint() -> None:
"""Integration test: Adapter checkpoint and restore."""
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, "seed.txt"), "w") as f:
f.write("seed")
adapter = BuiltInSandboxStrategyAdapter(strategy_name="copy_on_write")
resource = Resource(
resource_id=_ulid("cp-res"),
name="cp-res",
resource_type_name="fs-directory",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategyEnum.COPY_ON_WRITE,
location=tmpdir,
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=True,
checkpointable=False,
),
)
ref = adapter.create("plan-cp", resource)
adapter.write(ref, "cp.txt", b"v1")
adapter.checkpoint(ref, "cp-1")
adapter.write(ref, "cp.txt", b"v2")
# Verify v2
assert adapter.read(ref, "cp.txt") == b"v2"
# Restore
adapter.restore_checkpoint(ref, "cp-1")
assert adapter.read(ref, "cp.txt") == b"v1"
adapter.cleanup(ref)
print("adapter-checkpoint-ok")
# ---------------------------------------------------------------------------
# Test: CustomStrategyConfig validation
# ---------------------------------------------------------------------------
def _config_validation() -> None:
"""Integration test: CustomStrategyConfig validation."""
# Valid
cfg = CustomStrategyConfig(
name="test", module="cleveragents.test", class_name="TestCls"
)
assert cfg.name == "test"
# Empty name
try:
CustomStrategyConfig(name="", module="mod", class_name="Cls")
print("FAIL: Expected ValueError for empty name")
return
except ValueError:
pass
# Empty module
try:
CustomStrategyConfig(name="x", module="", class_name="Cls")
print("FAIL: Expected ValueError for empty module")
return
except ValueError:
pass
# Empty class
try:
CustomStrategyConfig(name="x", module="mod", class_name="")
print("FAIL: Expected ValueError for empty class")
return
except ValueError:
pass
print("config-validation-ok")
# ---------------------------------------------------------------------------
# Test: SandboxFactory custom strategy integration
# ---------------------------------------------------------------------------
def _factory_custom_integration() -> None:
"""Integration test: SandboxFactory with custom strategy registry."""
registry = SandboxStrategyRegistry(allowed_prefixes=("cleveragents.",))
registry.register(
"my_custom",
"cleveragents.infrastructure.sandbox.strategy_adapter",
"BuiltInSandboxStrategyAdapter",
)
factory = SandboxFactory(custom_registry=registry)
assert factory.has_custom_strategy("my_custom")
assert not factory.has_custom_strategy("nonexistent")
cls = factory.get_custom_strategy_class("my_custom")
assert cls is BuiltInSandboxStrategyAdapter
# Factory without registry
plain_factory = SandboxFactory()
assert not plain_factory.has_custom_strategy("anything")
assert plain_factory.get_custom_strategy_class("anything") is None
print("factory-custom-integration-ok")
# ---------------------------------------------------------------------------
# Main dispatcher
# ---------------------------------------------------------------------------
_TESTS = {
"sandbox-ref-lifecycle": _sandbox_ref_lifecycle,
"diff-view-lifecycle": _diff_view_lifecycle,
"protocol-check": _protocol_check,
"registry-lifecycle": _registry_lifecycle,
"adapter-lifecycle": _adapter_lifecycle,
"adapter-checkpoint": _adapter_checkpoint,
"config-validation": _config_validation,
"factory-custom-integration": _factory_custom_integration,
}
def main() -> None:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <test-name>")
print(f"Available: {', '.join(sorted(_TESTS))}")
sys.exit(1)
test_name = sys.argv[1]
if test_name not in _TESTS:
print(f"Unknown test: {test_name}")
print(f"Available: {', '.join(sorted(_TESTS))}")
sys.exit(1)
_TESTS[test_name]()
if __name__ == "__main__":
main()