Files
hermes-agent/tests/hermes_cli/test_steward_detection.py
ethernet 01e34cda7b fix(steward): one install-stamp resolver; identity fallback uses the process home
steward.read_install_stamp read <root>/install-stamp.json literally while
version_info._resolve_stamp_file honoured HERMES_INSTALL_ROOT. A Nix package
bakes its stamp outside the store's package dir, so `hermes --version` said
nix while sealed_steward said "unknown" and uninstall printed "managed by
unknown". steward.install_stamp_path now owns the location: the executing
tree resolves through pm.paths.install_root, any other tree is literal.

version_info._resolve_repo_dir fell back to the context-local profile home
for a value cached process-wide; the running code's identity is a process
fact, so it now reads the process home.
2026-09-21 19:38:22 -04:00

170 lines
6.7 KiB
Python

"""Steward detection reads the install stamp beside the code, not markers.
``hermes_cli.steward`` decides whether a steward owns this tree (and
therefore whether ``hermes update`` and the uninstaller must refuse code
mutation). That is a fact about the INSTALL, so it comes from the two
facts the tree itself carries: ``.git`` (a checkout we own) and the
install stamp that ships with the code (``install-stamp.json``, written by
``scripts/write_install_stamp.py`` with a required ``updateMechanism`` of
``self``/``electron-updater``/``external``).
"""
import json
import pytest
from hermes_cli.steward import (
STEWARD_DESKTOP,
STEWARD_DOCKER,
STEWARD_NIX,
classify_install,
read_install_stamp,
sealed_steward,
)
def _stamp(root, *, mechanism="external", **fields):
root.mkdir(exist_ok=True)
path = root / "install-stamp.json"
payload = {"commit": "a" * 40, "updateMechanism": mechanism, **fields}
path.write_text(json.dumps(payload), encoding="utf-8")
return path
class TestStampDrivenDetection:
@pytest.mark.parametrize(
("distribution", "mechanism"),
[
(STEWARD_NIX, "external"),
(STEWARD_DOCKER, "external"),
(STEWARD_DESKTOP, "electron-updater"),
],
)
def test_sealed_tree_names_its_steward(self, tmp_path, distribution, mechanism):
root = tmp_path / "install"
_stamp(root, mechanism=mechanism, distribution=distribution)
assert sealed_steward(root) == distribution
assert classify_install(root) == (distribution, False)
def test_git_checkout_is_not_steward_owned(self, tmp_path):
root = tmp_path / "checkout"
root.mkdir()
(root / ".git").mkdir()
assert sealed_steward(root) is None
assert classify_install(root) == ("git", True)
def test_git_beats_stamp(self, tmp_path):
"""A checkout carrying a baked stamp (self-updating source install)
is still a checkout — the uninstaller may remove it."""
root = tmp_path / "checkout"
root.mkdir()
(root / ".git").mkdir()
_stamp(root, mechanism="self", distribution=None)
assert sealed_steward(root) is None
def test_worktree_gitfile_counts_as_checkout(self, tmp_path):
root = tmp_path / "worktree"
root.mkdir()
(root / ".git").write_text("gitdir: ../elsewhere\n", encoding="utf-8")
assert sealed_steward(root) is None
def test_missing_stamp_gives_unknown_steward(self, tmp_path):
"""No .git and no stamp: we cannot prove the tree is ours — sealed
under an unknown steward, so code removal refuses."""
root = tmp_path / "bare"
root.mkdir()
assert sealed_steward(root) == "unknown"
assert classify_install(root) == ("unknown", False)
def test_executing_nix_tree_reads_the_stamp_the_wrapper_points_at(self, tmp_path, monkeypatch):
"""A Nix package bakes the stamp outside the store's package dir; its wrapper
carries HERMES_INSTALL_ROOT. The executing tree must classify as nix (not
"unknown"), the way version_info already reports it."""
from hermes_cli.version_info import _resolve_stamp_file
package = tmp_path / "store" / "lib" / "hermes-agent"
package.mkdir(parents=True)
share = tmp_path / "store" / "share" / "hermes-agent"
share.mkdir(parents=True)
_stamp(share, mechanism="external", distribution="nix")
monkeypatch.setattr("pm.paths.repo_root", lambda: package)
monkeypatch.setenv("HERMES_INSTALL_ROOT", str(share))
assert sealed_steward(package) == "nix"
assert _resolve_stamp_file() == share / "install-stamp.json"
# Another tree is taken literally: the wrapper speaks for the executing tree only.
other = tmp_path / "elsewhere"
other.mkdir()
assert sealed_steward(other) == "unknown"
def test_malformed_stamp_degrades_to_unknown(self, tmp_path):
root = tmp_path / "install"
root.mkdir()
(root / "install-stamp.json").write_text("{not json", encoding="utf-8")
assert read_install_stamp(root) == {}
assert sealed_steward(root) == "unknown"
class TestUpdateAdmission:
"""The update admission gate refuses sealed trees with the steward's text."""
@pytest.fixture(autouse=True)
def _no_image_marker(self, tmp_path, monkeypatch):
import hermes_cli.image_provenance as ip
monkeypatch.setattr(ip, "IMAGE_PROVENANCE_PATH", tmp_path / "absent.json")
def test_desktop_app_stamp_refuses_update(self, tmp_path, monkeypatch):
from hermes_cli.update_contract import evaluate_update_admission
root = tmp_path / "payload"
_stamp(root, mechanism="electron-updater", distribution=STEWARD_DESKTOP)
refusal = evaluate_update_admission(root)
assert refusal is not None
assert refusal.code == STEWARD_DESKTOP
assert "desktop app" in refusal.message
def test_nix_stamp_refuses_update(self, tmp_path):
from hermes_cli.update_contract import evaluate_update_admission
root = tmp_path / "store-tree"
_stamp(root, mechanism="external", distribution=STEWARD_NIX)
refusal = evaluate_update_admission(root)
assert refusal is not None
assert refusal.code == "nix"
assert "Nix store" in refusal.message
def test_docker_stamp_refuses_with_docker_message(self, tmp_path):
from hermes_cli.update_contract import evaluate_update_admission
root = tmp_path / "image-tree"
_stamp(root, mechanism="external", distribution=STEWARD_DOCKER)
refusal = evaluate_update_admission(root)
assert refusal is not None
assert refusal.code == "docker"
assert "docker pull" in refusal.update_command
def test_git_checkout_is_admitted(self, tmp_path, monkeypatch):
from hermes_cli.update_contract import evaluate_update_admission
root = tmp_path / "checkout"
root.mkdir()
(root / ".git").mkdir()
monkeypatch.setattr(
"hermes_cli.config.detect_install_method", lambda *a, **k: "git"
)
assert evaluate_update_admission(root) is None
def test_unknown_steward_falls_through_to_heuristics(self, tmp_path, monkeypatch):
"""A stampless gitless tree is not refused by the steward rung —
the pre-existing heuristics keep the final say (an unknown tree used
to be admitted, and stays admitted)."""
from hermes_cli.update_contract import evaluate_update_admission
root = tmp_path / "bare"
root.mkdir()
monkeypatch.setattr(
"hermes_cli.config.detect_install_method", lambda *a, **k: "unknown"
)
assert evaluate_update_admission(root) is None