import json from pathlib import Path import subprocess from hermes_cli.version_info import ( VersionInfo, _derived_version, _reset_version_info_cache, _resolve_stamp_file, _stamp_version_info, get_version_info, ) def setup_function(): _reset_version_info_cache() def test_derived_version_shows_plus_question_for_dirty_unknown_distance(): assert _derived_version("0.19.0", None, dirty=True) == "0.19.0+?" assert _derived_version("0.19.0", None, dirty=False) == "0.19.0" assert _derived_version("0.19.0", 5, dirty=True) == "0.19.0+5" assert _derived_version("0.19.0", 0, dirty=True) == "0.19.0" def test_display_version_names_the_distance_without_the_commit(): """Labels say how far past the release an install is; the commit is shown beside them where there is room (`hermes --version` keeps `.g`).""" ahead = VersionInfo("0.21.5", "0.21.5+1913.gf83a9e9", 1913, "f83a9e9" + "0" * 33, "main", "git") assert ahead.display_version == "0.21.5+1913" assert VersionInfo("0.21.5", "0.21.5", 0, None, None, "git").display_version == "0.21.5" def test_display_version_keeps_tagless_stamp_identity(tmp_path, monkeypatch): stamp = { "commit": "a" * 40, "baseVersion": "unknown", "displayVersion": "git.aaaaaaa", "source": "git", "payload": "bootstrap", "updateMechanism": "self", } stamp_file = tmp_path / "install-stamp.json" stamp_file.write_text(json.dumps(stamp), encoding="utf-8") monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file) info = get_version_info() assert info.base_version == "unknown" assert info.display_version == stamp["displayVersion"] assert info.commit == stamp["commit"] def test_display_version_keeps_unknown_when_no_identity(): info = VersionInfo("unknown", "unknown", None, None, None, "unknown") assert info.display_version == "unknown" def test_stamp_version_info_reads_nix_stamp(tmp_path, monkeypatch): stamp = { "schemaVersion": 2, "commit": "a" * 40, "branch": "feature/version", "baseVersion": "0.19.0", "displayVersion": "0.19.0+3", "distance": 3, "dirty": False, "source": "nix", "distribution": "nix", "updateMechanism": "external", } stamp_file = tmp_path / "install-stamp.json" stamp_file.write_text(json.dumps(stamp)) monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file) info = get_version_info() assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "a" * 40, "feature/version", "nix", distribution="nix") def test_stamp_version_info_preserves_ci_provenance_and_docker_distribution(tmp_path, monkeypatch): stamp = {"commit": "d" * 40, "source": "ci", "distribution": "docker", "updateMechanism": "external"} stamp_file = tmp_path / "install-stamp.json" stamp_file.write_text(json.dumps(stamp)) monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file) info = get_version_info() assert info.source == "ci" assert info.distribution == "docker" def test_stamp_version_info_preserves_missing_branch(tmp_path, monkeypatch): stamp = { "schemaVersion": 2, "commit": "b" * 40, "branch": None, "baseVersion": "0.19.0", "displayVersion": "0.19.0+?", "distance": None, "dirty": True, "source": "docker", "updateMechanism": "external", } stamp_file = tmp_path / "install-stamp.json" stamp_file.write_text(json.dumps(stamp)) monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file) info = get_version_info() assert info == VersionInfo("0.19.0", "0.19.0+?", None, "b" * 40, None, "docker", True) def test_stamp_version_info_ignores_fallback_commit(tmp_path, monkeypatch): """All-zero commit means the stamp has no real SHA — skip it.""" stamp = { "schemaVersion": 2, "commit": "0" * 40, "branch": "main", "source": "fallback", "updateMechanism": "self", } stamp_file = tmp_path / "install-stamp.json" stamp_file.write_text(json.dumps(stamp)) monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file) monkeypatch.setattr("hermes_cli.version_info._resolve_repo_dir", lambda: None) info = get_version_info() assert info.source == "unknown" assert info.commit is None def test_stamp_version_info_returns_none_when_file_missing(tmp_path, monkeypatch): monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: None) assert _stamp_version_info() is None def test_get_version_info_unknown_when_no_stamp_and_no_git(monkeypatch): monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: None) monkeypatch.setattr("hermes_cli.version_info._resolve_repo_dir", lambda: None) info = get_version_info() assert info.base_version == "unknown" assert info.derived_version == "unknown" assert info.distance is None assert info.commit is None assert info.source == "unknown" def test_get_version_info_derives_identity_from_reachable_release_tag(tmp_path, monkeypatch): repo = tmp_path / "repo" repo.mkdir() def git(*args: str) -> str: result = subprocess.run( ["git", *args], cwd=repo, text=True, capture_output=True, check=True, env={"HOME": str(tmp_path), "PATH": __import__("os").environ["PATH"]}, ) return result.stdout.strip() git("init", "-q") git("config", "user.name", "Hermes Test") git("config", "user.email", "hermes@example.invalid") (repo / "tracked").write_text("release\n", encoding="utf-8") git("add", "tracked") git("commit", "-qm", "release") git("tag", "v0.21.4") git("tag", "v2026.9.21") (repo / "tracked").write_text("next\n", encoding="utf-8") git("commit", "-qam", "next") monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: None) monkeypatch.setattr("hermes_cli.version_info._resolve_repo_dir", lambda: repo) info = get_version_info() assert info.base_version == "0.21.4" assert info.derived_version == f"0.21.4+1.g{git('rev-parse', '--short=7', 'HEAD')}" assert info.distance == 1 assert info.commit == git("rev-parse", "HEAD") assert info.source == "git" def test_get_version_info_takes_the_version_a_calver_only_release_shipped(tmp_path, monkeypatch): """Releases tagged only vYYYY.M.D resolve to their pyproject version, not "unknown".""" repo = tmp_path / "repo" repo.mkdir() def git(*args: str) -> str: result = subprocess.run( ["git", *args], cwd=repo, text=True, capture_output=True, check=True, env={"HOME": str(tmp_path), "PATH": __import__("os").environ["PATH"]}, ) return result.stdout.strip() git("init", "-q") git("config", "user.name", "Hermes Test") git("config", "user.email", "hermes@example.invalid") (repo / "pyproject.toml").write_text('[project]\nname = "hermes-agent"\nversion = "0.21.4"\n', encoding="utf-8") git("add", "pyproject.toml") git("commit", "-qm", "release") git("tag", "v2026.9.21") (repo / "pyproject.toml").write_text('[project]\nname = "hermes-agent"\nversion = "0.0.0"\n', encoding="utf-8") git("commit", "-qam", "next") monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: None) monkeypatch.setattr("hermes_cli.version_info._resolve_repo_dir", lambda: repo) info = get_version_info() assert info.base_version == "0.21.4" assert info.distance == 1 assert info.derived_version == f"0.21.4+1.g{git('rev-parse', '--short=7', 'HEAD')}" def test_resolve_stamp_file_honors_install_root(tmp_path, monkeypatch): """Sealed installs (the Nix wrapper) point HERMES_INSTALL_ROOT at the stamp dir.""" stamp = {"commit": "e" * 40, "source": "nix", "distribution": "nix", "updateMechanism": "external"} (tmp_path / "install-stamp.json").write_text(json.dumps(stamp)) monkeypatch.setenv("HERMES_INSTALL_ROOT", str(tmp_path)) assert _resolve_stamp_file() == tmp_path / "install-stamp.json" info = get_version_info() assert info.commit == "e" * 40 assert info.source == "nix" assert info.distribution == "nix" def test_resolve_stamp_file_install_root_without_stamp_is_none(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_INSTALL_ROOT", str(tmp_path)) assert _resolve_stamp_file() is None def test_resolve_stamp_file_falls_back_to_code_root_when_env_unset(tmp_path, monkeypatch): monkeypatch.delenv("HERMES_INSTALL_ROOT", raising=False) stamp = {"commit": "f" * 40, "source": "docker", "updateMechanism": "external"} (tmp_path / "install-stamp.json").write_text(json.dumps(stamp)) monkeypatch.setattr("pm.paths.repo_root", lambda: tmp_path) assert _resolve_stamp_file() == tmp_path / "install-stamp.json" def test_old_updater_version_stub_reads_the_same_stamp_as_version_info(tmp_path): """``hermes_cli.__version__`` exists only for shipped updaters that import it after the checkout swap (tests/compat/old_updater_surface.json). It must report the stamp's base version exactly as get_version_info() does, and the pre-stamp placeholder without one. A fresh interpreter, since the stub is evaluated when the package is imported.""" import os import sys repo = Path(__file__).resolve().parents[2] probe = ( f"import sys; sys.path.insert(0, {str(repo)!r}); import hermes_cli; " "from hermes_cli.version_info import get_version_info; " "print(hermes_cli.__version__, get_version_info().base_version)" ) def read(install_root: Path) -> list[str]: env = {**os.environ, "HERMES_INSTALL_ROOT": str(install_root)} return subprocess.run( [sys.executable, "-c", probe], env=env, capture_output=True, text=True, check=True ).stdout.split() stamped = tmp_path / "stamped" stamped.mkdir() stamp = {"commit": "c" * 40, "baseVersion": "9.8.7", "source": "ci", "updateMechanism": "external"} (stamped / "install-stamp.json").write_text(json.dumps(stamp)) compat, identity = read(stamped) assert compat == identity == stamp["baseVersion"] unstamped = tmp_path / "unstamped" unstamped.mkdir() assert read(unstamped)[0] == "0.0.0"