generated from coulomb/repo-seed
feat: snapshot/restore checkpoints (SAND-WP-0007)
Add workspace checkpoint API with SnapshotStore, extension hooks on compose-ssh and saas-stub, manager orchestration, CLI/HTTP surface, profile.compose-checkpoint, and docs/tests.
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
"""Compose command configuration."""
|
||||
"""Compose command configuration and snapshot hooks."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sandboxer.extensions.compose_ssh import ComposeSSHExtension
|
||||
from sandboxer.models import Profile
|
||||
|
||||
|
||||
def _profile() -> Profile:
|
||||
return Profile.model_validate(
|
||||
{
|
||||
"id": "profile.compose-checkpoint",
|
||||
"version": "1.0.0",
|
||||
"extension": "ext.compose-ssh",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_compose_cmd_from_config() -> None:
|
||||
@@ -11,4 +26,72 @@ def test_compose_cmd_from_config() -> None:
|
||||
def test_compose_cmd_env_override(monkeypatch) -> None:
|
||||
monkeypatch.setenv("SANDBOXER_COMPOSE_CMD", "nerdctl compose")
|
||||
ext = ComposeSSHExtension({"compose_cmd": "docker compose"})
|
||||
assert ext._compose_bin() == "nerdctl compose"
|
||||
assert ext._compose_bin() == "nerdctl compose"
|
||||
|
||||
|
||||
def test_supports_snapshots() -> None:
|
||||
ext = ComposeSSHExtension()
|
||||
assert ext.supports_snapshots() is True
|
||||
|
||||
|
||||
def test_snapshot_creates_remote_tarball() -> None:
|
||||
ext = ComposeSSHExtension({"base_dir": "/tmp/sandboxer"})
|
||||
handle = {
|
||||
"sandbox_id": "abc12345",
|
||||
"host": "coulombcore",
|
||||
"remote_dir": "/tmp/sandboxer/abc12345",
|
||||
"compose_file": "docker-compose.yml",
|
||||
"compose_project": "sbx-e2e-abc12345",
|
||||
"ssh_user": "root",
|
||||
}
|
||||
|
||||
def fake_run(cmd, *, timeout=60):
|
||||
if "tar czf" in cmd:
|
||||
return 0, ""
|
||||
if "stat" in cmd:
|
||||
return 0, "2048"
|
||||
return 0, ""
|
||||
|
||||
with patch.object(ext, "_ssh_for_handle") as ssh_factory:
|
||||
ssh = ssh_factory.return_value
|
||||
ssh.run.side_effect = fake_run
|
||||
meta = ext.snapshot(handle)
|
||||
|
||||
assert meta["artifact_path"].endswith(".tar.gz")
|
||||
assert meta["snapshot_id"]
|
||||
assert meta["size_bytes"] == "2048"
|
||||
|
||||
|
||||
def test_restore_from_snapshot_extracts_and_compose_up() -> None:
|
||||
ext = ComposeSSHExtension({"base_dir": "/tmp/sandboxer"})
|
||||
snapshot_meta = {
|
||||
"snapshot_id": "snap12345678",
|
||||
"artifact_path": "/tmp/sandboxer/snapshots/snap12345678.tar.gz",
|
||||
"host": "coulombcore",
|
||||
"compose_file": "docker-compose.yml",
|
||||
"ssh_user": "root",
|
||||
}
|
||||
|
||||
with patch("sandboxer.extensions.compose_ssh.SSHConfig.from_env") as ssh_factory:
|
||||
ssh = ssh_factory.return_value
|
||||
ssh.run.return_value = (0, "")
|
||||
ssh.user = "root"
|
||||
handle = ext.restore_from_snapshot(_profile(), snapshot_meta, {}, "coulombcore")
|
||||
|
||||
assert handle["sandbox_id"]
|
||||
assert handle["remote_dir"].endswith(handle["sandbox_id"])
|
||||
calls = [c.args[0] for c in ssh.run.call_args_list]
|
||||
assert any("tar xzf" in c for c in calls)
|
||||
assert any("up -d" in c for c in calls)
|
||||
|
||||
|
||||
def test_restore_cross_host_not_supported() -> None:
|
||||
ext = ComposeSSHExtension()
|
||||
snapshot_meta = {
|
||||
"snapshot_id": "snap1",
|
||||
"artifact_path": "/tmp/snap.tar.gz",
|
||||
"host": "host-a",
|
||||
"compose_file": "docker-compose.yml",
|
||||
}
|
||||
with pytest.raises(NotImplementedError, match="cross-host"):
|
||||
ext.restore_from_snapshot(_profile(), snapshot_meta, {}, "host-b")
|
||||
Reference in New Issue
Block a user