refactor(dashboard): discover via docker compose ls
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m8s

PRD 0018 chunk 5. The dashboard's operator-edit verbs
(`routes edit`, `pipelock edit`) enumerated running sidecars
via `docker ps --filter name=...` prefix scans. Switch to
`docker compose ls`-based discovery so the dashboard, cleanup
CLI, and launch step all agree on what's running.

Mechanics:

  - `claude_bottle/backend/docker/compose.py` grows three shared
    helpers: `list_compose_projects` (the JSON parse moved out
    of cleanup), `slug_from_compose_project` (inverse of
    `compose_project_name`), and `list_active_slugs` (sugar over
    the first two for the common "what's running?" question).
  - cleanup.py drops its private `_list_compose_projects` +
    `_PROJECT_PREFIX` in favor of the shared ones; `list_active`
    simplifies (one compose-ls call, not two).
  - dashboard.py's `_discover_sidecar_slugs` becomes
    `_discover_active_with_service`: cross-references the active
    slug list with a label-filtered `docker ps` so only bottles
    whose given service container is actually up surface in the
    edit menu. Bottles without an egress sidecar (no
    bottle.egress.routes) no longer appear for `routes edit`.

3 new unit tests cover the slug ↔ compose-project naming
contract; manual probe with a fake compose project confirms
both `discover_egress_slugs` and `discover_pipelock_slugs`
return the expected slug.
This commit is contained in:
2026-05-26 00:14:16 -04:00
parent 0ae544d2a6
commit 1fa3745832
4 changed files with 126 additions and 65 deletions
+28 -1
View File
@@ -14,7 +14,12 @@ from pathlib import Path
from claude_bottle.backend import BottleSpec
from claude_bottle.backend.docker.bottle_plan import DockerBottlePlan
from claude_bottle.backend.docker.compose import bottle_plan_to_compose
from claude_bottle.backend.docker.compose import (
COMPOSE_PROJECT_PREFIX,
bottle_plan_to_compose,
compose_project_name,
slug_from_compose_project,
)
from claude_bottle.egress import (
EgressPlan,
EgressRoute,
@@ -450,5 +455,27 @@ class TestFullMatrix(unittest.TestCase):
self.assertEqual(expected, set(s.keys()))
class TestProjectNaming(unittest.TestCase):
"""The slug ↔ compose-project mapping is the contract dashboard,
cleanup, and launch all rely on. Lock it down."""
def test_compose_project_name_is_prefix_plus_slug(self):
self.assertEqual(
f"{COMPOSE_PROJECT_PREFIX}myagent-abc12",
compose_project_name("myagent-abc12"),
)
def test_slug_from_compose_project_is_inverse(self):
self.assertEqual(
"myagent-abc12",
slug_from_compose_project(f"{COMPOSE_PROJECT_PREFIX}myagent-abc12"),
)
def test_slug_from_unrelated_project_returns_empty(self):
# Defends against `docker compose ls` including non-bottle
# projects on a host with other compose setups.
self.assertEqual("", slug_from_compose_project("other-project"))
if __name__ == "__main__":
unittest.main()