From eff293115d1f6017e3f82e31ce746009fca55382 Mon Sep 17 00:00:00 2001 From: Kennedy Umege Date: Wed, 8 Jul 2026 12:15:56 +0100 Subject: [PATCH] fix(gateway): report runtime source version in /health, not stale dist-info metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /health and /health/detailed resolve the version via importlib.metadata first, falling back to hermes_cli.__version__. On editable/source checkouts — including the standard git-based install that hermes-setup performs — hermes_agent-*.dist-info can survive a source update unchanged, so the health endpoints keep reporting the previous release even though the running code (CLI, dashboard, release tags) is newer. Stale metadata does not raise, so the source fallback never fires. Flip the preference: use hermes_cli.__version__ (the runtime source of truth shared by the CLI and dashboard) first, and fall back to distribution metadata only when the source import fails. The never-raise contract of the version probe is unchanged. Observed live: CLI, dashboard, and pyproject all reported 0.18.2 while /health returned 0.18.0 from a stale hermes_agent-0.18.0.dist-info left behind by a source update. Co-Authored-By: Claude Fable 5 --- gateway/platforms/api_server.py | 24 +++++++++++++----------- tests/gateway/test_api_server.py | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 973ba5507379..e0629d270d90 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -96,23 +96,25 @@ logger = logging.getLogger(__name__) def _hermes_version() -> str: - """Return the hermes-agent version string, or "dev" if it can't be resolved. + """Return the canonical Hermes Agent version string. - Tries the installed package metadata first (authoritative for a pip/uv - install), then the in-tree ``hermes_cli.__version__`` (covers editable / - source checkouts where metadata may be stale or absent). Never raises — - a version probe must not be able to break the health endpoint. + ``hermes_cli.__version__`` is the runtime source of truth used by the CLI, + dashboard, portal tags, and release script. Prefer it over installed + distribution metadata because editable/source checkouts can retain stale + ``hermes_agent-*.dist-info`` after a source update until the environment is + reinstalled. Never raises — a version probe must not be able to break the + health endpoint. """ - try: - from importlib.metadata import version - - return version("hermes-agent") - except Exception: - pass try: from hermes_cli import __version__ return __version__ + except Exception: + pass + try: + from importlib.metadata import version + + return version("hermes-agent") except Exception: return "dev" diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 56734698019a..7700cec107d0 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -30,6 +30,7 @@ from gateway.platforms.api_server import ( ResponseStore, _IdempotencyCache, _derive_chat_session_id, + _hermes_version, _redact_api_error_text, check_api_server_requirements, cors_middleware, @@ -776,6 +777,29 @@ class TestHealthEndpoint: assert isinstance(data["version"], str) assert data["version"] != "" + def test_health_version_prefers_runtime_source_over_stale_metadata(self): + """Editable installs can leave importlib.metadata at an older release. + + The health endpoint must report the running Hermes source version, not + stale ``hermes_agent-*.dist-info`` metadata from before a source update. + """ + from hermes_cli import __version__ + + with patch("importlib.metadata.version", return_value="0.18.0"): + assert _hermes_version() == __version__ + + @pytest.mark.asyncio + async def test_health_endpoint_prefers_runtime_version_over_stale_metadata(self, adapter): + from hermes_cli import __version__ + + app = _create_app(adapter) + with patch("importlib.metadata.version", return_value="0.18.0"): + async with TestClient(TestServer(app)) as cli: + resp = await cli.get("/health") + assert resp.status == 200 + data = await resp.json() + assert data["version"] == __version__ + @pytest.mark.asyncio async def test_v1_health_alias_returns_ok(self, adapter): """GET /v1/health should return the same response as /health."""