189 lines
6.8 KiB
Python
189 lines
6.8 KiB
Python
"""Unit: smolmachines `_ensure_smolmachine` agent-image pipeline
|
|
(PRD 0023 chunk 4c).
|
|
|
|
Asserts that the cache-hit path returns without touching the
|
|
registry / pack pipeline, and that the cache-miss path runs
|
|
build → tag → push → pack in order against a registry port the
|
|
helper yields.
|
|
|
|
The pipeline lives in `launch.py` (moved from `prepare.py` so the
|
|
docker build doesn't run before the dashboard's preflight modal;
|
|
the curses-endwin / tmux pane-routing handoff happens around
|
|
`launch`)."""
|
|
|
|
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
|
|
|
|
|
|
class TestEnsureSmolmachine(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory(prefix="cb-cache.")
|
|
self._cache_patch = patch.object(
|
|
_launch_mod, "_SMOLMACHINE_CACHE_DIR", Path(self._tmp.name),
|
|
)
|
|
self._cache_patch.start()
|
|
|
|
def tearDown(self):
|
|
self._cache_patch.stop()
|
|
self._tmp.cleanup()
|
|
|
|
def test_cache_hit_skips_registry_and_pack(self):
|
|
# Pre-populate the cache for image id `sha256:abcdef0123456789...`.
|
|
digest = "abcdef0123456789"
|
|
sidecar = Path(self._tmp.name) / f"{digest}.smolmachine.smolmachine"
|
|
sidecar.write_text("")
|
|
|
|
with patch.object(
|
|
_launch_mod.docker_mod, "build_image",
|
|
) as build, patch.object(
|
|
_launch_mod.docker_mod, "image_id",
|
|
return_value=f"sha256:{digest}fffffffffffffffff",
|
|
), patch.object(
|
|
_launch_mod.docker_mod, "save",
|
|
) as save, patch.object(
|
|
_launch_mod, "ephemeral_registry",
|
|
) as registry, patch.object(
|
|
_launch_mod, "crane_push_tarball",
|
|
) as push, patch.object(
|
|
_launch_mod._smolvm, "pack_create",
|
|
) as pack:
|
|
result = _launch_mod._ensure_smolmachine("bot-bottle-claude:latest")
|
|
|
|
self.assertEqual(sidecar, result)
|
|
# build still runs (Dockerfile edits land without manual rmi).
|
|
build.assert_called_once()
|
|
# No save (500MB tarball), no registry, no push, no pack on
|
|
# cache hit.
|
|
save.assert_not_called()
|
|
registry.assert_not_called()
|
|
push.assert_not_called()
|
|
pack.assert_not_called()
|
|
|
|
def test_cache_miss_runs_build_save_push_pack_in_order(self):
|
|
digest = "0123456789abcdef"
|
|
|
|
# ephemeral_registry yields a RegistryHandle with the
|
|
# docker network + a push endpoint (container DNS) and
|
|
# pull endpoint (host port-forward).
|
|
from bot_bottle.backend.smolmachines.local_registry import (
|
|
RegistryHandle,
|
|
)
|
|
|
|
class _Reg:
|
|
def __enter__(self_inner): # type: ignore
|
|
return RegistryHandle(
|
|
network="cb-net-xyz",
|
|
push_endpoint="cb-registry-xyz:5000",
|
|
pull_endpoint="localhost:54321",
|
|
)
|
|
def __exit__(self_inner, *exc): # type: ignore
|
|
return False
|
|
|
|
calls: list[str] = []
|
|
|
|
def record(name): # type: ignore
|
|
def _f(*a, **kw): # type: ignore
|
|
calls.append(name)
|
|
return _f
|
|
|
|
with patch.object(
|
|
_launch_mod.docker_mod, "build_image",
|
|
side_effect=record("build"),
|
|
), patch.object(
|
|
_launch_mod.docker_mod, "image_id",
|
|
return_value=f"sha256:{digest}fffffffffffffffff",
|
|
), patch.object(
|
|
_launch_mod.docker_mod, "save",
|
|
side_effect=record("save"),
|
|
) as save, patch.object(
|
|
_launch_mod, "ephemeral_registry",
|
|
return_value=_Reg(),
|
|
), patch.object(
|
|
_launch_mod, "crane_push_tarball",
|
|
side_effect=record("push"),
|
|
) as push, patch.object(
|
|
_launch_mod._smolvm, "pack_create",
|
|
side_effect=record("pack"),
|
|
) as pack:
|
|
_launch_mod._ensure_smolmachine("bot-bottle-claude:latest")
|
|
|
|
# Build → save → push → pack in that order. No `docker
|
|
# push` (the daemon's HTTPS-by-default path is what we're
|
|
# sidestepping).
|
|
self.assertEqual(["build", "save", "push", "pack"], calls)
|
|
|
|
# docker save targets a per-digest tarball alongside the
|
|
# cached sidecar.
|
|
save_args = save.call_args.args
|
|
self.assertEqual("bot-bottle-claude:latest", save_args[0])
|
|
self.assertTrue(save_args[1].endswith(f"{digest}.image.tar"))
|
|
|
|
# crane push runs against the push_endpoint (container DNS
|
|
# on the registry network) with the digest as the tag.
|
|
push_args = push.call_args.args
|
|
self.assertEqual(
|
|
f"cb-registry-xyz:5000/bot-bottle:{digest}", push_args[2],
|
|
)
|
|
|
|
# pack_create reads from the pull_endpoint (host port-
|
|
# forward, smolvm is on the host). Same repo+tag, just a
|
|
# different routing hostname — the registry stores one blob.
|
|
pack_args = pack.call_args.args
|
|
self.assertEqual(
|
|
f"localhost:54321/bot-bottle:{digest}", pack_args[0],
|
|
)
|
|
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()
|