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:
@@ -152,6 +152,65 @@ def test_swarm_check_json_output(http_stub) -> None:
|
||||
assert '"decky01"' in result.output
|
||||
|
||||
|
||||
# ------------------------------------------------------------- swarm deckies
|
||||
|
||||
|
||||
def test_swarm_deckies_empty(http_stub) -> None:
|
||||
http_stub.script[("GET", "/swarm/deckies")] = _FakeResp([])
|
||||
result = runner.invoke(app, ["swarm", "deckies"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "No deckies" in result.output
|
||||
|
||||
|
||||
def test_swarm_deckies_renders_table(http_stub) -> None:
|
||||
http_stub.script[("GET", "/swarm/deckies")] = _FakeResp([
|
||||
{"decky_name": "decky-01", "host_uuid": "u-1", "host_name": "w1",
|
||||
"host_address": "10.0.0.1", "host_status": "active",
|
||||
"services": ["ssh"], "state": "running", "last_error": None,
|
||||
"compose_hash": None, "updated_at": "2026-04-18T00:00:00Z"},
|
||||
{"decky_name": "decky-02", "host_uuid": "u-2", "host_name": "w2",
|
||||
"host_address": "10.0.0.2", "host_status": "active",
|
||||
"services": ["smb", "ssh"], "state": "failed", "last_error": "boom",
|
||||
"compose_hash": None, "updated_at": "2026-04-18T00:00:00Z"},
|
||||
])
|
||||
result = runner.invoke(app, ["swarm", "deckies"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "decky-01" in result.output
|
||||
assert "decky-02" in result.output
|
||||
assert "w1" in result.output and "w2" in result.output
|
||||
assert "smb,ssh" in result.output
|
||||
|
||||
|
||||
def test_swarm_deckies_json_output(http_stub) -> None:
|
||||
http_stub.script[("GET", "/swarm/deckies")] = _FakeResp([
|
||||
{"decky_name": "decky-01", "host_uuid": "u-1", "host_name": "w1",
|
||||
"host_address": "10.0.0.1", "host_status": "active",
|
||||
"services": ["ssh"], "state": "running", "last_error": None,
|
||||
"compose_hash": None, "updated_at": "2026-04-18T00:00:00Z"},
|
||||
])
|
||||
result = runner.invoke(app, ["swarm", "deckies", "--json"])
|
||||
assert result.exit_code == 0
|
||||
assert '"decky_name"' in result.output
|
||||
assert '"decky-01"' in result.output
|
||||
|
||||
|
||||
def test_swarm_deckies_filter_by_host_name_looks_up_uuid(http_stub) -> None:
|
||||
http_stub.script[("GET", "/swarm/hosts")] = _FakeResp([
|
||||
{"uuid": "u-x", "name": "w1"},
|
||||
])
|
||||
http_stub.script[("GET", "/swarm/deckies?host_uuid=u-x")] = _FakeResp([])
|
||||
result = runner.invoke(app, ["swarm", "deckies", "--host", "w1"])
|
||||
assert result.exit_code == 0
|
||||
assert http_stub[-1][1].endswith("/swarm/deckies?host_uuid=u-x")
|
||||
|
||||
|
||||
def test_swarm_deckies_filter_by_state(http_stub) -> None:
|
||||
http_stub.script[("GET", "/swarm/deckies?state=failed")] = _FakeResp([])
|
||||
result = runner.invoke(app, ["swarm", "deckies", "--state", "failed"])
|
||||
assert result.exit_code == 0
|
||||
assert http_stub[-1][1].endswith("/swarm/deckies?state=failed")
|
||||
|
||||
|
||||
# ------------------------------------------------------------- swarm decommission
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user