refactor(backend): has_backend() helper + docker/enumerate split + ActiveAgent rename
test / unit (pull_request) Successful in 28s
test / integration (pull_request) Successful in 42s

Addresses PR #78 review feedback:

- New `has_backend(name)` on the backend package + abstract
  `BottleBackend.is_available()` on each concrete subclass.
  Replaces inline `shutil.which("docker") is None` checks in
  docker/cleanup.py:178 and smolmachines/enumerate.py:73.
  Docker → `shutil.which("docker") is not None`; smolmachines
  → `smolvm.is_available()`. Cross-backend `enumerate_active_
  agents()` skips backends whose `is_available()` is False so a
  docker-only host doesn't fail when iterating past
  smolmachines (and vice versa).
- Move docker `enumerate_active` + parser helpers out of
  cleanup.py into a new `backend/docker/enumerate.py`, mirroring
  the smolmachines/enumerate.py layout. cleanup.py is now
  purely about prepare_cleanup / cleanup; the active-listing
  concern owns its own file.
- Drop the `ActiveAgent = ActiveBottle` alias in dashboard.py.
  The canonical name is `ActiveAgent` (the thing running inside
  a bottle is always called "agent" in this codebase; the bottle
  is the container). Renamed `enumerate_active_bottles` →
  `enumerate_active_agents` to match.

Tests:
- `test_backend_selection.TestEnumerateActiveAgents
  .test_skips_unavailable_backends` locks down the
  `is_available()`-gated iteration.
- New `TestHasBackend` covers `has_backend("docker")` consulting
  the backend's `is_available`, and unknown-name → False.
- Existing tests follow the rename; the docker-availability-
  side-effect test in `test_docker_enumerate_active` moves up
  to the cross-backend layer (where the gate lives now).

607 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 19:03:16 -04:00
parent adff1263d8
commit 3b418580a9
11 changed files with 295 additions and 176 deletions
+19 -7
View File
@@ -1,10 +1,11 @@
"""DockerBottleBackend — the Docker implementation of BottleBackend.
This module is a thin façade. The real work lives in three siblings:
This module is a thin façade. The real work lives in four siblings:
- prepare.py — host-side resolution into a DockerBottlePlan
- launch.py — bring-up + teardown context manager
- cleanup.py — orphan enumeration, removal, and active listing
- prepare.py — host-side resolution into a DockerBottlePlan
- launch.py — bring-up + teardown context manager
- cleanup.py — orphan enumeration + removal
- enumerate.py — active-agent listing
The base class's `prepare` template runs cross-backend host-side
validation before calling `_resolve_plan` here.
@@ -12,12 +13,14 @@ validation before calling `_resolve_plan` here.
from __future__ import annotations
import shutil
from contextlib import contextmanager
from pathlib import Path
from typing import Generator, Sequence
from .. import ActiveBottle, BottleBackend, BottleSpec
from .. import ActiveAgent, BottleBackend, BottleSpec
from . import cleanup as _cleanup
from . import enumerate as _enumerate
from . import launch as _launch
from . import prepare as _prepare
from .bottle import DockerBottle
@@ -36,6 +39,15 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
name = "docker"
@classmethod
def is_available(cls) -> bool:
"""`docker` on PATH is sufficient; we don't probe `docker info`
eagerly because the cross-backend enumerator runs this on
every `list active` and we'd pay a subprocess per call. A
broken daemon will surface its own error during prepare /
launch."""
return shutil.which("docker") is not None
def _resolve_plan(self, spec: BottleSpec, *, stage_dir: Path) -> DockerBottlePlan:
return _prepare.resolve_plan(spec, stage_dir=stage_dir)
@@ -65,5 +77,5 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
def cleanup(self, plan: DockerBottleCleanupPlan) -> None:
_cleanup.cleanup(plan)
def enumerate_active(self) -> Sequence[ActiveBottle]:
return _cleanup.enumerate_active()
def enumerate_active(self) -> Sequence[ActiveAgent]:
return _enumerate.enumerate_active()