feat(supervise): start orchestrator on demand via backend-agnostic bring-up

Add BottleBackend.ensure_orchestrator() -> str: the backend-agnostic
entry point that brings up the per-host orchestrator + shared gateway
(idempotent) and returns its host-reachable control-plane URL. Docker
starts the orchestrator + gateway containers (OrchestratorService);
firecracker boots the infra VM; macos-container dies with a pointer
(no orchestrator). Previously bring-up was reachable only through each
backend's consolidated_launch, with no shared handle.

Wire it into `bot-bottle supervise`: supervise is often the first thing
an operator runs, before any bottle has booted the control plane, so
`_resolve_orchestrator_url` now starts the selected backend's
orchestrator on demand when discovery finds nothing, instead of failing
with "launch a bottle first".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
This commit is contained in:
2026-07-16 21:52:11 -04:00
parent 265119d601
commit 2641ab70fd
6 changed files with 105 additions and 2 deletions
+26
View File
@@ -176,5 +176,31 @@ class TestEditInEditor(unittest.TestCase):
os.environ["EDITOR"] = original_editor
class TestResolveOrchestratorUrl(unittest.TestCase):
"""`_resolve_orchestrator_url` starts the backend orchestrator on demand
when discovery finds nothing — supervise is often the first thing run."""
def test_returns_discovered_url_without_starting(self) -> None:
with patch.object(
supervise_cli, "discover_orchestrator_url",
return_value="http://127.0.0.1:8099",
), patch("bot_bottle.backend.get_bottle_backend") as get_backend:
url = supervise_cli._resolve_orchestrator_url()
self.assertEqual(url, "http://127.0.0.1:8099")
get_backend.assert_not_called() # nothing to start; discovery won
def test_starts_backend_orchestrator_when_none_running(self) -> None:
backend = MagicMock()
backend.name = "firecracker"
backend.ensure_orchestrator.return_value = "http://10.243.255.1:8099"
with patch.object(
supervise_cli, "discover_orchestrator_url",
side_effect=supervise_cli.OrchestratorClientError("none"),
), patch("bot_bottle.backend.get_bottle_backend", return_value=backend):
url = supervise_cli._resolve_orchestrator_url()
self.assertEqual(url, "http://10.243.255.1:8099")
backend.ensure_orchestrator.assert_called_once_with()
if __name__ == "__main__":
unittest.main()