fix(update-check): stop reporting phantom "N commits behind" inside Docker (#39559)
Inside the published Docker image, both the `--tui` banner and the dashboard-embedded TUI report `1 commit behind — run docker pull nousresearch/hermes-agent:latest to update` even though the container has no git repo and no way to compute a commit delta. Root cause: two independent update-detection paths, only one of which knows it's running in Docker. - `recommended_update_command()` → `detect_install_method()` reads the `.install_method` stamp that `docker/stage2-hook.sh` writes at boot → returns "docker", so the *command string* correctly says `docker pull`. - `banner.check_for_updates()` (the source of the "N commits behind" *count*) has no notion of the docker install method. It only detects a build via `HERMES_REVISION` (nix-only, unset in the image) or a `.git` dir (excluded from the image by .dockerignore). Neither matches, so it silently falls through to `check_via_pypi()`, whose PyPI-version mismatch flag (1) is then rendered verbatim by the CLI banner (build_welcome_banner), the Ink TUI badge (branding.tsx), and `hermes version` as "1 commit behind" — a phantom count, no commit math involved. `hermes update` already refuses to run in-place in the container. The dashboard's REST `/api/hermes/update/check` endpoint already short-circuits docker (returns behind=None + the docker guidance). This mirrors that guard inside `check_for_updates()` so the banner/TUI/version surfaces agree: when `detect_install_method() == "docker"`, return None before any git/pypi probe (and before writing a cache entry). None makes the render guards (`typeof === 'number' && > 0`, `behind and behind > 0`) stay false, so the badge/line disappears entirely — matching the System page. Fix is in one place (check_for_updates) because all three consumers route through it via get_update_result()/_update_result. Tests: test_check_for_updates_docker_returns_none asserts None + no git/pypi probe + no cache write; test_check_for_updates_non_docker_still_checks guards against over-broadening (pip still version-checks). Mutation-tested: removing the guard fails the docker test. Verified against a real `docker build` of the image — see PR description.
This commit is contained in:
@ -225,6 +225,25 @@ def check_for_updates() -> Optional[int]:
|
||||
cache_file = hermes_home / ".update_check"
|
||||
embedded_rev = os.environ.get("HERMES_REVISION") or None
|
||||
|
||||
# Docker images have no working tree to count commits against — the
|
||||
# published image excludes `.git` (see .dockerignore) and sets no
|
||||
# HERMES_REVISION (that's nix-only). Without this guard the checks below
|
||||
# fall through to `check_via_pypi()`, whose PyPI-version mismatch flag (1)
|
||||
# then gets rendered by the CLI banner and the TUI badge as a phantom
|
||||
# "1 commit behind" — even though no git repo or commit math is involved,
|
||||
# and `hermes update` correctly refuses to run in-place inside the
|
||||
# container anyway. The dashboard's REST `/api/hermes/update/check`
|
||||
# endpoint already short-circuits docker the same way (web_server.py);
|
||||
# mirror that here so the banner/TUI surfaces agree. Returning None makes
|
||||
# both the Rich banner (build_welcome_banner) and the Ink badge
|
||||
# (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing.
|
||||
try:
|
||||
from hermes_cli.config import detect_install_method
|
||||
if detect_install_method() == "docker":
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Read cache — invalidate if the embedded rev OR installed version has
|
||||
# changed since the last check. The version guard matters for pip installs:
|
||||
# `check_via_pypi()` compares against VERSION, so a `pip install --upgrade`
|
||||
|
||||
@ -131,6 +131,62 @@ def test_check_for_updates_fallback_to_project_root(tmp_path, monkeypatch):
|
||||
assert mock_run.call_count >= 1
|
||||
|
||||
|
||||
def test_check_for_updates_docker_returns_none(tmp_path, monkeypatch):
|
||||
"""Inside the Docker image, check_for_updates() must short-circuit to None.
|
||||
|
||||
Regression: the published image excludes .git (.dockerignore) and sets no
|
||||
HERMES_REVISION (nix-only), so without a docker guard check_for_updates()
|
||||
falls through to check_via_pypi(), whose version-mismatch flag (1) gets
|
||||
rendered by both the Rich banner and the Ink TUI badge as a phantom
|
||||
"1 commit behind" — despite there being no git repo or commit math in the
|
||||
container, and `hermes update` correctly refusing to run there. The guard
|
||||
must return None (so the > 0 render guards stay false) AND not reach the
|
||||
git/pypi probes or write a cache entry.
|
||||
"""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
cache_file = tmp_path / ".update_check"
|
||||
|
||||
with patch("hermes_cli.config.detect_install_method", return_value="docker"), \
|
||||
patch("hermes_cli.banner.subprocess.run") as mock_run, \
|
||||
patch("hermes_cli.banner.check_via_pypi") as mock_pypi:
|
||||
result = banner.check_for_updates()
|
||||
|
||||
assert result is None
|
||||
# Neither the git probe nor the PyPI probe should have run.
|
||||
mock_run.assert_not_called()
|
||||
mock_pypi.assert_not_called()
|
||||
# And no phantom "behind" count should be cached for the next 6h.
|
||||
assert not cache_file.exists()
|
||||
|
||||
|
||||
def test_check_for_updates_non_docker_still_checks(tmp_path, monkeypatch):
|
||||
"""The docker guard must NOT over-broaden: a pip install still version-checks.
|
||||
|
||||
Invariant guarding against the guard firing for non-docker methods — pip
|
||||
installs legitimately reach check_via_pypi() and surface a real update.
|
||||
"""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
# No local git checkout -> the PyPI (pip-install) path is exercised.
|
||||
fake_banner = tmp_path / "hermes_cli" / "banner.py"
|
||||
fake_banner.parent.mkdir(parents=True, exist_ok=True)
|
||||
fake_banner.touch()
|
||||
monkeypatch.setattr(banner, "__file__", str(fake_banner))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.delenv("HERMES_REVISION", raising=False)
|
||||
|
||||
with patch("hermes_cli.config.detect_install_method", return_value="pip"), \
|
||||
patch("hermes_cli.banner.subprocess.run") as mock_run, \
|
||||
patch("hermes_cli.banner.check_via_pypi", return_value=1) as mock_pypi:
|
||||
result = banner.check_for_updates()
|
||||
|
||||
assert result == 1
|
||||
mock_pypi.assert_called_once()
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_prefetch_non_blocking():
|
||||
"""prefetch_update_check() should return immediately without blocking."""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
Reference in New Issue
Block a user