feat(swarm): add decnet swarm deckies to list deployed shards by host

`swarm list` only shows enrolled workers — there was no way to see which
deckies are running and where. Adds GET /swarm/deckies on the controller
(joins DeckyShard with SwarmHost for name/address/status) plus the CLI
wrapper with --host / --state filters and --json.
This commit is contained in:
2026-04-18 21:10:07 -04:00
parent 4db9c7464c
commit 8914c27220
6 changed files with 233 additions and 0 deletions

View File

@@ -287,6 +287,57 @@ def test_check_marks_hosts_active(client: TestClient, stub_agent) -> None:
assert one["last_heartbeat"] is not None
# ---------------------------------------------------------------- /deckies
def test_list_deckies_empty(client: TestClient) -> None:
resp = client.get("/swarm/deckies")
assert resp.status_code == 200
assert resp.json() == []
def test_list_deckies_joins_host_identity(client: TestClient, repo) -> None:
import asyncio
h1 = client.post(
"/swarm/enroll",
json={"name": "deck-host-1", "address": "10.0.0.11", "agent_port": 8765},
).json()
h2 = client.post(
"/swarm/enroll",
json={"name": "deck-host-2", "address": "10.0.0.12", "agent_port": 8765},
).json()
async def _seed() -> None:
await repo.upsert_decky_shard({
"decky_name": "decky-01", "host_uuid": h1["host_uuid"],
"services": ["ssh"], "state": "running",
})
await repo.upsert_decky_shard({
"decky_name": "decky-02", "host_uuid": h2["host_uuid"],
"services": ["smb", "ssh"], "state": "failed", "last_error": "boom",
})
asyncio.get_event_loop().run_until_complete(_seed())
rows = client.get("/swarm/deckies").json()
assert len(rows) == 2
by_name = {r["decky_name"]: r for r in rows}
assert by_name["decky-01"]["host_name"] == "deck-host-1"
assert by_name["decky-01"]["host_address"] == "10.0.0.11"
assert by_name["decky-01"]["state"] == "running"
assert by_name["decky-02"]["services"] == ["smb", "ssh"]
assert by_name["decky-02"]["last_error"] == "boom"
# host_uuid filter
only = client.get(f"/swarm/deckies?host_uuid={h1['host_uuid']}").json()
assert [r["decky_name"] for r in only] == ["decky-01"]
# state filter
failed = client.get("/swarm/deckies?state=failed").json()
assert [r["decky_name"] for r in failed] == ["decky-02"]
# ---------------------------------------------------------------- /health (root)