5935940276
CI / lint (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 31s
CI / security (pull_request) Successful in 29s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 1m21s
CI / quality (pull_request) Successful in 2m33s
CI / unit_tests (pull_request) Successful in 3m10s
CI / integration_tests (pull_request) Successful in 5m17s
CI / coverage (pull_request) Successful in 4m25s
CI / docker (pull_request) Successful in 40s
CI / lint (push) Successful in 12s
CI / typecheck (push) Successful in 31s
CI / quality (push) Successful in 15s
CI / security (push) Successful in 32s
CI / build (push) Successful in 21s
CI / integration_tests (push) Successful in 3m6s
CI / unit_tests (push) Successful in 3m30s
CI / benchmark-regression (push) Has been skipped
CI / docker (push) Successful in 1m43s
CI / coverage (push) Successful in 5m21s
CI / benchmark-regression (pull_request) Successful in 31m11s
CI / benchmark-publish (push) Successful in 17m24s
Implement sandbox_boundary(r) function that walks up containment edges in the resource DAG to the nearest sandboxable ancestor, enabling resources sharing a boundary to share one sandbox instance. Changes: - Add boundary.py: is_sandbox_boundary(), sandbox_boundary(), compute_sandbox_domains(), BoundaryCache (thread-safe, per-execution) - Update SandboxManager: resolve_sandbox_key() and get_or_create_sandbox_for_resource() key by (plan_id, boundary_id) instead of (plan_id, resource_id); boundary cache lifecycle methods - Define "sandboxable" via ResourceCapabilities.sandboxable + non-none sandbox_strategy as per specification section 24659-24674 - Export new symbols from sandbox __init__.py - Add vulture whitelist entries for new public API Tests: - 26 Behave BDD scenarios (features/sandbox_boundary_algebra.feature) - 5 Robot Framework integration tests (robot/sandbox_boundary_algebra.robot) - ASV benchmarks for boundary walk, domain grouping, and cache performance ISSUES CLOSED: #548
282 lines
8.5 KiB
Python
282 lines
8.5 KiB
Python
"""Helper utilities for sandbox boundary algebra Robot integration tests.
|
|
|
|
Covers the integration between:
|
|
- sandbox_boundary() function
|
|
- compute_sandbox_domains() grouping
|
|
- BoundaryCache caching
|
|
- SandboxManager boundary-aware keying
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from cleveragents.domain.models.core.resource import (
|
|
PhysVirt,
|
|
Resource,
|
|
ResourceCapabilities,
|
|
SandboxStrategy,
|
|
)
|
|
from cleveragents.infrastructure.sandbox.boundary import (
|
|
BoundaryCache,
|
|
NoSandboxBoundaryError,
|
|
compute_sandbox_domains,
|
|
is_sandbox_boundary,
|
|
sandbox_boundary,
|
|
)
|
|
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
|
|
from cleveragents.infrastructure.sandbox.manager import SandboxManager
|
|
|
|
_COUNTER = 0
|
|
|
|
|
|
def _ulid(name: str) -> str:
|
|
"""Generate a deterministic ULID for test resources."""
|
|
global _COUNTER
|
|
_COUNTER += 1
|
|
base = abs(hash(name)) % (10**20)
|
|
raw = f"{_COUNTER:06d}{base:020d}"
|
|
charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
result = ""
|
|
for ch in raw:
|
|
result += charset[int(ch)]
|
|
return (result + "0" * 26)[:26]
|
|
|
|
|
|
def _resource(
|
|
name: str,
|
|
sandboxable: bool = False,
|
|
strategy: str = "none",
|
|
parents: list[str] | None = None,
|
|
location: str | None = None,
|
|
) -> Resource:
|
|
return Resource(
|
|
resource_id=_ulid(name),
|
|
name=name,
|
|
resource_type_name="git-checkout" if sandboxable else "fs-file",
|
|
classification=PhysVirt.PHYSICAL,
|
|
sandbox_strategy=SandboxStrategy(strategy),
|
|
capabilities=ResourceCapabilities(
|
|
readable=True,
|
|
writable=True,
|
|
sandboxable=sandboxable,
|
|
checkpointable=False,
|
|
),
|
|
parents=parents or [],
|
|
location=location,
|
|
)
|
|
|
|
|
|
def _test_boundary_walk() -> None:
|
|
"""Integration: boundary walks up to nearest sandboxable ancestor."""
|
|
checkout = _resource("checkout", sandboxable=True, strategy="git_worktree")
|
|
directory = _resource("dir", sandboxable=False, parents=[checkout.resource_id])
|
|
file_res = _resource("file", sandboxable=False, parents=[directory.resource_id])
|
|
registry = {
|
|
checkout.resource_id: checkout,
|
|
directory.resource_id: directory,
|
|
file_res.resource_id: file_res,
|
|
}
|
|
|
|
assert is_sandbox_boundary(checkout)
|
|
assert not is_sandbox_boundary(directory)
|
|
assert not is_sandbox_boundary(file_res)
|
|
|
|
boundary = sandbox_boundary(file_res, registry)
|
|
assert boundary is not None
|
|
assert boundary.resource_id == checkout.resource_id
|
|
|
|
# Self-boundary
|
|
self_boundary = sandbox_boundary(checkout, registry)
|
|
assert self_boundary is not None
|
|
assert self_boundary.resource_id == checkout.resource_id
|
|
|
|
print("boundary-walk-ok")
|
|
|
|
|
|
def _test_domain_grouping() -> None:
|
|
"""Integration: domain grouping partitions resources by boundary."""
|
|
co1 = _resource("co1", sandboxable=True, strategy="git_worktree")
|
|
co2 = _resource("co2", sandboxable=True, strategy="copy_on_write")
|
|
f1 = _resource("f1", parents=[co1.resource_id])
|
|
f2 = _resource("f2", parents=[co1.resource_id])
|
|
f3 = _resource("f3", parents=[co2.resource_id])
|
|
orphan = _resource("orphan")
|
|
registry = {r.resource_id: r for r in [co1, co2, f1, f2, f3, orphan]}
|
|
|
|
domains = compute_sandbox_domains([f1, f2, f3, orphan], registry)
|
|
|
|
assert co1.resource_id in domains
|
|
assert len(domains[co1.resource_id]) == 2
|
|
assert co2.resource_id in domains
|
|
assert len(domains[co2.resource_id]) == 1
|
|
assert "__unsandboxable__" in domains
|
|
assert len(domains["__unsandboxable__"]) == 1
|
|
|
|
print("domain-grouping-ok")
|
|
|
|
|
|
def _test_boundary_cache() -> None:
|
|
"""Integration: boundary cache stores and retrieves results."""
|
|
checkout = _resource("co-cache", sandboxable=True, strategy="git_worktree")
|
|
file_res = _resource("f-cache", parents=[checkout.resource_id])
|
|
registry = {
|
|
checkout.resource_id: checkout,
|
|
file_res.resource_id: file_res,
|
|
}
|
|
cache = BoundaryCache()
|
|
|
|
assert cache.size == 0
|
|
result = cache.get_boundary(file_res, registry)
|
|
assert result is not None
|
|
assert result.resource_id == checkout.resource_id
|
|
assert cache.size == 1
|
|
|
|
# Cache hit
|
|
result2 = cache.get_boundary(file_res, registry)
|
|
assert result2 is not None
|
|
assert result2.resource_id == checkout.resource_id
|
|
assert cache.size == 1
|
|
|
|
cache.clear()
|
|
assert cache.size == 0
|
|
|
|
print("boundary-cache-ok")
|
|
|
|
|
|
def _test_manager_boundary_keying() -> None:
|
|
"""Integration: SandboxManager resolves keys via boundary algebra.
|
|
|
|
Uses ``resolve_sandbox_key`` (no real sandbox creation) and then
|
|
``get_or_create_sandbox`` with the resolved boundary to demonstrate
|
|
that two child resources share the same sandbox.
|
|
"""
|
|
checkout = _resource(
|
|
"co-mgr",
|
|
sandboxable=True,
|
|
strategy="git_worktree",
|
|
location="/tmp",
|
|
)
|
|
f1 = _resource("f1-mgr", parents=[checkout.resource_id])
|
|
f2 = _resource("f2-mgr", parents=[checkout.resource_id])
|
|
registry = {
|
|
checkout.resource_id: checkout,
|
|
f1.resource_id: f1,
|
|
f2.resource_id: f2,
|
|
}
|
|
|
|
factory = SandboxFactory()
|
|
manager = SandboxManager(factory=factory, cleanup_on_exit=False)
|
|
|
|
key1 = manager.resolve_sandbox_key(f1, registry)
|
|
key2 = manager.resolve_sandbox_key(f2, registry)
|
|
assert key1 == checkout.resource_id
|
|
assert key2 == checkout.resource_id
|
|
|
|
# Both files resolve to the same key — verify they would share a sandbox
|
|
# by using get_or_create_sandbox with the resolved key and "none" strategy
|
|
# (to avoid needing a real git repo).
|
|
sb1 = manager.get_or_create_sandbox(
|
|
plan_id="plan-1",
|
|
resource_id=key1,
|
|
original_path="/tmp",
|
|
sandbox_strategy="none",
|
|
)
|
|
sb2 = manager.get_or_create_sandbox(
|
|
plan_id="plan-1",
|
|
resource_id=key2,
|
|
original_path="/tmp",
|
|
sandbox_strategy="none",
|
|
)
|
|
assert sb1 is sb2
|
|
|
|
# Orphan raises NoSandboxBoundaryError.
|
|
orphan = _resource("orphan-mgr")
|
|
registry[orphan.resource_id] = orphan
|
|
try:
|
|
manager.resolve_sandbox_key(orphan, registry)
|
|
print("FAIL: expected NoSandboxBoundaryError")
|
|
return
|
|
except NoSandboxBoundaryError:
|
|
pass
|
|
|
|
manager.cleanup_all("plan-1")
|
|
print("manager-boundary-keying-ok")
|
|
|
|
|
|
def _test_multi_resource_plan() -> None:
|
|
"""Integration: multi-resource plan with separate sandbox domains.
|
|
|
|
Uses resolve_sandbox_key to verify two different boundaries produce
|
|
different keys, then creates sandboxes using the keys directly.
|
|
"""
|
|
co_git = _resource(
|
|
"co-git",
|
|
sandboxable=True,
|
|
strategy="git_worktree",
|
|
location="/tmp",
|
|
)
|
|
co_fs = _resource(
|
|
"co-fs",
|
|
sandboxable=True,
|
|
strategy="copy_on_write",
|
|
location="/tmp",
|
|
)
|
|
f_git = _resource("f-git", parents=[co_git.resource_id])
|
|
f_fs = _resource("f-fs", parents=[co_fs.resource_id])
|
|
registry = {r.resource_id: r for r in [co_git, co_fs, f_git, f_fs]}
|
|
|
|
factory = SandboxFactory()
|
|
manager = SandboxManager(factory=factory, cleanup_on_exit=False)
|
|
|
|
key_git = manager.resolve_sandbox_key(f_git, registry)
|
|
key_fs = manager.resolve_sandbox_key(f_fs, registry)
|
|
assert key_git != key_fs
|
|
assert key_git == co_git.resource_id
|
|
assert key_fs == co_fs.resource_id
|
|
|
|
# Create sandboxes via resolved keys using "none" strategy (test env).
|
|
sb_git = manager.get_or_create_sandbox(
|
|
plan_id="plan-2",
|
|
resource_id=key_git,
|
|
original_path="/tmp",
|
|
sandbox_strategy="none",
|
|
)
|
|
sb_fs = manager.get_or_create_sandbox(
|
|
plan_id="plan-2",
|
|
resource_id=key_fs,
|
|
original_path="/tmp",
|
|
sandbox_strategy="none",
|
|
)
|
|
|
|
# Different boundaries → different sandboxes.
|
|
assert sb_git is not sb_fs
|
|
|
|
sandboxes = manager.list_sandboxes("plan-2")
|
|
assert len(sandboxes) == 2
|
|
|
|
manager.cleanup_all("plan-2")
|
|
print("multi-resource-plan-ok")
|
|
|
|
|
|
_COMMANDS = {
|
|
"boundary-walk": _test_boundary_walk,
|
|
"domain-grouping": _test_domain_grouping,
|
|
"boundary-cache": _test_boundary_cache,
|
|
"manager-boundary-keying": _test_manager_boundary_keying,
|
|
"multi-resource-plan": _test_multi_resource_plan,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <command>")
|
|
print(f"Commands: {', '.join(_COMMANDS)}")
|
|
sys.exit(1)
|
|
|
|
cmd = sys.argv[1]
|
|
fn = _COMMANDS.get(cmd)
|
|
if fn is None:
|
|
print(f"Unknown command: {cmd}")
|
|
sys.exit(1)
|
|
fn()
|