refactor(backend): type enumeration failures
prd-number-check / require-numbered-prds (pull_request) Successful in 6s
lint / lint (push) Successful in 1m0s
test / coverage (pull_request) Blocked by required conditions
test / unit (pull_request) Successful in 59s
test / image-input-builds (pull_request) Successful in 1m8s
test / integration-docker (pull_request) Waiting to run
tracker-policy-pr / check-pr (pull_request) Failing after 13s

This commit is contained in:
2026-07-26 22:32:43 +00:00
parent 90097a50ee
commit 38d3f0fe0c
8 changed files with 42 additions and 20 deletions
+3
View File
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
BottleImages,
BottlePlan,
BottleSpec,
EnumerationError,
ExecResult,
)
from .selection import (
@@ -59,6 +60,7 @@ _LAZY_MODULES: dict[str, str] = {
"BottleImages": "base",
"BottleBackend": "base",
"BackendStatus": "base",
"EnumerationError": "base",
"get_bottle_backend": "selection",
"known_backend_names": "selection",
"has_backend": "selection",
@@ -100,6 +102,7 @@ __all__ = [
"BottlePlan",
"BottleSpec",
"ExecResult",
"EnumerationError",
"CommitCancelled",
"Freezer",
"get_freezer",
+4
View File
@@ -42,6 +42,10 @@ class BackendStatus(enum.IntEnum):
READY = 0
class EnumerationError(RuntimeError):
"""A backend could not produce an authoritative live-resource snapshot."""
@dataclass(frozen=True)
class BottleSpec:
"""CLI-supplied intent. Backend-agnostic — each backend's prepare
+6 -3
View File
@@ -16,6 +16,7 @@ from pathlib import Path
from typing import Any
from ...log import die, warn
from ..base import EnumerationError
# --- Lifecycle helpers (PRD 0018 chunk 3) ----------------------------------
@@ -75,12 +76,14 @@ def list_compose_projects(
)
except FileNotFoundError as exc:
if raise_on_error:
raise RuntimeError("docker compose ls failed: docker not found") from exc
raise EnumerationError(
"docker compose ls failed: docker not found"
) from exc
return []
if result.returncode != 0:
message = f"docker compose ls failed: {result.stderr.strip()}"
if raise_on_error:
raise RuntimeError(message)
raise EnumerationError(message)
if warn_on_error:
warn(message)
return []
@@ -89,7 +92,7 @@ def list_compose_projects(
except json.JSONDecodeError as e:
message = f"docker compose ls returned malformed JSON: {e}"
if raise_on_error:
raise RuntimeError(message) from e
raise EnumerationError(message) from e
if warn_on_error:
warn(message)
return []
+3 -3
View File
@@ -12,7 +12,7 @@ from __future__ import annotations
import subprocess
from .. import ActiveAgent
from .. import ActiveAgent, EnumerationError
from ...bottle_state import read_metadata
from .compose import compose_project_name, list_active_slugs
@@ -75,7 +75,7 @@ def _query_services_by_project() -> dict[str, set[str]]:
capture_output=True, text=True, check=False,
)
except FileNotFoundError as exc:
raise RuntimeError("docker ps failed: docker not found") from exc
raise EnumerationError("docker ps failed: docker not found") from exc
if r.returncode != 0:
raise RuntimeError(f"docker ps failed: {r.stderr.strip()}")
raise EnumerationError(f"docker ps failed: {r.stderr.strip()}")
return _parse_services_by_project(r.stdout or "")
+12 -11
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import subprocess
from ...bottle_state import read_metadata
from .. import ActiveAgent
from .. import ActiveAgent, EnumerationError
from .infra import INFRA_NAME, ORCHESTRATOR_NAME
# The name every agent container carries: `bot-bottle-<slug>`. Exported
@@ -20,17 +20,18 @@ CONTAINER_NAME_PREFIX = "bot-bottle-"
_INFRA_NAMES = frozenset({INFRA_NAME, ORCHESTRATOR_NAME})
class EnumerationError(RuntimeError):
"""container list failed; the resulting live set is not authoritative."""
def enumerate_active() -> list[ActiveAgent]:
result = subprocess.run(
["container", "list", "--quiet"],
capture_output=True,
text=True,
check=False,
)
try:
result = subprocess.run(
["container", "list", "--quiet"],
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError as exc:
raise EnumerationError(
"container list failed: container CLI not found"
) from exc
if result.returncode != 0:
raise EnumerationError(
f"container list failed: "
+2 -1
View File
@@ -18,6 +18,7 @@ from bot_bottle.backend.docker.compose import (
list_compose_projects,
slug_from_compose_project,
)
from bot_bottle.backend import EnumerationError
class TestProjectNaming(unittest.TestCase):
@@ -76,7 +77,7 @@ class TestComposeProjectListing(unittest.TestCase):
args=["docker"], returncode=1, stdout="", stderr="no daemon",
),
):
with self.assertRaisesRegex(RuntimeError, "no daemon"):
with self.assertRaisesRegex(EnumerationError, "no daemon"):
list_active_slugs(
warn_on_error=False,
raise_on_error=True,
+2 -1
View File
@@ -28,6 +28,7 @@ from unittest.mock import patch
from tests.unit import use_bottle_root
from bot_bottle import bottle_state
from bot_bottle.backend.docker import enumerate as _enumerate
from bot_bottle.backend import EnumerationError
class TestParseServicesByProject(unittest.TestCase):
@@ -82,7 +83,7 @@ class TestQueryServicesByProject(unittest.TestCase):
args=["docker"], returncode=1, stdout="", stderr="daemon down",
),
):
with self.assertRaisesRegex(RuntimeError, "daemon down"):
with self.assertRaisesRegex(EnumerationError, "daemon down"):
_enumerate._query_services_by_project()
+10 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import unittest
from unittest.mock import patch
from bot_bottle.backend import EnumerationError
from bot_bottle.backend.macos_container import cleanup, enumerate as enum_mod
from bot_bottle.backend.macos_container.bottle_cleanup_plan import (
MacosContainerBottleCleanupPlan,
@@ -69,10 +70,18 @@ class TestMacosContainerEnumerate(unittest.TestCase):
self.assertEqual(["dev-abc"], [a.slug for a in agents])
def test_raises_when_the_cli_fails(self):
from bot_bottle.backend.macos_container.enumerate import EnumerationError
with self.assertRaises(EnumerationError):
self._enumerate("", returncode=1)
def test_raises_typed_error_when_cli_is_missing(self):
with (
patch.object(
enum_mod.subprocess, "run", side_effect=FileNotFoundError,
),
self.assertRaisesRegex(EnumerationError, "CLI not found"),
):
enum_mod.enumerate_active()
if __name__ == "__main__":
unittest.main()