refactor: BottleImages dataclass, prelaunch_checks, build_or_load_images
Addresses review comments 3098, 3099, 3100 on PR #336: - Add BottleImages(agent, sidecar) dataclass to backend/__init__.py. Docker/macOS backends use str image refs; smolmachines uses Path artifacts. Replaces the singular `image` variable from the canonical pattern in comment 3100. - Replace _image_stale_checks/skip_stale with public prelaunch_checks(). CLI now calls backend.prelaunch_checks(plan) before backend.launch(plan); if StaleImageError is raised and the operator confirms, launch proceeds without re-checking. Removes the while-True/skip_stale retry loop. - Add abstract _build_or_load_images(plan) -> BottleImages to BottleBackend. launch() calls it then passes images to _launch_impl. Each backend implements both methods. - Fix comment 3098 (macos-container): _build_images is removed. build_or_load_images() has separate fresh/cached code paths — the cached path never calls a build helper. - Update _start_bundle (smolmachines) to accept sidecar_artifact: Path directly. Sidecar artifact resolution moves to _sidecar_from_path(), called by build_or_load_images alongside _agent_from_path().
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""Unit: stale-image check functions across backends, and the
|
||||
BottleBackend.launch template method (skip_stale flag).
|
||||
BottleBackend.launch template method (prelaunch_checks + build_or_load_images).
|
||||
|
||||
No real images or containers are used — all Docker/container/smolmachine
|
||||
calls are mocked at the module boundary."""
|
||||
@@ -23,51 +23,41 @@ def _bottle_cm(bottle: Any) -> MagicMock:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BottleBackend.launch template — _image_stale_checks + skip_stale
|
||||
# BottleBackend.launch template — prelaunch_checks + _build_or_load_images
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBottleBackendLaunchTemplate(unittest.TestCase):
|
||||
"""Verify the concrete launch() method on BottleBackend calls
|
||||
_image_stale_checks unless skip_stale=True, then delegates to _launch_impl."""
|
||||
_build_or_load_images and _launch_impl, and that prelaunch_checks is a no-op
|
||||
on the base class."""
|
||||
|
||||
def _make_backend(self) -> Any:
|
||||
from bot_bottle.backend.docker.backend import DockerBottleBackend
|
||||
return DockerBottleBackend()
|
||||
|
||||
def test_stale_checks_called_by_default(self) -> None:
|
||||
def test_launch_delegates_to_build_or_load_and_launch_impl(self) -> None:
|
||||
from bot_bottle.backend import BottleImages
|
||||
backend = self._make_backend()
|
||||
plan = cast(Any, SimpleNamespace())
|
||||
bottle = MagicMock()
|
||||
images = BottleImages(agent="agent:latest", sidecar="sidecar:latest")
|
||||
with patch.object(
|
||||
backend, "_image_stale_checks",
|
||||
) as stale_mock, patch.object(
|
||||
backend, "_build_or_load_images", return_value=images,
|
||||
) as build_mock, patch.object(
|
||||
backend, "_launch_impl",
|
||||
return_value=_bottle_cm(bottle),
|
||||
):
|
||||
) as impl_mock:
|
||||
with backend.launch(plan):
|
||||
pass
|
||||
stale_mock.assert_called_once_with(plan)
|
||||
build_mock.assert_called_once_with(plan)
|
||||
impl_mock.assert_called_once_with(plan, images)
|
||||
|
||||
def test_skip_stale_bypasses_stale_checks(self) -> None:
|
||||
backend = self._make_backend()
|
||||
plan = cast(Any, SimpleNamespace())
|
||||
bottle = MagicMock()
|
||||
with patch.object(
|
||||
backend, "_image_stale_checks",
|
||||
) as stale_mock, patch.object(
|
||||
backend, "_launch_impl",
|
||||
return_value=_bottle_cm(bottle),
|
||||
):
|
||||
with backend.launch(plan, skip_stale=True):
|
||||
pass
|
||||
stale_mock.assert_not_called()
|
||||
|
||||
def test_noop_default_image_stale_checks(self) -> None:
|
||||
def test_noop_default_prelaunch_checks(self) -> None:
|
||||
from bot_bottle.backend.docker.backend import DockerBottleBackend
|
||||
from bot_bottle.backend import BottleBackend
|
||||
backend = DockerBottleBackend()
|
||||
# Call the base-class _image_stale_checks directly to verify it's a no-op.
|
||||
BottleBackend._image_stale_checks(backend, cast(Any, SimpleNamespace())) # type: ignore[arg-type]
|
||||
# Base-class prelaunch_checks is a no-op — must not raise.
|
||||
BottleBackend.prelaunch_checks(backend, cast(Any, SimpleNamespace())) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -142,13 +132,13 @@ class TestDockerStaleChecks(unittest.TestCase):
|
||||
self.assertEqual(1, cs.call_count)
|
||||
self.assertIn(plan.image, cs.call_args.args[0])
|
||||
|
||||
def test_backend_image_stale_checks_delegates(self) -> None:
|
||||
def test_backend_prelaunch_checks_delegates(self) -> None:
|
||||
from bot_bottle.backend.docker.backend import DockerBottleBackend
|
||||
from bot_bottle.backend.docker import launch as mod
|
||||
backend = DockerBottleBackend()
|
||||
plan = self._plan()
|
||||
with patch.object(mod, "stale_checks") as sc:
|
||||
backend._image_stale_checks(plan) # type: ignore[arg-type]
|
||||
backend.prelaunch_checks(plan) # type: ignore[arg-type]
|
||||
sc.assert_called_once_with(plan)
|
||||
|
||||
|
||||
@@ -227,13 +217,13 @@ class TestSmolmachinesStaleChecks(unittest.TestCase):
|
||||
sidecar_calls = [c for c in csp.call_args_list if "sidecar" in c.args[0]]
|
||||
self.assertGreaterEqual(len(sidecar_calls), 1)
|
||||
|
||||
def test_backend_image_stale_checks_delegates(self) -> None:
|
||||
def test_backend_prelaunch_checks_delegates(self) -> None:
|
||||
from bot_bottle.backend.smolmachines.backend import SmolmachinesBottleBackend
|
||||
from bot_bottle.backend.smolmachines import launch as mod
|
||||
backend = SmolmachinesBottleBackend()
|
||||
plan = self._plan()
|
||||
with patch.object(mod, "stale_checks") as sc:
|
||||
backend._image_stale_checks(plan) # type: ignore[arg-type]
|
||||
backend.prelaunch_checks(plan) # type: ignore[arg-type]
|
||||
sc.assert_called_once_with(plan)
|
||||
|
||||
|
||||
@@ -288,13 +278,13 @@ class TestMacosContainerStaleChecks(unittest.TestCase):
|
||||
mod.stale_checks(self._plan())
|
||||
cs.assert_not_called()
|
||||
|
||||
def test_backend_image_stale_checks_delegates(self) -> None:
|
||||
def test_backend_prelaunch_checks_delegates(self) -> None:
|
||||
from bot_bottle.backend.macos_container.backend import MacosContainerBottleBackend
|
||||
from bot_bottle.backend.macos_container import launch as mod
|
||||
backend = MacosContainerBottleBackend()
|
||||
plan = self._plan()
|
||||
with patch.object(mod, "stale_checks") as sc:
|
||||
backend._image_stale_checks(plan) # type: ignore[arg-type]
|
||||
backend.prelaunch_checks(plan) # type: ignore[arg-type]
|
||||
sc.assert_called_once_with(plan)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user