diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 15cfeaf354..d2d5c65e26 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -425,7 +425,8 @@ def _install_method_project_root(project_root: Optional[Path] = None) -> Path: def detect_install_method(project_root: Optional[Path] = None) -> str: - """Detect how Hermes was installed: 'apt', 'docker', 'nix', 'nixos', 'git', or 'unknown'. + """Detect how Hermes was installed: 'apt', 'docker', 'nix', 'nixos', + 'home-manager', 'git', or 'unknown'. Resolution order: 1. Code-scoped stamp ``/.install_method`` (next to the @@ -473,7 +474,10 @@ def detect_install_method(project_root: Optional[Path] = None) -> str: # generic Debian/Ubuntu APT signal. If another APT-managed distribution is # added, give it a distinct install method or make update-command selection # platform-aware instead of silently reusing Termux's `pkg` command. - supported_methods = {"apt", "docker", "nix", "nixos", "git", "unknown"} + # "home-manager" is here because step 3 can return it. A stamp must name + # every method that this function returns. Without it, the stamp of a + # home-manager install gives "unknown". + supported_methods = {"apt", "docker", "nix", "nixos", "home-manager", "git", "unknown"} # 1. Code-scoped stamp — authoritative, immune to shared $HERMES_HOME. try: diff --git a/tests/hermes_cli/test_managed_install_shapes.py b/tests/hermes_cli/test_managed_install_shapes.py index 89aad936a4..8a94b8c539 100644 --- a/tests/hermes_cli/test_managed_install_shapes.py +++ b/tests/hermes_cli/test_managed_install_shapes.py @@ -79,20 +79,45 @@ def test_env_var_wins_over_the_marker(hermes_home, monkeypatch): @pytest.mark.parametrize("managed_value", ["nixos", "home-manager"]) def test_managed_install_names_its_system_and_offers_an_update( - hermes_home, monkeypatch, managed_value + hermes_home, monkeypatch, tmp_path, managed_value ): """The message names the system, so the user knows what owns the install.""" monkeypatch.setenv("HERMES_MANAGED", managed_value) + # This test uses an install tree of its own. The real checkout can carry + # a stamp from the install shape of the contributor. A stamp answers + # first, and detection never reaches the managed state under test. + install_tree = tmp_path / "install" + install_tree.mkdir() + assert managed_value in config_mod.format_managed_message("set model") assert "set model" in config_mod.format_managed_message("set model") assert config_mod.get_managed_update_command() - assert config_mod.detect_install_method(config_mod.get_project_root()) == managed_value + assert config_mod.detect_install_method(install_tree) == managed_value # `hermes update` cannot run on a managed install, so the advice must not # name it. assert config_mod.recommended_update_command() != "hermes update" +@pytest.mark.parametrize("managed_value", ["nixos", "home-manager"]) +def test_a_stamp_can_name_every_managed_system( + hermes_home, monkeypatch, tmp_path, managed_value +): + """A stamp must give back every value that detection can return. + + Detection reads the stamp against an allowlist. A managed system that is + absent from that allowlist gives "unknown". The update guidance then + names a command that the managed guard refuses. + """ + monkeypatch.delenv("HERMES_MANAGED", raising=False) + install_tree = tmp_path / "install" + install_tree.mkdir() + + config_mod.stamp_install_method(managed_value, project_root=install_tree) + + assert config_mod.detect_install_method(install_tree) == managed_value + + def test_unmanaged_install_offers_no_update_command(hermes_home, monkeypatch): monkeypatch.delenv("HERMES_MANAGED", raising=False) assert config_mod.get_managed_update_command() is None