feat(dashboard): check-before-update flow on the System page (#38205)
The dashboard's update button ran 'hermes update' immediately with no preview. Now the System page shows whether an update is available and asks the user to confirm before applying it. - New GET /api/hermes/update/check: reports install method, current version, and commits-behind (via banner.check_for_updates, 6h-cached; ?force=1 busts the cache). Soft-fails to behind=null on network error; marks docker/nix/homebrew as can_apply=false with the out-of-band cmd. - System page: update-status badge on the Hermes version row (latest / N behind), a Check-for-updates button, and an Update-now button that opens a ConfirmDialog showing the commit count before POST /api/hermes/ update fires. Cached status loads with the rest of the page. - Docs + 5 endpoint tests (git/up-to-date/docker/soft-failure + auth gate).
This commit is contained in:
@@ -412,8 +412,89 @@ class TestAdminEndpointsAuthGate:
|
||||
"/api/curator",
|
||||
"/api/portal",
|
||||
"/api/system/stats",
|
||||
"/api/hermes/update/check",
|
||||
],
|
||||
)
|
||||
def test_gated(self, path):
|
||||
resp = self.client.get(path)
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestUpdateCheckEndpoint:
|
||||
"""``GET /api/hermes/update/check`` reports availability without applying.
|
||||
|
||||
Powers the dashboard's check-before-you-update flow: the System page
|
||||
shows the commit-behind count and asks the user to confirm before
|
||||
``POST /api/hermes/update`` runs ``hermes update``.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, _isolate_hermes_home):
|
||||
self.client, _ = _client()
|
||||
|
||||
def test_git_install_reports_behind_count(self, monkeypatch):
|
||||
import hermes_cli.web_server as ws
|
||||
|
||||
monkeypatch.setattr(ws, "detect_install_method", lambda *a, **k: "git")
|
||||
# Stub the shared checker so the contract is deterministic (no network).
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
monkeypatch.setattr(banner, "check_for_updates", lambda: 5)
|
||||
|
||||
r = self.client.get("/api/hermes/update/check")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert {
|
||||
"install_method",
|
||||
"current_version",
|
||||
"behind",
|
||||
"update_available",
|
||||
"can_apply",
|
||||
"update_command",
|
||||
"message",
|
||||
} <= set(body)
|
||||
assert body["install_method"] == "git"
|
||||
assert body["behind"] == 5
|
||||
assert body["update_available"] is True
|
||||
# git/pip installs can apply the update in place from the dashboard.
|
||||
assert body["can_apply"] is True
|
||||
|
||||
def test_up_to_date(self, monkeypatch):
|
||||
import hermes_cli.web_server as ws
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
monkeypatch.setattr(ws, "detect_install_method", lambda *a, **k: "git")
|
||||
monkeypatch.setattr(banner, "check_for_updates", lambda: 0)
|
||||
|
||||
body = self.client.get("/api/hermes/update/check").json()
|
||||
assert body["behind"] == 0
|
||||
assert body["update_available"] is False
|
||||
|
||||
def test_docker_is_not_applyable(self, monkeypatch):
|
||||
import hermes_cli.web_server as ws
|
||||
|
||||
monkeypatch.setattr(ws, "detect_install_method", lambda *a, **k: "docker")
|
||||
body = self.client.get("/api/hermes/update/check").json()
|
||||
# Docker images are immutable — the dashboard can't apply an update.
|
||||
assert body["can_apply"] is False
|
||||
assert body["message"]
|
||||
assert body["behind"] is None
|
||||
|
||||
def test_check_failure_is_soft(self, monkeypatch):
|
||||
import hermes_cli.web_server as ws
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
monkeypatch.setattr(ws, "detect_install_method", lambda *a, **k: "git")
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("offline")
|
||||
|
||||
monkeypatch.setattr(banner, "check_for_updates", _boom)
|
||||
# A failed check must not 500 — it returns behind=null with guidance.
|
||||
r = self.client.get("/api/hermes/update/check")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["behind"] is None
|
||||
assert body["update_available"] is False
|
||||
assert body["message"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user