refactor(backend): rename consolidated_launch -> infra_launch (0081 review)
Mechanical rename addressing review 6141 on #519: "consolidated launch" was unclear about what it composes. Rename the per-backend module `consolidated_launch.py` -> `infra_launch.py` and the error `ConsolidatedLaunchError` -> `InfraLaunchError` across all three backends and their callers/tests. Unify the three identical per-backend error classes into one `InfraLaunchError` defined in `backend/base.py` (re-exported by each `infra_launch`) so the base backend can raise and catch a single shared type — groundwork for the base owning the gateway-attach flow. Add a glossary entry defining **infra** = the per-host gateway + orchestrator service pair every bottle attaches to. No behavior change. Refs #516, #519. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Unit: consolidated launch sequence — compose the orchestrator primitives (PRD 0070)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from bot_bottle.backend.docker.infra_launch import (
|
||||
launch_consolidated,
|
||||
deprovision_consolidated,
|
||||
)
|
||||
from bot_bottle.egress import EgressPlan, EgressRoute
|
||||
from bot_bottle.git_gate import GitGatePlan
|
||||
from bot_bottle.orchestrator.client import RegisteredBottle
|
||||
|
||||
_MOD = "bot_bottle.backend.docker.infra_launch"
|
||||
_UTIL = "bot_bottle.backend.provision_bottle"
|
||||
|
||||
|
||||
def _egress_plan() -> EgressPlan:
|
||||
return EgressPlan(
|
||||
slug="demo", routes_path=Path("/x"), routes=(EgressRoute(host="api.example.com"),),
|
||||
token_env_map={},
|
||||
)
|
||||
|
||||
|
||||
def _git_plan() -> GitGatePlan:
|
||||
return GitGatePlan(
|
||||
slug="demo", entrypoint_script=Path(), hook_script=Path(),
|
||||
access_hook_script=Path(), upstreams=(),
|
||||
)
|
||||
|
||||
|
||||
def _client(*, bottles: list[dict[str, object]] | None = None) -> Mock:
|
||||
c = Mock()
|
||||
c.list_bottles.return_value = bottles or []
|
||||
c.register_bottle.return_value = RegisteredBottle("b1", "tok")
|
||||
return c
|
||||
|
||||
|
||||
class TestLaunchConsolidated(unittest.TestCase):
|
||||
def _run(
|
||||
self, client: Mock, provision: Mock | None = None,
|
||||
*, on_network: tuple[str, ...] = ("172.18.0.2",),
|
||||
):
|
||||
service = MagicMock()
|
||||
service.ensure_running.return_value = "http://orch:8080"
|
||||
# The launch flow reads the gateway's agent-facing address + provisioning
|
||||
# transport off the Gateway service (service.gateway()).
|
||||
gateway = service.gateway.return_value
|
||||
gateway.name = "bot-bottle-gateway"
|
||||
gateway.address.return_value = "172.18.0.2"
|
||||
with patch(f"{_MOD}._network_cidr", return_value="172.18.0.0/16"), \
|
||||
patch(f"{_MOD}._network_container_ips", return_value=list(on_network)), \
|
||||
patch(f"{_MOD}.OrchestratorClient", return_value=client), \
|
||||
patch(f"{_UTIL}.provision_git_gate", provision or Mock()):
|
||||
return launch_consolidated(_egress_plan(), _git_plan(), service=service)
|
||||
|
||||
def test_allocates_ip_registers_and_provisions(self) -> None:
|
||||
client = _client()
|
||||
provision = Mock()
|
||||
ctx = self._run(client, provision)
|
||||
# .1 is the router, .2 is the gateway → first bottle gets .3.
|
||||
self.assertEqual("172.18.0.3", ctx.source_ip)
|
||||
self.assertEqual("172.18.0.2", ctx.gateway_ip)
|
||||
self.assertEqual("b1", ctx.bottle_id)
|
||||
self.assertEqual("tok", ctx.identity_token)
|
||||
self.assertEqual("http://orch:8080", ctx.orchestrator_url)
|
||||
# Registered with the source IP + the egress policy blob.
|
||||
kwargs = client.register_bottle.call_args
|
||||
self.assertEqual("172.18.0.3", kwargs.args[0])
|
||||
self.assertIn("api.example.com", kwargs.kwargs["policy"])
|
||||
provision.assert_called_once()
|
||||
|
||||
def test_skips_all_addresses_on_the_network(self) -> None:
|
||||
# Gateway .2 + orchestrator .3 already attached -> agent gets .4.
|
||||
ctx = self._run(_client(), on_network=("172.18.0.2", "172.18.0.3"))
|
||||
self.assertEqual("172.18.0.4", ctx.source_ip)
|
||||
|
||||
def test_provision_failure_rolls_back_registration(self) -> None:
|
||||
client = _client()
|
||||
provision = Mock(side_effect=RuntimeError("provision boom"))
|
||||
with self.assertRaises(RuntimeError):
|
||||
self._run(client, provision)
|
||||
client.teardown_bottle.assert_called_once_with("b1") # no orphan left
|
||||
|
||||
|
||||
class TestTeardownConsolidated(unittest.TestCase):
|
||||
def test_deregisters_and_deprovisions(self) -> None:
|
||||
client = Mock()
|
||||
with patch(f"{_UTIL}.OrchestratorClient", return_value=client), \
|
||||
patch(f"{_UTIL}.deprovision_git_gate") as deprov:
|
||||
deprovision_consolidated("b1", orchestrator_url="http://orch:8080")
|
||||
client.teardown_bottle.assert_called_once_with("b1")
|
||||
deprov.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user