refactor: move bot_bottle_root/host_db_path to paths; kill the monkeypatch (#352)
lint / lint (push) Successful in 1m57s
test / unit (pull_request) Successful in 55s
test / integration (pull_request) Successful in 15s
test / coverage (pull_request) Successful in 1m1s

Create bot_bottle/paths.py as the canonical home for the app-root path
helpers (bot_bottle_root, host_db_path, HOST_DB_FILENAME) — foundational,
not supervise- or db-specific. `bot_bottle_root()` now honours a
BOT_BOTTLE_ROOT env override.

Repoint every consumer (supervise, supervise_types, db_store, queue_store,
audit_store, store_manager, bottle_state, cli/supervise, docker/cleanup,
orchestrator/registry) at paths; remove the definitions (and supervise's
duplicate host_db_path) and the now-dead `import sys`. Add paths.py to the
sidecar bundle (Dockerfile.sidecars) for the flat-import copies.

Tests: replace ~12 files' monkeypatching of supervise.bot_bottle_root (and
the flat/pkg/supervise_types triple-patch dance) with a single
`use_bottle_root()` helper that sets BOT_BOTTLE_ROOT — every module and
flat/package copy reads the same env var, so one override covers them all.
Net -97 lines. Behaviour-preserving: full unit suite unchanged (only the
pre-existing /bin/sleep sidecar-init errors remain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-13 14:04:23 -04:00
parent b4b4e08f62
commit 421d31c32f
26 changed files with 135 additions and 232 deletions
+1
View File
@@ -66,6 +66,7 @@ COPY bot_bottle/egress_dlp_config.py /app/egress_dlp_config.py
COPY bot_bottle/egress_addon.py /app/egress_addon.py COPY bot_bottle/egress_addon.py /app/egress_addon.py
COPY bot_bottle/dlp_detectors.py /app/dlp_detectors.py COPY bot_bottle/dlp_detectors.py /app/dlp_detectors.py
COPY bot_bottle/yaml_subset.py /app/yaml_subset.py COPY bot_bottle/yaml_subset.py /app/yaml_subset.py
COPY bot_bottle/paths.py /app/paths.py
COPY bot_bottle/migrations.py /app/migrations.py COPY bot_bottle/migrations.py /app/migrations.py
COPY bot_bottle/db_store.py /app/db_store.py COPY bot_bottle/db_store.py /app/db_store.py
COPY bot_bottle/supervise_types.py /app/supervise_types.py COPY bot_bottle/supervise_types.py /app/supervise_types.py
+4 -2
View File
@@ -6,11 +6,13 @@ import sqlite3
from pathlib import Path from pathlib import Path
try: try:
from .supervise_types import AuditEntry, host_db_path from .supervise_types import AuditEntry
from .paths import host_db_path
from .db_store import DbStore from .db_store import DbStore
from .migrations import TableMigrations from .migrations import TableMigrations
except ImportError: except ImportError:
from supervise_types import AuditEntry, host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from supervise_types import AuditEntry # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
+2 -2
View File
@@ -26,7 +26,7 @@ from __future__ import annotations
import shutil import shutil
import subprocess import subprocess
from ... import supervise as _supervise from ...paths import bot_bottle_root
from ...log import info, warn from ...log import info, warn
from . import util as docker_mod from . import util as docker_mod
from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_cleanup_plan import DockerBottleCleanupPlan
@@ -94,7 +94,7 @@ def _list_orphan_state_dirs(
ANY backend — used so this docker-side check doesn't reap a ANY backend — used so this docker-side check doesn't reap a
running non-docker bottle's state dir (the layout is shared running non-docker bottle's state dir (the layout is shared
across backends).""" across backends)."""
state_root = _supervise.bot_bottle_root() / "state" state_root = bot_bottle_root() / "state"
if not state_root.is_dir(): if not state_root.is_dir():
return [] return []
orphans: list[str] = [] orphans: list[str] = []
+2 -2
View File
@@ -36,7 +36,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from . import supervise as _supervise from .paths import bot_bottle_root
# Directory layout: ~/.bot-bottle/state/<identity>/... # Directory layout: ~/.bot-bottle/state/<identity>/...
@@ -163,7 +163,7 @@ def read_metadata(identity: str) -> BottleMetadata | None:
def bottle_state_dir(identity: str) -> Path: def bottle_state_dir(identity: str) -> Path:
"""Per-bottle state directory on the host. Created lazily by the """Per-bottle state directory on the host. Created lazily by the
write helpers; readers tolerate its absence.""" write helpers; readers tolerate its absence."""
return _supervise.bot_bottle_root() / _STATE_SUBDIR / identity return bot_bottle_root() / _STATE_SUBDIR / identity
def per_bottle_dockerfile_path(identity: str) -> Path: def per_bottle_dockerfile_path(identity: str) -> Path:
+2 -2
View File
@@ -19,7 +19,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from .. import supervise as _supervise from ..paths import bot_bottle_root
from ..bottle_state import read_metadata from ..bottle_state import read_metadata
from ..backend.docker.egress_apply import ( from ..backend.docker.egress_apply import (
EgressApplyError, EgressApplyError,
@@ -276,7 +276,7 @@ def _write_crash_log(exc: BaseException) -> Path:
) )
entry = f"=== supervise crash {stamp} ===\n{body}\n" entry = f"=== supervise crash {stamp} ===\n{body}\n"
try: try:
log_dir = _supervise.bot_bottle_root() / "logs" log_dir = bot_bottle_root() / "logs"
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
path = log_dir / "supervise-crash.log" path = log_dir / "supervise-crash.log"
with path.open("a", encoding="utf-8") as fh: with path.open("a", encoding="utf-8") as fh:
+1 -25
View File
@@ -11,30 +11,6 @@ except ImportError:
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
# The single shared host state DB. All bot-bottle SQLite stores (supervise
# queue, audit, the orchestrator registry) co-tenant this one file — the
# TableMigrations schema_key namespaces each store's tables.
HOST_DB_FILENAME = "bot-bottle.db"
def host_db_path() -> Path:
"""Path to the shared host state DB, `<bot_bottle_root>/db/bot-bottle.db`.
Kept in its own `db/` subdirectory (not directly under the root) so a
backend that can only bind-mount *directories* can share this one file
with a sidecar without exposing the root's other contents (git-gate
keys, per-bottle state, ...).
Resolves `bot_bottle_root` from `supervise_types` at call time (a lazy
import — avoids a load-time cycle, and lets a monkey-patch of
`supervise_types.bot_bottle_root` propagate here)."""
try:
from . import supervise_types as _st
except ImportError: # flat imports inside the sidecar bundle
import supervise_types as _st # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
return _st.bot_bottle_root() / "db" / HOST_DB_FILENAME
class DbVersionError(Exception): class DbVersionError(Exception):
"""Raised when the on-disk schema is behind the current migration list.""" """Raised when the on-disk schema is behind the current migration list."""
@@ -80,4 +56,4 @@ class DbStore:
pass pass
__all__ = ["DbStore", "DbVersionError", "HOST_DB_FILENAME", "host_db_path"] __all__ = ["DbStore", "DbVersionError"]
+2 -1
View File
@@ -35,8 +35,9 @@ import time
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from ..db_store import DbStore, host_db_path from ..db_store import DbStore
from ..migrations import TableMigrations from ..migrations import TableMigrations
from ..paths import host_db_path
# 256 bits of urandom, URL-safe — unguessable per-bottle identity token. # 256 bits of urandom, URL-safe — unguessable per-bottle identity token.
IDENTITY_TOKEN_BYTES = 32 IDENTITY_TOKEN_BYTES = 32
+43
View File
@@ -0,0 +1,43 @@
"""Foundational filesystem paths for bot-bottle.
`bot_bottle_root()` is the app data root — state, queue, audit logs,
git-gate keys, and the shared DB all live under it. It defaults to
`~/.bot-bottle` and is overridable with the **`BOT_BOTTLE_ROOT`** env var.
The env override is the single knob for redirecting the root: the test
suite points it at a throwaway dir instead of monkey-patching the function
(every module and every flat/package copy reads the same env var, so one
override covers them all), and operators can relocate the root if needed.
This module has no bot-bottle imports, so it is safe to import from any
layer (and to COPY flat into the sidecar bundle).
"""
from __future__ import annotations
import os
from pathlib import Path
# The single shared host state DB. All bot-bottle SQLite stores (supervise
# queue, audit, the orchestrator registry) co-tenant this one file — the
# TableMigrations schema_key namespaces each store's tables.
HOST_DB_FILENAME = "bot-bottle.db"
def bot_bottle_root() -> Path:
"""The app data root — `$BOT_BOTTLE_ROOT` if set, else `~/.bot-bottle`."""
override = os.environ.get("BOT_BOTTLE_ROOT")
return Path(override) if override else Path.home() / ".bot-bottle"
def host_db_path() -> Path:
"""Path to the shared host state DB, `<root>/db/bot-bottle.db`.
Kept in its own `db/` subdirectory (not directly under the root) so a
backend that can only bind-mount *directories* can share this one file
with a sidecar without exposing the root's other contents (git-gate
keys, per-bottle state, ...)."""
return bot_bottle_root() / "db" / HOST_DB_FILENAME
__all__ = ["HOST_DB_FILENAME", "bot_bottle_root", "host_db_path"]
+4 -2
View File
@@ -7,11 +7,13 @@ import sqlite3
from pathlib import Path from pathlib import Path
try: try:
from .supervise_types import Proposal, Response, host_db_path from .supervise_types import Proposal, Response
from .paths import host_db_path
from .db_store import DbStore from .db_store import DbStore
from .migrations import TableMigrations from .migrations import TableMigrations
except ImportError: except ImportError:
from supervise_types import Proposal, Response, host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from supervise_types import Proposal, Response # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
+2 -5
View File
@@ -23,13 +23,10 @@ class StoreManager:
def __init__(self, db_path: Path | None = None) -> None: def __init__(self, db_path: Path | None = None) -> None:
if db_path is None: if db_path is None:
# Lazy import to avoid a circular dependency: supervise imports
# StoreManager at module level; StoreManager must not import
# supervise at module level in return.
try: try:
from .supervise import host_db_path from .paths import host_db_path
except ImportError: except ImportError:
from supervise import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
db_path = host_db_path() db_path = host_db_path()
self.db_path = db_path self.db_path = db_path
+6 -14
View File
@@ -41,7 +41,6 @@ try:
from .supervise_types import ( from .supervise_types import (
ACTION_OPERATOR_EDIT, ACTION_OPERATOR_EDIT,
AuditEntry, AuditEntry,
HOST_DB_FILENAME,
Proposal, Proposal,
Response, Response,
STATUSES, STATUSES,
@@ -54,13 +53,11 @@ try:
TOOL_EGRESS_TOKEN_ALLOW, TOOL_EGRESS_TOKEN_ALLOW,
TOOL_GITLEAKS_ALLOW, TOOL_GITLEAKS_ALLOW,
TOOL_LIST_EGRESS_ROUTES, TOOL_LIST_EGRESS_ROUTES,
bot_bottle_root,
) )
except ImportError: except ImportError:
from supervise_types import ( # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module from supervise_types import ( # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
ACTION_OPERATOR_EDIT, ACTION_OPERATOR_EDIT,
AuditEntry, AuditEntry,
HOST_DB_FILENAME,
Proposal, Proposal,
Response, Response,
STATUSES, STATUSES,
@@ -73,10 +70,15 @@ except ImportError:
TOOL_EGRESS_TOKEN_ALLOW, TOOL_EGRESS_TOKEN_ALLOW,
TOOL_GITLEAKS_ALLOW, TOOL_GITLEAKS_ALLOW,
TOOL_LIST_EGRESS_ROUTES, TOOL_LIST_EGRESS_ROUTES,
bot_bottle_root,
) )
try:
from .paths import bot_bottle_root, host_db_path
except ImportError: # flat imports inside the sidecar bundle
from paths import bot_bottle_root, host_db_path # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
SUPERVISE_HOSTNAME = "supervise" SUPERVISE_HOSTNAME = "supervise"
SUPERVISE_PORT = 9100 SUPERVISE_PORT = 9100
@@ -106,16 +108,6 @@ def audit_dir() -> Path:
return bot_bottle_root() / "audit" return bot_bottle_root() / "audit"
def host_db_path() -> Path:
# Calls bot_bottle_root() through this module's globals so that patches
# on supervise.bot_bottle_root propagate to callers going through
# supervise.host_db_path().
#
# Kept in its own "db" subdirectory (see supervise_types.host_db_path
# for why) — must stay in sync with that copy.
return bot_bottle_root() / "db" / HOST_DB_FILENAME
def audit_log_path(component: str, slug: str) -> Path: def audit_log_path(component: str, slug: str) -> Path:
return audit_dir() / f"{component}-{slug}.log" return audit_dir() / f"{component}-{slug}.log"
+5 -25
View File
@@ -1,14 +1,11 @@
"""Shared types and path helpers for supervise stores (PRD 0013). """Shared types for supervise stores (PRD 0013).
Extracted from supervise.py so queue_store and audit_store can import Extracted from supervise.py so queue_store and audit_store can import
Proposal, Response, AuditEntry, and host_db_path without creating a Proposal, Response, and AuditEntry without creating a circular import
circular import (supervise imports from queue_store/audit_store and (supervise imports from queue_store/audit_store and vice-versa).
vice-versa).
host_db_path / HOST_DB_FILENAME now live in db_store (shared DB infra) and The path helpers (`bot_bottle_root`, `host_db_path`, `HOST_DB_FILENAME`)
are re-exported here for the historical import path. db_store.host_db_path live in `paths` — import them from there.
resolves bot_bottle_root from this module lazily, so patching
supervise_types.bot_bottle_root still propagates to the DB path.
""" """
from __future__ import annotations from __future__ import annotations
@@ -17,16 +14,6 @@ import dataclasses
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path
# host_db_path / HOST_DB_FILENAME now live in db_store (shared DB infra);
# re-exported here for the historical import path. db_store resolves
# bot_bottle_root from this module lazily, so patching
# supervise_types.bot_bottle_root still propagates.
try:
from .db_store import HOST_DB_FILENAME, host_db_path
except ImportError: # flat imports inside the sidecar bundle
from db_store import HOST_DB_FILENAME, host_db_path # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
TOOL_EGRESS_BLOCK = "egress-block" TOOL_EGRESS_BLOCK = "egress-block"
TOOL_EGRESS_ALLOW = "egress-allow" TOOL_EGRESS_ALLOW = "egress-allow"
@@ -49,10 +36,6 @@ STATUSES: tuple[str, ...] = (STATUS_APPROVED, STATUS_MODIFIED, STATUS_REJECTED)
ACTION_OPERATOR_EDIT = "operator-edit" ACTION_OPERATOR_EDIT = "operator-edit"
def bot_bottle_root() -> Path:
return Path.home() / ".bot-bottle"
def _require_str(raw: dict[str, object], key: str) -> str: def _require_str(raw: dict[str, object], key: str) -> str:
value = raw.get(key) value = raw.get(key)
if not isinstance(value, str): if not isinstance(value, str):
@@ -164,7 +147,6 @@ class AuditEntry:
__all__ = [ __all__ = [
"ACTION_OPERATOR_EDIT", "ACTION_OPERATOR_EDIT",
"AuditEntry", "AuditEntry",
"HOST_DB_FILENAME",
"Proposal", "Proposal",
"Response", "Response",
"STATUSES", "STATUSES",
@@ -177,6 +159,4 @@ __all__ = [
"TOOL_EGRESS_TOKEN_ALLOW", "TOOL_EGRESS_TOKEN_ALLOW",
"TOOL_GITLEAKS_ALLOW", "TOOL_GITLEAKS_ALLOW",
"TOOL_LIST_EGRESS_ROUTES", "TOOL_LIST_EGRESS_ROUTES",
"bot_bottle_root",
"host_db_path",
] ]
+21 -2
View File
@@ -10,8 +10,12 @@ and unisolated tests otherwise pollute the developer's home dir.
Individual tests that need their own ``HOME`` still override Individual tests that need their own ``HOME`` still override
``os.environ['HOME']`` and restore it; they now restore to this isolated ``os.environ['HOME']`` and restore it; they now restore to this isolated
dir rather than the real one, so isolation holds either way. Tests that dir rather than the real one, so isolation holds either way.
patch ``supervise.bot_bottle_root`` directly are unaffected.
Tests that need their own app-data root call ``use_bottle_root`` (below),
which sets ``BOT_BOTTLE_ROOT`` the supported override read by
``paths.bot_bottle_root`` instead of monkey-patching. One env setting
covers every module and every flat/package copy of the helper.
""" """
from __future__ import annotations from __future__ import annotations
@@ -20,6 +24,9 @@ import atexit
import os import os
import shutil import shutil
import tempfile import tempfile
from collections.abc import Callable
from pathlib import Path
from unittest import mock
_real_home = os.environ.get("HOME") _real_home = os.environ.get("HOME")
_tmp_home = tempfile.mkdtemp(prefix="bot-bottle-unit-home.") _tmp_home = tempfile.mkdtemp(prefix="bot-bottle-unit-home.")
@@ -35,3 +42,15 @@ def _restore_home() -> None:
atexit.register(_restore_home) atexit.register(_restore_home)
def use_bottle_root(root: Path) -> Callable[[], None]:
"""Redirect ``paths.bot_bottle_root()`` to ``root`` by setting
``BOT_BOTTLE_ROOT`` the supported override. Returns a callable that
undoes it (store it as the test's restore hook, or pass to addCleanup).
Replaces monkey-patching ``supervise.bot_bottle_root``; one env var
covers every module and the flat/package copies alike (they all read it)."""
patcher = mock.patch.dict(os.environ, {"BOT_BOTTLE_ROOT": str(root)})
patcher.start()
return patcher.stop
+3 -8
View File
@@ -7,7 +7,8 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from bot_bottle import supervise, bottle_state from bot_bottle import bottle_state
from tests.unit import use_bottle_root
from bot_bottle.backend import ActiveAgent from bot_bottle.backend import ActiveAgent
from bot_bottle.backend.freeze import get_freezer from bot_bottle.backend.freeze import get_freezer
from bot_bottle.backend.docker.freezer import DockerFreezer from bot_bottle.backend.docker.freezer import DockerFreezer
@@ -18,13 +19,7 @@ from bot_bottle.backend.firecracker.freezer import FirecrackerFreezer
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="freezer-test.") self._tmp = tempfile.TemporaryDirectory(prefix="freezer-test.")
original = supervise.bot_bottle_root self._restore = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
def _teardown_fake_home(self): def _teardown_fake_home(self):
self._restore() self._restore()
+3 -4
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle.backend import BottleSpec from bot_bottle.backend import BottleSpec
from bot_bottle.backend.docker import DockerBottleBackend from bot_bottle.backend.docker import DockerBottleBackend
from bot_bottle.backend.resolve_common import mint_slug from bot_bottle.backend.resolve_common import mint_slug
@@ -55,11 +55,10 @@ class _FakeStateMixin:
def setUp(self) -> None: def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory(prefix="backend-prepare.") self.tmp = tempfile.TemporaryDirectory(prefix="backend-prepare.")
self.root = Path(self.tmp.name) / ".bot-bottle" self.root = Path(self.tmp.name) / ".bot-bottle"
self.original_root = supervise.bot_bottle_root self._restore = use_bottle_root(self.root)
supervise.bot_bottle_root = lambda: self.root # type: ignore[assignment]
def tearDown(self) -> None: def tearDown(self) -> None:
supervise.bot_bottle_root = self.original_root # type: ignore[assignment] self._restore()
self.tmp.cleanup() self.tmp.cleanup()
+3 -4
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, call, patch
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle.backend import Bottle, BottleSpec, ExecResult from bot_bottle.backend import Bottle, BottleSpec, ExecResult
from bot_bottle.backend.docker import DockerBottleBackend from bot_bottle.backend.docker import DockerBottleBackend
from bot_bottle.backend.firecracker import FirecrackerBottleBackend from bot_bottle.backend.firecracker import FirecrackerBottleBackend
@@ -55,11 +55,10 @@ class _FakeStateMixin:
self.tmp_dir = tempfile.TemporaryDirectory(prefix="backend-workspace.") self.tmp_dir = tempfile.TemporaryDirectory(prefix="backend-workspace.")
self.tmp = Path(self.tmp_dir.name) self.tmp = Path(self.tmp_dir.name)
self.root = self.tmp / ".bot-bottle" self.root = self.tmp / ".bot-bottle"
self.original_root = supervise.bot_bottle_root self._restore = use_bottle_root(self.root)
supervise.bot_bottle_root = lambda: self.root # type: ignore[assignment]
def tearDown(self) -> None: def tearDown(self) -> None:
supervise.bot_bottle_root = self.original_root # type: ignore[assignment] self._restore()
self.tmp_dir.cleanup() self.tmp_dir.cleanup()
+2 -8
View File
@@ -6,7 +6,7 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle.bottle_state import ( from bot_bottle.bottle_state import (
BottleMetadata, BottleMetadata,
@@ -18,13 +18,7 @@ from bot_bottle.bottle_state import (
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="bottle-state-test.") self._tmp = tempfile.TemporaryDirectory(prefix="bottle-state-test.")
original = supervise.bot_bottle_root self._restore = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
def _teardown_fake_home(self): def _teardown_fake_home(self):
self._restore() self._restore()
+2 -8
View File
@@ -8,7 +8,7 @@ from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from bot_bottle.cli.commit import cmd_commit from bot_bottle.cli.commit import cmd_commit
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle.backend.freeze import CommitCancelled from bot_bottle.backend.freeze import CommitCancelled
@@ -16,13 +16,7 @@ from bot_bottle.backend.freeze import CommitCancelled
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="cli-commit-test.") self._tmp = tempfile.TemporaryDirectory(prefix="cli-commit-test.")
original = supervise.bot_bottle_root self._restore = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
def _teardown_fake_home(self): def _teardown_fake_home(self):
self._restore() self._restore()
+3 -8
View File
@@ -8,7 +8,7 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle.cli import start as start_mod from bot_bottle.cli import start as start_mod
@@ -16,15 +16,10 @@ from bot_bottle.cli import start as start_mod
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="cli-start-settle.") self._tmp = tempfile.TemporaryDirectory(prefix="cli-start-settle.")
self._original = supervise.bot_bottle_root self._restore = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
def _teardown_fake_home(self): def _teardown_fake_home(self):
supervise.bot_bottle_root = self._original # type: ignore[assignment] self._restore()
self._tmp.cleanup() self._tmp.cleanup()
+2 -8
View File
@@ -15,7 +15,7 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs
@@ -23,13 +23,7 @@ from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self) -> None: def _setup_fake_home(self) -> None:
self._tmp = tempfile.TemporaryDirectory(prefix="docker-cleanup-test.") self._tmp = tempfile.TemporaryDirectory(prefix="docker-cleanup-test.")
original = supervise.bot_bottle_root self._restore = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
def _teardown_fake_home(self) -> None: def _teardown_fake_home(self) -> None:
self._restore() self._restore()
+2 -8
View File
@@ -23,7 +23,7 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle import bottle_state from bot_bottle import bottle_state
from bot_bottle.backend.docker import enumerate as _enumerate from bot_bottle.backend.docker import enumerate as _enumerate
@@ -75,13 +75,7 @@ class TestParseServicesByProject(unittest.TestCase):
class _FakeHomeMixin: class _FakeHomeMixin:
def _setup_fake_home(self) -> None: def _setup_fake_home(self) -> None:
self._tmp = tempfile.TemporaryDirectory(prefix="enum-active.") self._tmp = tempfile.TemporaryDirectory(prefix="enum-active.")
original = supervise.bot_bottle_root self._restore_home = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original)
def _teardown_fake_home(self) -> None: def _teardown_fake_home(self) -> None:
self._restore_home() self._restore_home()
+2 -8
View File
@@ -8,7 +8,7 @@ from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle.backend.egress_apply import EgressApplyError from bot_bottle.backend.egress_apply import EgressApplyError
from bot_bottle.backend.docker.egress_apply import applicator from bot_bottle.backend.docker.egress_apply import applicator
@@ -67,14 +67,8 @@ class TestValidateRoutesContent(unittest.TestCase):
class TestApplyRoutesChange(unittest.TestCase): class TestApplyRoutesChange(unittest.TestCase):
def setUp(self): def setUp(self):
self._tmp = tempfile.TemporaryDirectory(prefix="egress-apply-test.") self._tmp = tempfile.TemporaryDirectory(prefix="egress-apply-test.")
original = supervise.bot_bottle_root
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
self.addCleanup(lambda: setattr(supervise, "bot_bottle_root", original))
self.addCleanup(self._tmp.cleanup) self.addCleanup(self._tmp.cleanup)
self.addCleanup(use_bottle_root(Path(self._tmp.name) / ".bot-bottle"))
def test_writes_live_routes_and_signals_reload(self): def test_writes_live_routes_and_signals_reload(self):
calls: list[list[str]] = [] calls: list[list[str]] = []
+4 -43
View File
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from bot_bottle import supervise from bot_bottle import supervise
from bot_bottle import supervise_types as sv_types from tests.unit import use_bottle_root
from bot_bottle.audit_store import AuditStore from bot_bottle.audit_store import AuditStore
from bot_bottle.queue_store import QueueStore from bot_bottle.queue_store import QueueStore
from bot_bottle.supervise import ( from bot_bottle.supervise import (
@@ -123,20 +123,7 @@ class TestQueueIO(unittest.TestCase):
self._tmp.cleanup() self._tmp.cleanup()
def _patch_home(self, fake_home: Path): def _patch_home(self, fake_home: Path):
original_sv = supervise.bot_bottle_root return use_bottle_root(fake_home / ".bot-bottle")
original_svt = sv_types.bot_bottle_root
def fake_root() -> Path:
return fake_home / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
sv_types.bot_bottle_root = fake_root # type: ignore[assignment]
def restore() -> None:
supervise.bot_bottle_root = original_sv # type: ignore[assignment]
sv_types.bot_bottle_root = original_svt # type: ignore[assignment]
return restore
def test_write_and_read_proposal(self): def test_write_and_read_proposal(self):
p = _proposal() p = _proposal()
@@ -240,20 +227,7 @@ class TestAuditLog(unittest.TestCase):
self._tmp.cleanup() self._tmp.cleanup()
def _patch_home(self, fake_home: Path): def _patch_home(self, fake_home: Path):
original_sv = supervise.bot_bottle_root return use_bottle_root(fake_home / ".bot-bottle")
original_svt = sv_types.bot_bottle_root
def fake_root() -> Path:
return fake_home / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
sv_types.bot_bottle_root = fake_root # type: ignore[assignment]
def restore() -> None:
supervise.bot_bottle_root = original_sv # type: ignore[assignment]
sv_types.bot_bottle_root = original_svt # type: ignore[assignment]
return restore
def test_write_then_read_single_entry(self): def test_write_then_read_single_entry(self):
e = AuditEntry( e = AuditEntry(
@@ -400,20 +374,7 @@ class TestSupervisePrepare(unittest.TestCase):
self._tmp.cleanup() self._tmp.cleanup()
def _patch_home(self, fake_home: Path): def _patch_home(self, fake_home: Path):
original_sv = supervise.bot_bottle_root return use_bottle_root(fake_home / ".bot-bottle")
original_svt = sv_types.bot_bottle_root
def fake_root() -> Path:
return fake_home / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
sv_types.bot_bottle_root = fake_root # type: ignore[assignment]
def restore() -> None:
supervise.bot_bottle_root = original_sv # type: ignore[assignment]
sv_types.bot_bottle_root = original_svt # type: ignore[assignment]
return restore
def test_prepare_creates_queue(self): def test_prepare_creates_queue(self):
plan = _StubSupervise().prepare("dev", self.stage_dir) plan = _StubSupervise().prepare("dev", self.stage_dir)
+2 -15
View File
@@ -12,7 +12,7 @@ from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from bot_bottle import supervise from bot_bottle import supervise
from bot_bottle import supervise_types as sv_types from tests.unit import use_bottle_root
from bot_bottle.audit_store import AuditStore from bot_bottle.audit_store import AuditStore
from bot_bottle.cli import supervise as supervise_cli from bot_bottle.cli import supervise as supervise_cli
from bot_bottle.queue_store import QueueStore from bot_bottle.queue_store import QueueStore
@@ -55,20 +55,7 @@ class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="supervise-test.") self._tmp = tempfile.TemporaryDirectory(prefix="supervise-test.")
original_sv = supervise.bot_bottle_root self._restore_home = use_bottle_root(Path(self._tmp.name) / ".bot-bottle")
original_svt = sv_types.bot_bottle_root
def fake_root() -> Path:
return Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
sv_types.bot_bottle_root = fake_root # type: ignore[assignment]
def restore() -> None:
supervise.bot_bottle_root = original_sv # type: ignore[assignment]
sv_types.bot_bottle_root = original_svt # type: ignore[assignment]
self._restore_home = restore
QueueStore("").migrate() QueueStore("").migrate()
AuditStore().migrate() AuditStore().migrate()
@@ -11,12 +11,13 @@ from __future__ import annotations
import contextlib import contextlib
import io import io
import os
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
from bot_bottle import supervise from tests.unit import use_bottle_root
from bot_bottle.cli import supervise as supervise_cli from bot_bottle.cli import supervise as supervise_cli
from bot_bottle.log import Die, die from bot_bottle.log import Die, die
@@ -45,12 +46,11 @@ class _FakeHomeMixin:
def _setup_fake_home(self): def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="supervise-crash-test.") self._tmp = tempfile.TemporaryDirectory(prefix="supervise-crash-test.")
self._orig_root = supervise.bot_bottle_root
self._root = Path(self._tmp.name) / ".bot-bottle" self._root = Path(self._tmp.name) / ".bot-bottle"
supervise.bot_bottle_root = lambda: self._root # type: ignore[assignment] self._restore_root = use_bottle_root(self._root)
def _teardown_fake_home(self): def _teardown_fake_home(self):
supervise.bot_bottle_root = self._orig_root # type: ignore[assignment] self._restore_root()
self._tmp.cleanup() self._tmp.cleanup()
@@ -127,11 +127,11 @@ class TestWriteCrashLog(_FakeHomeMixin, unittest.TestCase):
# OSError and the helper must fall back to a tempfile. # OSError and the helper must fall back to a tempfile.
bad = Path(self._tmp.name) / "not-a-dir" bad = Path(self._tmp.name) / "not-a-dir"
bad.write_text("x") bad.write_text("x")
supervise.bot_bottle_root = lambda: bad # type: ignore[assignment] with mock.patch.dict(os.environ, {"BOT_BOTTLE_ROOT": str(bad)}):
try: try:
raise RuntimeError("explode2") raise RuntimeError("explode2")
except RuntimeError as e: except RuntimeError as e:
path = supervise_cli._write_crash_log(e) path = supervise_cli._write_crash_log(e)
self.assertTrue(path.exists()) self.assertTrue(path.exists())
self.assertIn("explode2", path.read_text()) self.assertIn("explode2", path.read_text())
+3 -19
View File
@@ -10,6 +10,8 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from tests.unit import use_bottle_root
# The server module loads `supervise` via same-directory import inside # The server module loads `supervise` via same-directory import inside
# the container (Dockerfile.supervise WORKDIRs into /app). For tests # the container (Dockerfile.supervise WORKDIRs into /app). For tests
@@ -19,10 +21,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "bot_bott
import supervise as _sv # noqa: E402 # type: ignore import supervise as _sv # noqa: E402 # type: ignore
import queue_store as _qs # noqa: E402 # type: ignore import queue_store as _qs # noqa: E402 # type: ignore
import audit_store as _as # noqa: E402 # type: ignore import audit_store as _as # noqa: E402 # type: ignore
import supervise_types as svt_flat # noqa: E402 # type: ignore
from bot_bottle import supervise_server # noqa: E402 from bot_bottle import supervise_server # noqa: E402
from bot_bottle import supervise_types as svt_pkg # noqa: E402
from bot_bottle.supervise_server import ( from bot_bottle.supervise_server import (
ERR_INTERNAL, ERR_INTERNAL,
ERR_INVALID_PARAMS, ERR_INVALID_PARAMS,
@@ -279,23 +279,7 @@ class TestHandleToolsCall(unittest.TestCase):
self._tmp.cleanup() self._tmp.cleanup()
def _patch_home(self, fake_home: Path): def _patch_home(self, fake_home: Path):
original_sv = _sv.bot_bottle_root return use_bottle_root(fake_home / ".bot-bottle")
original_flat = svt_flat.bot_bottle_root
original_pkg = svt_pkg.bot_bottle_root
def fake_root() -> Path:
return fake_home / ".bot-bottle"
_sv.bot_bottle_root = fake_root # type: ignore[assignment]
svt_flat.bot_bottle_root = fake_root # type: ignore[assignment]
svt_pkg.bot_bottle_root = fake_root # type: ignore[assignment]
def restore() -> None:
_sv.bot_bottle_root = original_sv # type: ignore[assignment]
svt_flat.bot_bottle_root = original_flat # type: ignore[assignment]
svt_pkg.bot_bottle_root = original_pkg # type: ignore[assignment]
return restore
def _respond_when_proposal_appears(self, status: str, notes: str = "") -> threading.Thread: def _respond_when_proposal_appears(self, status: str, notes: str = "") -> threading.Thread:
"""Background thread: poll the queue for a fresh proposal, write a """Background thread: poll the queue for a fresh proposal, write a