"""Unit: the shared gateway-attach reconcile flow (PRD 0081). `reconcile_running_bottles` drives the cold-boot reattach across every backend via three primitives. These exercise the control flow with a duck-typed fake (the real `BottleBackend` ABC has a large surface; the reconcile only touches the three methods below).""" from __future__ import annotations import unittest from bot_bottle.backend.base import InfraLaunchError from bot_bottle.backend.gateway_attach import ( GatewayAttachResources, reconcile_running_bottles, ) class _FakeBackend: def __init__(self, bottles: list[str], *, attach_fail: tuple[str, ...] = ()) -> None: self._bottles = bottles self._attach_fail = set(attach_fail) self.resource_calls = 0 self.attached: list[tuple[str, str]] = [] def _running_bottles(self) -> list[str]: return self._bottles def _gateway_attach_resources(self) -> GatewayAttachResources: self.resource_calls += 1 return GatewayAttachResources(ca_pem="CA") def _attach_bottle_to_gateway( self, bottle: str, resources: GatewayAttachResources, ) -> None: if bottle in self._attach_fail: raise InfraLaunchError(f"attach {bottle} failed") self.attached.append((bottle, resources.ca_pem)) class TestReconcileRunningBottles(unittest.TestCase): def test_no_bottles_skips_resource_gathering(self) -> None: """With no running bottles the reconcile is a no-op and never gathers resources — that read targets the per-host singleton gateway, so doing it here would make a non-default/isolated bring-up fail looking for the default `bot-bottle-orch-gateway` (#519 review).""" fake = _FakeBackend([]) reconcile_running_bottles(fake) # type: ignore[arg-type] self.assertEqual(0, fake.resource_calls) self.assertEqual([], fake.attached) def test_attaches_every_running_bottle_once(self) -> None: fake = _FakeBackend(["a", "b"]) reconcile_running_bottles(fake) # type: ignore[arg-type] self.assertEqual(1, fake.resource_calls) self.assertEqual([("a", "CA"), ("b", "CA")], fake.attached) def test_aggregates_attach_failures_and_raises(self) -> None: """Every bottle is attempted before raising, and the failures are reported together (fail hard, full blast radius).""" fake = _FakeBackend(["a", "b", "c"], attach_fail=("a", "c")) with self.assertRaises(InfraLaunchError) as ctx: reconcile_running_bottles(fake) # type: ignore[arg-type] msg = str(ctx.exception) self.assertIn("2 running bottle(s)", msg) self.assertIn("attach a failed", msg) self.assertIn("attach c failed", msg) self.assertEqual([("b", "CA")], fake.attached) if __name__ == "__main__": unittest.main()