"""Unit: stale-image check functions across backends, and the 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.""" from __future__ import annotations import unittest from pathlib import Path from types import SimpleNamespace from typing import Any, cast from unittest.mock import MagicMock, patch def _bottle_cm(bottle: Any) -> MagicMock: """Return a mock context manager that yields `bottle`.""" cm = MagicMock() cm.__enter__ = MagicMock(return_value=bottle) cm.__exit__ = MagicMock(return_value=False) return cm # --------------------------------------------------------------------------- # BottleBackend.launch template — prelaunch_checks + _build_or_load_images # --------------------------------------------------------------------------- class TestBottleBackendLaunchTemplate(unittest.TestCase): """Verify the concrete launch() method on BottleBackend calls _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_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, "_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 build_mock.assert_called_once_with(plan) impl_mock.assert_called_once_with(plan, images) def test_noop_default_prelaunch_checks(self) -> None: from bot_bottle.backend.docker.backend import DockerBottleBackend from bot_bottle.backend import BottleBackend backend = DockerBottleBackend() # Base-class prelaunch_checks is a no-op — must not raise. BottleBackend.prelaunch_checks(backend, cast(Any, SimpleNamespace())) # type: ignore[arg-type] # --------------------------------------------------------------------------- # Docker backend stale_checks # --------------------------------------------------------------------------- class TestDockerStaleChecks(unittest.TestCase): def _plan(self, policy: str = "cached", slug: str = "dev-abc") -> Any: spec = SimpleNamespace(image_policy=policy) provision = SimpleNamespace(image="bot-bottle-agent:latest") return cast(Any, SimpleNamespace( spec=spec, slug=slug, image="bot-bottle-agent:latest", agent_provision=provision, )) def test_fresh_policy_is_noop(self) -> None: from bot_bottle.backend.docker import launch as mod with patch.object(mod, "read_committed_image") as rci, \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan("fresh")) rci.assert_not_called() cs.assert_not_called() def test_committed_image_present_checks_only_committed(self) -> None: from bot_bottle.backend.docker import launch as mod from datetime import datetime, timezone ts = datetime(2025, 1, 1, tzinfo=timezone.utc) with patch.object(mod, "read_committed_image", return_value="committed:latest"), \ patch.object(mod.docker_mod, "image_exists", return_value=True), \ patch.object(mod.docker_mod, "image_created_at", return_value=ts), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) cs.assert_called_once() self.assertIn("committed:latest", cs.call_args.args[0]) def test_no_committed_image_checks_agent_and_sidecar(self) -> None: from bot_bottle.backend.docker import launch as mod from datetime import datetime, timezone ts = datetime(2025, 1, 1, tzinfo=timezone.utc) with patch.object(mod, "read_committed_image", return_value=""), \ patch.object(mod.docker_mod, "image_exists", return_value=True), \ patch.object(mod.docker_mod, "image_created_at", return_value=ts), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) # Should have been called for both agent and sidecar images. self.assertEqual(2, cs.call_count) def test_image_not_present_skips_check(self) -> None: from bot_bottle.backend.docker import launch as mod with patch.object(mod, "read_committed_image", return_value=""), \ patch.object(mod.docker_mod, "image_exists", return_value=False), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) cs.assert_not_called() def test_only_sidecar_missing_checks_only_agent(self) -> None: from bot_bottle.backend.docker import launch as mod from datetime import datetime, timezone ts = datetime(2025, 1, 1, tzinfo=timezone.utc) plan = self._plan() def image_exists(ref: str) -> bool: return ref == plan.image # Only agent present; sidecar missing. with patch.object(mod, "read_committed_image", return_value=""), \ patch.object(mod.docker_mod, "image_exists", side_effect=image_exists), \ patch.object(mod.docker_mod, "image_created_at", return_value=ts), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(plan) self.assertEqual(1, cs.call_count) self.assertIn(plan.image, cs.call_args.args[0]) 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.prelaunch_checks(plan) # type: ignore[arg-type] sc.assert_called_once_with(plan) # --------------------------------------------------------------------------- # macOS container backend stale_checks # --------------------------------------------------------------------------- class TestMacosContainerStaleChecks(unittest.TestCase): def _plan(self, policy: str = "cached", slug: str = "dev-abc") -> Any: return cast(Any, SimpleNamespace( spec=SimpleNamespace(image_policy=policy), slug=slug, image="bot-bottle-agent:latest", )) def test_fresh_policy_is_noop(self) -> None: from bot_bottle.backend.macos_container import launch as mod with patch.object(mod, "read_committed_image") as rci, \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan("fresh")) rci.assert_not_called() cs.assert_not_called() def test_committed_image_present_checks_only_committed(self) -> None: from bot_bottle.backend.macos_container import launch as mod from datetime import datetime, timezone ts = datetime(2025, 1, 1, tzinfo=timezone.utc) with patch.object(mod, "read_committed_image", return_value="committed:latest"), \ patch.object(mod.container_mod, "image_exists", return_value=True), \ patch.object(mod.container_mod, "image_created_at", return_value=ts), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) cs.assert_called_once() self.assertIn("committed:latest", cs.call_args.args[0]) def test_no_committed_image_checks_agent_and_sidecar(self) -> None: from bot_bottle.backend.macos_container import launch as mod from datetime import datetime, timezone ts = datetime(2025, 1, 1, tzinfo=timezone.utc) with patch.object(mod, "read_committed_image", return_value=""), \ patch.object(mod.container_mod, "image_exists", return_value=True), \ patch.object(mod.container_mod, "image_created_at", return_value=ts), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) self.assertEqual(2, cs.call_count) def test_image_not_present_skips_check(self) -> None: from bot_bottle.backend.macos_container import launch as mod with patch.object(mod, "read_committed_image", return_value=""), \ patch.object(mod.container_mod, "image_exists", return_value=False), \ patch.object(mod, "check_stale") as cs: mod.stale_checks(self._plan()) cs.assert_not_called() 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.prelaunch_checks(plan) # type: ignore[arg-type] sc.assert_called_once_with(plan) if __name__ == "__main__": unittest.main()