feat: support smolmachines bottle commit

This commit is contained in:
2026-06-23 03:40:03 +00:00
committed by didericis
parent 64fac71025
commit 6e73cc4d86
8 changed files with 266 additions and 93 deletions
+43 -10
View File
@@ -5,9 +5,15 @@ from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, call, patch
from unittest.mock import MagicMock, patch
from bot_bottle.cli.commit import cmd_commit, _committed_image_tag, _agent_container_name
from bot_bottle.cli.commit import (
cmd_commit,
_agent_container_name,
_committed_image_tag,
_committed_smolmachine_artifact,
_committed_smolmachine_output,
)
from bot_bottle import supervise
from bot_bottle import bottle_state
@@ -41,6 +47,16 @@ class TestCommitHelpers(unittest.TestCase):
_agent_container_name("dev-abc12"),
)
def test_committed_smolmachine_paths(self):
output = _committed_smolmachine_output("dev-abc12")
artifact = _committed_smolmachine_artifact("dev-abc12")
self.assertTrue(str(output).endswith(
"/.bot-bottle/state/dev-abc12/committed-smolmachine"
))
self.assertTrue(str(artifact).endswith(
"/.bot-bottle/state/dev-abc12/committed-smolmachine.smolmachine"
))
class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
"""cmd_commit with an explicit slug bypasses the TUI picker."""
@@ -117,14 +133,14 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
mock_commit.assert_called_once()
class TestCmdCommitNonDockerBackend(_FakeHomeMixin, unittest.TestCase):
class TestCmdCommitSmolmachinesBackend(_FakeHomeMixin, unittest.TestCase):
def setUp(self):
self._setup_fake_home()
def tearDown(self):
self._teardown_fake_home()
def test_dies_for_smolmachines_backend(self):
def test_packs_smolmachines_bottle(self):
slug = "dev-abc12"
bottle_state.write_metadata(bottle_state.BottleMetadata(
identity=slug, agent_name="dev", cwd="", copy_cwd=False,
@@ -132,13 +148,30 @@ class TestCmdCommitNonDockerBackend(_FakeHomeMixin, unittest.TestCase):
))
with patch(
"bot_bottle.cli.commit.die", side_effect=SystemExit("die"),
) as mock_die:
with self.assertRaises(SystemExit):
cmd_commit([slug])
"bot_bottle.cli.commit.pack_create_from_vm",
) as mock_pack, patch(
"bot_bottle.cli.commit.info",
):
rc = cmd_commit([slug])
mock_die.assert_called_once()
self.assertIn("smolmachines", mock_die.call_args.args[0])
self.assertEqual(0, rc)
mock_pack.assert_called_once_with(
f"bot-bottle-{slug}",
_committed_smolmachine_output(slug),
)
self.assertEqual(
str(_committed_smolmachine_artifact(slug)),
bottle_state.read_committed_image(slug),
)
self.assertTrue(bottle_state.is_preserved(slug))
class TestCmdCommitUnsupportedBackend(_FakeHomeMixin, unittest.TestCase):
def setUp(self):
self._setup_fake_home()
def tearDown(self):
self._teardown_fake_home()
def test_dies_for_macos_container_backend(self):
slug = "dev-abc12"
@@ -7,6 +7,7 @@ import io
import tempfile
import unittest
from pathlib import Path
from typing import Any
from unittest import mock
from bot_bottle.agent_provider import AgentProvisionPlan
@@ -73,36 +74,28 @@ def _plan(tmp: str) -> DockerBottlePlan:
)
def _std_mocks(test, plan):
"""Context manager providing the standard launch-step mocks needed to
get through the non-image parts of `launch()` without real Docker."""
return mock.patch.multiple(
launch_mod,
egress_tls_init=mock.DEFAULT,
network_mod=mock.DEFAULT,
bottle_plan_to_compose=mock.DEFAULT,
write_compose_file=mock.DEFAULT,
compose_up=mock.DEFAULT,
compose_dump_logs=mock.DEFAULT,
compose_down=mock.DEFAULT,
)
class TestLaunchCommittedImage(unittest.TestCase):
def setUp(self):
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="launch-committed-test.")
def tearDown(self):
def tearDown(self) -> None:
import shutil
shutil.rmtree(self._tmp, ignore_errors=True)
def _run_launch(self, plan, *, committed_tag=None, image_present=True):
def _run_launch(
self,
plan: DockerBottlePlan,
*,
committed_tag: str | None = None,
image_present: bool = True,
) -> list[str]:
"""Drive launch() through its full sequence with the committed-image
behaviour controlled by the arguments. Returns the images that were
passed to `build_image` (empty list if it was never called)."""
built = []
built: list[str] = []
def fake_build(image, ctx, *, dockerfile=""):
def fake_build(image: str, ctx: str, *, dockerfile: str = "") -> None:
del ctx, dockerfile
built.append(image)
with mock.patch.object(
@@ -136,19 +129,19 @@ class TestLaunchCommittedImage(unittest.TestCase):
return built
def test_skips_build_when_committed_image_present(self):
def test_skips_build_when_committed_image_present(self) -> None:
plan = _plan(self._tmp)
built = self._run_launch(plan, committed_tag=_COMMITTED_TAG, image_present=True)
self.assertEqual([], built, "build_image should not be called when committed image exists")
def test_uses_committed_image_in_compose_spec(self):
def test_uses_committed_image_in_compose_spec(self) -> None:
"""The compose spec renderer receives the committed image tag via
plan.image — captured here by checking what bottle_plan_to_compose
was called with."""
plan = _plan(self._tmp)
captured_plans = []
captured_plans: list[DockerBottlePlan] = []
def fake_compose(p):
def fake_compose(p: DockerBottlePlan) -> dict[str, Any]:
captured_plans.append(p)
return {"services": {"agent": {}}}
@@ -183,12 +176,12 @@ class TestLaunchCommittedImage(unittest.TestCase):
self.assertEqual(1, len(captured_plans))
self.assertEqual(_COMMITTED_TAG, captured_plans[0].image)
def test_falls_back_to_build_when_no_committed_image(self):
def test_falls_back_to_build_when_no_committed_image(self) -> None:
plan = _plan(self._tmp)
built = self._run_launch(plan, committed_tag=None)
self.assertEqual([_DEFAULT_IMAGE], built)
def test_falls_back_to_build_when_committed_image_missing_from_daemon(self):
def test_falls_back_to_build_when_committed_image_missing_from_daemon(self) -> None:
plan = _plan(self._tmp)
built = self._run_launch(
plan, committed_tag=_COMMITTED_TAG, image_present=False,
@@ -16,6 +16,8 @@ from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import patch
from bot_bottle.backend.smolmachines import launch as _launch_mod
@@ -141,5 +143,46 @@ class TestEnsureSmolmachine(unittest.TestCase):
self.assertTrue(str(pack_args[1]).endswith(f"{digest}.smolmachine"))
class TestAgentFromPath(unittest.TestCase):
def _plan(self) -> Any:
return cast(Any, SimpleNamespace(
slug="dev-abc12",
agent_image="bot-bottle-claude:latest",
agent_dockerfile_path="/repo/Dockerfile",
))
def test_uses_committed_artifact_when_present(self):
with tempfile.TemporaryDirectory(prefix="committed-smolmachine.") as tmp:
artifact = Path(tmp) / "committed-smolmachine.smolmachine"
artifact.write_text("")
with patch.object(
_launch_mod, "read_committed_image", return_value=str(artifact),
), patch.object(
_launch_mod, "_ensure_smolmachine",
) as ensure, patch.object(
_launch_mod, "info",
):
result = _launch_mod._agent_from_path(self._plan())
self.assertEqual(artifact, result)
ensure.assert_not_called()
def test_falls_back_when_committed_artifact_missing(self):
packed = Path("/cache/agent.smolmachine")
with patch.object(
_launch_mod, "read_committed_image",
return_value="/missing/committed.smolmachine",
), patch.object(
_launch_mod, "_ensure_smolmachine", return_value=packed,
) as ensure:
result = _launch_mod._agent_from_path(self._plan())
self.assertEqual(packed, result)
ensure.assert_called_once_with(
"bot-bottle-claude:latest",
dockerfile="/repo/Dockerfile",
)
if __name__ == "__main__":
unittest.main()
+20
View File
@@ -24,6 +24,7 @@ from bot_bottle.backend.smolmachines.smolvm import (
machine_start,
machine_stop,
pack_create,
pack_create_from_vm,
wait_exec_ready,
)
@@ -63,6 +64,17 @@ class TestArgvShapes(unittest.TestCase):
argv,
)
def test_pack_create_from_vm_argv(self):
with self._patch_run() as m:
pack_create_from_vm("bot-bottle-dev-abc12", Path("/tmp/committed"))
argv = m.call_args.args[0]
self.assertEqual(
["smolvm", "pack", "create",
"--from-vm", "bot-bottle-dev-abc12",
"-o", "/tmp/committed"],
argv,
)
def test_machine_create_minimal(self):
with self._patch_run() as m:
machine_create("agent-xyz")
@@ -193,6 +205,14 @@ class TestErrorPath(unittest.TestCase):
with self.assertRaises(SmolvmError):
pack_create("missing:tag", Path("/tmp/out"))
def test_pack_create_from_vm_failure_raises(self):
with patch(
"bot_bottle.backend.smolmachines.smolvm.subprocess.run",
return_value=_fail("pack failed"),
):
with self.assertRaises(SmolvmError):
pack_create_from_vm("bot-bottle-dev-abc12", Path("/tmp/out"))
def test_exec_failure_returns_result(self):
# The in-VM command's exit code is what Bottle.exec sees;
# `false` exiting non-zero is not a smolvm failure.