"""SandboxManager unit tests with mocked backend.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from pathlib import Path from threading import Lock from unittest.mock import patch import pytest from sandboxer.core.manager import SandboxManager from sandboxer.lifecycle.store import SandboxStore from sandboxer.models import ( ActorType, Consumer, SandboxCreateRequest, SandboxExecRequest, SandboxState, SandboxStatus, ) class FakeBackend: def provision(self, profile, inputs, host): return { "sandbox_id": "test1234", "host": host, "remote_dir": "/tmp/sandboxer/test1234", "compose_project": "sbx-e2e-test1234", "compose_file": "docker-compose.yml", "ssh_user": "root", } def wait_ready(self, handle): return { "ssh": f"root@{handle['host']}", "remote_dir": handle["remote_dir"], "compose_project": handle["compose_project"], "host": handle["host"], } def teardown(self, handle): return { "compose_removed": "True", "remote_dir_removed": "True", "remote_dir": handle["remote_dir"], } class FakeExecBackend: def supports_execution(self): return True def execute(self, handle, command, **kwargs): return { "exit_code": 0, "timed_out": False, "stdout": "inside\n", "stderr": "", "output_truncated": False, "workspace_dir": handle["workspace_dir"], } class FakeNonExecBackend: def supports_execution(self): return False @pytest.fixture def store(tmp_path: Path) -> SandboxStore: return SandboxStore(path=tmp_path / "sandboxes.json") def test_create_and_destroy(store: SandboxStore) -> None: manager = SandboxManager(store=store) request = SandboxCreateRequest( profile="profile.compose-e2e", inputs={"repo": "/tmp/repo"}, consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"), ) fake = FakeBackend() with ( patch("sandboxer.core.manager.resolve_backend", return_value=fake), patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None), patch("sandboxer.core.manager.resolve_host", return_value="coulombcore"), ): status = manager.create(request) assert status.state == SandboxState.READY assert status.sandbox_id == "test1234" destroyed = manager.destroy(status.sandbox_id) assert destroyed.state == SandboxState.DESTROYED def test_destroy_idempotent(store: SandboxStore) -> None: now = datetime.now(UTC) status = SandboxStatus( sandbox_id="gone1234", profile_id="profile.compose-e2e", extension_id="ext.compose-ssh", state=SandboxState.DESTROYED, consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"), created_at=now, updated_at=now, destroyed_at=now, ) store.save(status) manager = SandboxManager(store=store) result = manager.destroy("gone1234") assert result.state == SandboxState.DESTROYED def _ready_bwrap_status(*, state: SandboxState = SandboxState.READY) -> SandboxStatus: now = datetime.now(UTC) return SandboxStatus( sandbox_id="bwrap123", profile_id="profile.bwrap-local", extension_id="ext.bwrap", state=state, consumer=Consumer( actor=ActorType.AGT, project="glas-harness", session_id="session-1", run_id="run-1", ), host="localhost", inputs={"pid": "42", "workspace_dir": "/tmp/sandboxer-bwrap/bwrap123"}, expires_at=now + timedelta(hours=1), created_at=now, updated_at=now, ready_at=now, ) def _exec_request(**consumer_overrides) -> SandboxExecRequest: consumer = { "actor": "agt", "project": "glas-harness", "session_id": "session-1", "run_id": "run-1", **consumer_overrides, } return SandboxExecRequest( command=["python3", "-V"], consumer=Consumer.model_validate(consumer), credential_route_refs=["rein-openweights-openrouter-approle"], stdin_text='{"title":"bounded task"}', timeout_seconds=30, ) def test_execute_binds_identity_and_restores_ready_state(store: SandboxStore) -> None: store.save(_ready_bwrap_status()) manager = SandboxManager(store=store) backend = FakeExecBackend() with ( patch("sandboxer.core.manager.resolve_backend", return_value=backend), patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None), ): result = manager.execute("bwrap123", _exec_request()) assert result.exit_code == 0 assert result.stdout == "inside\n" assert result.network_default == "deny" assert result.network_egress == [] assert result.credential_route_refs == ["rein-openweights-openrouter-approle"] assert store.get("bwrap123").state == SandboxState.READY def test_execute_refuses_identity_mismatch(store: SandboxStore) -> None: store.save(_ready_bwrap_status()) manager = SandboxManager(store=store) with pytest.raises(PermissionError, match="exactly match"): manager.execute("bwrap123", _exec_request(run_id="another-run")) def test_execute_refuses_non_ready_and_expired_sandboxes(store: SandboxStore) -> None: active = _ready_bwrap_status(state=SandboxState.ACTIVE) store.save(active) manager = SandboxManager(store=store) with pytest.raises(RuntimeError, match="must be ready"): manager.execute("bwrap123", _exec_request()) def test_execute_refuses_concurrent_command(store: SandboxStore) -> None: store.save(_ready_bwrap_status()) manager = SandboxManager(store=store) lock = manager._exec_locks.setdefault("bwrap123", Lock()) lock.acquire() try: with pytest.raises(RuntimeError, match="already has an active"): manager.execute("bwrap123", _exec_request()) finally: lock.release() expired = _ready_bwrap_status() expired.expires_at = datetime.now(UTC) - timedelta(seconds=1) store.save(expired) with pytest.raises(RuntimeError, match="TTL has expired"): manager.execute("bwrap123", _exec_request()) def test_execute_refuses_invalid_credential_route_ref(store: SandboxStore) -> None: store.save(_ready_bwrap_status()) request = _exec_request() request.credential_route_refs = ["looks like a secret value"] with pytest.raises(ValueError, match="value-free catalog"): SandboxManager(store=store).execute("bwrap123", request) def test_execute_refuses_extension_without_owner_boundary(store: SandboxStore) -> None: status = _ready_bwrap_status() status.profile_id = "profile.compose-e2e" status.extension_id = "ext.compose-ssh" store.save(status) with ( patch("sandboxer.core.manager.resolve_backend", return_value=FakeNonExecBackend()), pytest.raises(RuntimeError, match="no owner-mediated execution boundary"), ): SandboxManager(store=store).execute("bwrap123", _exec_request()) def test_execute_restores_ready_after_backend_failure(store: SandboxStore) -> None: store.save(_ready_bwrap_status()) manager = SandboxManager(store=store) backend = FakeExecBackend() backend.execute = lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")) with ( patch("sandboxer.core.manager.resolve_backend", return_value=backend), patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None), pytest.raises(RuntimeError, match="boom"), ): manager.execute("bwrap123", _exec_request()) assert store.get("bwrap123").state == SandboxState.READY