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:
2026-06-24 07:57:40 +02:00
parent 2760ef2373
commit 952cebf2e9
21 changed files with 966 additions and 34 deletions

View File

@@ -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")