"""Unit: bundle bringup primitives for the smolmachines backend (PRD 0023 chunk 2c). Tests mock `subprocess.run` and assert on the docker argv shape. The end-to-end integration smoke (real docker daemon, real bundle image) lands in chunk 2d.""" from __future__ import annotations import subprocess import unittest from pathlib import Path from unittest.mock import patch from bot_bottle.backend.smolmachines.sidecar_bundle import ( BundleLaunchSpec, bundle_container_name, bundle_network_name, create_bundle_network, ensure_bundle_image, start_bundle_vm, remove_bundle_network, start_bundle, stop_bundle, stop_bundle_vm, ) def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess: # type: ignore return subprocess.CompletedProcess( args=[], returncode=0, stdout=stdout, stderr=stderr, ) def _fail(stderr: str = "boom") -> subprocess.CompletedProcess: # type: ignore return subprocess.CompletedProcess( args=[], returncode=1, stdout="", stderr=stderr, ) def _spec(**kwargs) -> BundleLaunchSpec: # type: ignore defaults = dict( slug="demo-abc12", network_name="bot-bottle-bundle-demo-abc12", subnet="192.168.50.0/24", gateway="192.168.50.1", bundle_ip="192.168.50.2", ) defaults.update(kwargs) return BundleLaunchSpec(**defaults) # type: ignore class TestNamingHelpers(unittest.TestCase): def test_network_name_uses_bundle_prefix(self): # Distinct from the docker backend's # `bot-bottle-net-` so two backends running the # same agent slug don't collide. self.assertEqual( "bot-bottle-bundle-myagent-xyz", bundle_network_name("myagent-xyz"), ) def test_container_name_matches_docker_bundle_shape(self): # Same shape PRD 0024 chunk 5 set for the docker backend's # bundle container — dashboard prefix-discovery covers # both backends with one filter. self.assertEqual( "bot-bottle-sidecars-myagent-xyz", bundle_container_name("myagent-xyz"), ) class TestNetworkLifecycle(unittest.TestCase): def _patch_run(self, **kwargs): # type: ignore return patch( "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", **kwargs, ) def test_create_argv_explicit_subnet_and_gateway(self): with self._patch_run(return_value=_ok()) as m: create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1") self.assertEqual( ["docker", "network", "create", "--subnet", "192.168.50.0/24", "--gateway", "192.168.50.1", "nn"], m.call_args.args[0], ) def test_create_treats_existing_network_as_success(self): with self._patch_run(return_value=_fail("network nn already exists")): # No SystemExit. create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1") def test_create_other_failure_is_fatal(self): with self._patch_run(return_value=_fail("invalid subnet")): with self.assertRaises(SystemExit): create_bundle_network("nn", "bogus", "bogus") def test_remove_missing_network_is_idempotent(self): # No SystemExit / no warn-and-continue noise; missing # network is the expected case during a partial teardown. with self._patch_run(return_value=_fail("Error: No such network: nn")): remove_bundle_network("nn") def test_remove_clean_returns_success(self): with self._patch_run(return_value=_ok()): remove_bundle_network("nn") class TestStartBundle(unittest.TestCase): def _patch_run(self): return patch( "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_ok(), ) def test_argv_pins_ip_on_network(self): with self._patch_run() as m: start_bundle(_spec()) argv = m.call_args.args[0] # --network NETNAME --ip on the docker run. self.assertIn("--network", argv) self.assertIn("bot-bottle-bundle-demo-abc12", argv) self.assertIn("--ip", argv) self.assertIn("192.168.50.2", argv) # Detached and auto-removed. self.assertIn("--detach", argv) self.assertIn("--rm", argv) # Container name uses the per-slug bundle prefix. i = argv.index("--name") self.assertEqual("bot-bottle-sidecars-demo-abc12", argv[i + 1]) # Image at the end. self.assertEqual("bot-bottle-sidecars:latest", argv[-1]) def test_daemons_env_passed_in(self): with self._patch_run() as m: start_bundle(_spec(daemons_csv="egress,supervise")) argv = m.call_args.args[0] self.assertIn("-e", argv) self.assertIn( "BOT_BOTTLE_SIDECAR_DAEMONS=egress,supervise", argv, ) def test_environment_entries_pass_through(self): with self._patch_run() as m: start_bundle(_spec(environment=( "SUPERVISE_BOTTLE_SLUG=demo-abc12", "EGRESS_TOKEN_0", # bare-name → host env inherit ))) argv = m.call_args.args[0] self.assertIn("SUPERVISE_BOTTLE_SLUG=demo-abc12", argv) self.assertIn("EGRESS_TOKEN_0", argv) def test_volumes_render_with_ro_flag(self): with self._patch_run() as m: start_bundle(_spec(volumes=( ("/host/egress-ca.pem", "/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem", True), ("/host/queue", "/run/supervise/queue", False), ))) argv = m.call_args.args[0] self.assertIn( "/host/egress-ca.pem:/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem:ro", argv, ) self.assertIn("/host/queue:/run/supervise/queue", argv) def test_failure_dies(self): with patch( "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_fail("invalid mount"), ): with self.assertRaises(SystemExit): start_bundle(_spec()) def test_host_env_inherited_to_subprocess(self): # Bare-name entries in spec.environment rely on the docker # subprocess being run with the host env. Confirm `env=` # threads through. with patch( "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_ok(), ) as m: start_bundle(_spec(), env={"FOO": "bar"}) self.assertEqual({"FOO": "bar"}, m.call_args.kwargs["env"]) class TestEnsureBundleImage(unittest.TestCase): def test_builds_sidecar_dockerfile_before_plain_docker_run(self): with patch( "bot_bottle.backend.smolmachines.sidecar_bundle.docker_mod.build_image", ) as build: ensure_bundle_image() build.assert_called_once() args = build.call_args.args kwargs = build.call_args.kwargs self.assertEqual("bot-bottle-sidecars:latest", args[0]) self.assertTrue((Path(args[1]) / "Dockerfile.sidecars").is_file()) self.assertEqual("Dockerfile.sidecars", kwargs["dockerfile"]) class TestStartBundleVm(unittest.TestCase): def test_creates_sidecar_vm_with_ports_volumes_and_env(self): calls: list[tuple[str, tuple[object, ...], dict[str, object]]] = [] def record(name): # type: ignore def inner(*args, **kwargs): # type: ignore calls.append((name, args, kwargs)) return inner with patch( "bot_bottle.backend.smolmachines.sidecar_bundle.allocate_raw_host_ports", return_value={9099: 55001, 9420: 55002}, ), patch( "bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_create", side_effect=record("create"), ), patch( "bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_start", side_effect=record("start"), ), patch( "bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.wait_exec_ready", side_effect=record("wait"), ): launch = start_bundle_vm( _spec( daemons_csv="egress,git-http", environment=("FOO=bar", "TOKEN"), volumes=(("/host/routes", "/etc/egress", True),), ports_to_publish=(9099, 9420), ), from_path=Path("/cache/sidecar.smolmachine"), host_env={"TOKEN": "secret"}, ) self.assertEqual({9099: 55001, 9420: 55002}, launch.raw_ports) create = calls[0] self.assertEqual("create", create[0]) self.assertEqual(("bot-bottle-sidecars-demo-abc12",), create[1]) self.assertEqual(Path("/cache/sidecar.smolmachine"), create[2]["from_path"]) self.assertTrue(create[2]["net"]) self.assertEqual(((55001, 9099), (55002, 9420)), create[2]["ports"]) self.assertEqual((("/host/routes", "/etc/egress", True),), create[2]["volumes"]) self.assertEqual( { "BOT_BOTTLE_SIDECAR_DAEMONS": "egress,git-http", "FOO": "bar", "TOKEN": "secret", }, create[2]["env"], ) class TestStopBundle(unittest.TestCase): def _patch_run(self, **kwargs): # type: ignore return patch( "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", **kwargs, ) def test_argv_force_removes(self): with self._patch_run(return_value=_ok()) as m: stop_bundle("demo-abc12") self.assertEqual( ["docker", "rm", "-f", "bot-bottle-sidecars-demo-abc12"], m.call_args.args[0], ) def test_missing_container_is_idempotent(self): with self._patch_run(return_value=_fail( "Error: No such container: bot-bottle-sidecars-demo-abc12" )): stop_bundle("demo-abc12") # no raise class TestStopBundleVm(unittest.TestCase): def test_stops_then_deletes_sidecar_vm(self): with patch( "bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_stop", ) as stop, patch( "bot_bottle.backend.smolmachines.sidecar_bundle._smolvm.machine_delete", ) as delete: stop_bundle_vm("demo-abc12") stop.assert_called_once_with("bot-bottle-sidecars-demo-abc12") delete.assert_called_once_with("bot-bottle-sidecars-demo-abc12") if __name__ == "__main__": unittest.main()