44e2b5a897
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 42s
lint / lint (push) Failing after 54s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 17s
test / publish-infra (pull_request) Has been skipped
Give each service its own store package + manager, and cut the supervise module
along the control/data-plane boundary so nothing in the shared layer reaches up
into the orchestrator.
Stores, by owner:
- bot_bottle/store/ keeps only the shared base (DbStore, migrations) and the
concrete stores that aren't service-owned (audit_store, config_store).
- bot_bottle/orchestrator/store/ now houses the orchestrator-owned stores —
queue_store (supervise queue), secret_store, config_store — plus a new
orchestrator store_manager that migrates them (composing audit/config
downward from the base). The old shared store_manager is gone.
Supervise plane, by tier:
- bot_bottle/supervisor/ (NEUTRAL, importable by every tier including the
gateway): types.py (the Proposal/Response/AuditEntry wire types + the tool/
status/poll constants + the shared daemon constants moved out of
supervise.py) and plan.py (SupervisePlan, a pure DTO).
- bot_bottle/orchestrator/supervisor/ (orchestrator-only): queue.py (the
queue/audit I/O wrappers + render_diff + sha256_hex) and supervise.py (the
Supervise lifecycle that stages the DB via the store manager). Its __init__
re-exports the neutral vocabulary so orchestrator-side callers import from
one place.
The gateway now imports only bot_bottle.supervisor.types (never
bot_bottle.supervise), so the data plane holds no code dependency on the
orchestrator — it reaches the queue over the control-plane RPC. This removes
the circular import that moving queue_store under orchestrator introduced
(supervise -> orchestrator -> service -> supervise).
supervise_types.py -> supervisor/types.py; supervise.py deleted (split). Full
unit suite green (2251).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
"""Unit: OrchestratorConfigStore and resolve_teardown_timeout.
|
|
|
|
Also verifies the lifecycle ordering invariant: resolve_teardown_timeout()
|
|
must be called before launch_consolidated() / register_agent() so that a
|
|
resolver failure cannot leave an orphaned registration with no teardown
|
|
callback.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
from bot_bottle.orchestrator.store.config_store import (
|
|
DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
|
TEARDOWN_TIMEOUT_ENV,
|
|
OrchestratorConfigStore,
|
|
resolve_teardown_timeout,
|
|
)
|
|
|
|
|
|
class TestOrchestratorConfigStore(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.db = Path(self._tmp.name) / "test.db"
|
|
self.store = OrchestratorConfigStore(self.db)
|
|
self.store.migrate()
|
|
|
|
def tearDown(self) -> None:
|
|
self._tmp.cleanup()
|
|
|
|
def test_get_returns_none_when_not_set(self) -> None:
|
|
self.assertIsNone(self.store.get_teardown_timeout_seconds())
|
|
|
|
def test_set_and_get_roundtrip(self) -> None:
|
|
self.store.set_teardown_timeout_seconds(42.5)
|
|
self.assertEqual(42.5, self.store.get_teardown_timeout_seconds())
|
|
|
|
def test_set_overwrites_existing_value(self) -> None:
|
|
self.store.set_teardown_timeout_seconds(10.0)
|
|
self.store.set_teardown_timeout_seconds(20.0)
|
|
self.assertEqual(20.0, self.store.get_teardown_timeout_seconds())
|
|
|
|
def test_delete_clears_value_and_returns_true(self) -> None:
|
|
self.store.set_teardown_timeout_seconds(30.0)
|
|
deleted = self.store.delete_teardown_timeout_seconds()
|
|
self.assertTrue(deleted)
|
|
self.assertIsNone(self.store.get_teardown_timeout_seconds())
|
|
|
|
def test_delete_absent_returns_false(self) -> None:
|
|
self.assertFalse(self.store.delete_teardown_timeout_seconds())
|
|
|
|
def test_is_migrated_true_after_migrate(self) -> None:
|
|
self.assertTrue(self.store.is_migrated())
|
|
|
|
def test_is_migrated_false_before_migrate(self) -> None:
|
|
store = OrchestratorConfigStore(Path(self._tmp.name) / "new.db")
|
|
self.assertFalse(store.is_migrated())
|
|
|
|
|
|
class TestResolveTeardownTimeout(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.db = Path(self._tmp.name) / "cfg.db"
|
|
|
|
def tearDown(self) -> None:
|
|
self._tmp.cleanup()
|
|
os.environ.pop(TEARDOWN_TIMEOUT_ENV, None)
|
|
|
|
def test_returns_default_when_nothing_configured(self) -> None:
|
|
self.assertEqual(
|
|
DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
|
resolve_teardown_timeout(self.db),
|
|
)
|
|
|
|
def test_env_var_overrides_default(self) -> None:
|
|
os.environ[TEARDOWN_TIMEOUT_ENV] = "99"
|
|
self.assertEqual(99.0, resolve_teardown_timeout(self.db))
|
|
|
|
def test_env_var_overrides_db_value(self) -> None:
|
|
store = OrchestratorConfigStore(self.db)
|
|
store.migrate()
|
|
store.set_teardown_timeout_seconds(55.0)
|
|
os.environ[TEARDOWN_TIMEOUT_ENV] = "77"
|
|
self.assertEqual(77.0, resolve_teardown_timeout(self.db))
|
|
|
|
def test_db_value_overrides_default(self) -> None:
|
|
store = OrchestratorConfigStore(self.db)
|
|
store.migrate()
|
|
store.set_teardown_timeout_seconds(42.0)
|
|
self.assertEqual(42.0, resolve_teardown_timeout(self.db))
|
|
|
|
def test_invalid_env_var_falls_through_to_default(self) -> None:
|
|
os.environ[TEARDOWN_TIMEOUT_ENV] = "not-a-number"
|
|
self.assertEqual(DEFAULT_TEARDOWN_TIMEOUT_SECONDS, resolve_teardown_timeout(self.db))
|
|
|
|
def test_non_positive_env_var_falls_through_to_default(self) -> None:
|
|
os.environ[TEARDOWN_TIMEOUT_ENV] = "0"
|
|
self.assertEqual(DEFAULT_TEARDOWN_TIMEOUT_SECONDS, resolve_teardown_timeout(self.db))
|
|
|
|
def test_non_positive_db_value_falls_through_to_default(self) -> None:
|
|
store = OrchestratorConfigStore(self.db)
|
|
store.migrate()
|
|
store.set_teardown_timeout_seconds(0.0)
|
|
self.assertEqual(DEFAULT_TEARDOWN_TIMEOUT_SECONDS, resolve_teardown_timeout(self.db))
|
|
|
|
def test_migrates_db_on_first_call(self) -> None:
|
|
result = resolve_teardown_timeout(self.db)
|
|
self.assertEqual(DEFAULT_TEARDOWN_TIMEOUT_SECONDS, result)
|
|
self.assertTrue(OrchestratorConfigStore(self.db).is_migrated())
|
|
|
|
|
|
class TestTeardownTimeoutResolvedBeforeRegistration(unittest.TestCase):
|
|
"""Ordering invariant: if resolve_teardown_timeout() raises, the bottle
|
|
must not yet be registered — no orphaned state can result."""
|
|
|
|
def _src(self, module: ModuleType) -> str:
|
|
return inspect.getsource(module)
|
|
|
|
def test_docker_resolves_timeout_before_launch_consolidated(self) -> None:
|
|
from bot_bottle.backend.docker import launch
|
|
src = self._src(launch)
|
|
resolve_at = src.index("teardown_timeout = resolve_teardown_timeout()")
|
|
launch_at = src.index("ctx = launch_consolidated(")
|
|
self.assertLess(resolve_at, launch_at)
|
|
|
|
def test_firecracker_resolves_timeout_before_launch_consolidated(self) -> None:
|
|
from bot_bottle.backend.firecracker import launch
|
|
src = self._src(launch)
|
|
resolve_at = src.index("teardown_timeout = resolve_teardown_timeout()")
|
|
launch_at = src.index("ctx = launch_consolidated(")
|
|
self.assertLess(resolve_at, launch_at)
|
|
|
|
def test_macos_resolves_timeout_before_register_agent(self) -> None:
|
|
from bot_bottle.backend.macos_container import launch
|
|
src = self._src(launch)
|
|
resolve_at = src.index("teardown_timeout = resolve_teardown_timeout()")
|
|
register_at = src.index("ctx = register_agent(")
|
|
self.assertLess(resolve_at, register_at)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|