| """Tests for the Command Installation check in hermes doctor.""" |
|
|
| import os |
| import sys |
| import types |
| from argparse import Namespace |
| from pathlib import Path |
|
|
| import pytest |
|
|
| import hermes_cli.doctor as doctor_mod |
|
|
|
|
| def _setup_doctor_env(monkeypatch, tmp_path, venv_name="venv"): |
| """Create a minimal HERMES_HOME + PROJECT_ROOT for doctor tests.""" |
| home = tmp_path / ".hermes" |
| home.mkdir(parents=True, exist_ok=True) |
| (home / "config.yaml").write_text("memory: {}\n", encoding="utf-8") |
|
|
| project = tmp_path / "project" |
| project.mkdir(exist_ok=True) |
|
|
| |
| venv_bin_dir = project / venv_name / "bin" |
| venv_bin_dir.mkdir(parents=True, exist_ok=True) |
| hermes_bin = venv_bin_dir / "hermes" |
| hermes_bin.write_text("#!/usr/bin/env python\n# entry point\n") |
| hermes_bin.chmod(0o755) |
|
|
| monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) |
| monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project) |
| monkeypatch.setattr(doctor_mod, "_DHH", str(home)) |
|
|
| |
| fake_model_tools = types.SimpleNamespace( |
| check_tool_availability=lambda *a, **kw: ([], []), |
| TOOLSET_REQUIREMENTS={}, |
| ) |
| monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) |
|
|
| |
| try: |
| from hermes_cli import auth as _auth_mod |
| monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) |
| monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) |
| except Exception: |
| pass |
|
|
| |
| try: |
| import httpx |
| monkeypatch.setattr(httpx, "get", lambda *a, **kw: types.SimpleNamespace(status_code=200)) |
| except Exception: |
| pass |
|
|
| return home, project, hermes_bin |
|
|
|
|
| def _run_doctor(fix=False): |
| """Run doctor and capture stdout.""" |
| import io |
| import contextlib |
|
|
| buf = io.StringIO() |
| with contextlib.redirect_stdout(buf): |
| doctor_mod.run_doctor(Namespace(fix=fix)) |
| return buf.getvalue() |
|
|
|
|
| class TestDoctorCommandInstallation: |
| """Tests for the ◆ Command Installation section.""" |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_correct_symlink_shows_ok(self, monkeypatch, tmp_path): |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| |
| cmd_link_dir = tmp_path / ".local" / "bin" |
| cmd_link_dir.mkdir(parents=True) |
| cmd_link = cmd_link_dir / "hermes" |
| cmd_link.symlink_to(hermes_bin) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" in out |
| assert "Venv entry point exists" in out |
| assert "correct target" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_missing_symlink_shows_fail(self, monkeypatch, tmp_path): |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
| |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" in out |
| assert "Venv entry point exists" in out |
| assert "not found" in out |
| assert "hermes doctor --fix" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_fix_creates_missing_symlink(self, monkeypatch, tmp_path): |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=True) |
| assert "Command Installation" in out |
| assert "Created symlink" in out |
|
|
| |
| cmd_link = tmp_path / ".local" / "bin" / "hermes" |
| assert cmd_link.is_symlink() |
| assert cmd_link.resolve() == hermes_bin.resolve() |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_wrong_target_symlink_shows_warn(self, monkeypatch, tmp_path): |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| |
| cmd_link_dir = tmp_path / ".local" / "bin" |
| cmd_link_dir.mkdir(parents=True) |
| cmd_link = cmd_link_dir / "hermes" |
| wrong_target = tmp_path / "wrong_hermes" |
| wrong_target.write_text("#!/usr/bin/env python\n") |
| cmd_link.symlink_to(wrong_target) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" in out |
| assert "wrong target" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_fix_repairs_wrong_symlink(self, monkeypatch, tmp_path): |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| |
| cmd_link_dir = tmp_path / ".local" / "bin" |
| cmd_link_dir.mkdir(parents=True) |
| cmd_link = cmd_link_dir / "hermes" |
| wrong_target = tmp_path / "wrong_hermes" |
| wrong_target.write_text("#!/usr/bin/env python\n") |
| cmd_link.symlink_to(wrong_target) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=True) |
| assert "Fixed symlink" in out |
|
|
| |
| assert cmd_link.is_symlink() |
| assert cmd_link.resolve() == hermes_bin.resolve() |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_missing_venv_entry_point_shows_warn(self, monkeypatch, tmp_path): |
| home = tmp_path / ".hermes" |
| home.mkdir(parents=True, exist_ok=True) |
| (home / "config.yaml").write_text("memory: {}\n", encoding="utf-8") |
|
|
| project = tmp_path / "project" |
| project.mkdir(exist_ok=True) |
| |
|
|
| monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) |
| monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project) |
| monkeypatch.setattr(doctor_mod, "_DHH", str(home)) |
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| fake_model_tools = types.SimpleNamespace( |
| check_tool_availability=lambda *a, **kw: ([], []), |
| TOOLSET_REQUIREMENTS={}, |
| ) |
| monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) |
| try: |
| from hermes_cli import auth as _auth_mod |
| monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) |
| monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) |
| except Exception: |
| pass |
| try: |
| import httpx |
| monkeypatch.setattr(httpx, "get", lambda *a, **kw: types.SimpleNamespace(status_code=200)) |
| except Exception: |
| pass |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" in out |
| assert "Venv entry point not found" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_dot_venv_dir_is_found(self, monkeypatch, tmp_path): |
| """The check finds entry points in .venv/ as well as venv/.""" |
| home, project, _ = _setup_doctor_env(monkeypatch, tmp_path, venv_name=".venv") |
|
|
| |
| hermes_bin = project / ".venv" / "bin" / "hermes" |
| cmd_link_dir = tmp_path / ".local" / "bin" |
| cmd_link_dir.mkdir(parents=True) |
| cmd_link = cmd_link_dir / "hermes" |
| cmd_link.symlink_to(hermes_bin) |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=False) |
| assert "Venv entry point exists" in out |
| assert ".venv/bin/hermes" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_non_symlink_regular_file_shows_ok(self, monkeypatch, tmp_path): |
| """If ~/.local/bin/hermes is a regular file (not symlink), accept it.""" |
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| cmd_link_dir = tmp_path / ".local" / "bin" |
| cmd_link_dir.mkdir(parents=True) |
| cmd_link = cmd_link_dir / "hermes" |
| cmd_link.write_text("#!/bin/sh\nexec python -m hermes_cli.main \"$@\"\n") |
|
|
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=False) |
| assert "non-symlink" in out |
|
|
| @pytest.mark.skipif(sys.platform == "win32", reason="Symlink check is Unix-only") |
| def test_termux_uses_prefix_bin(self, monkeypatch, tmp_path): |
| """On Termux, the command link dir is $PREFIX/bin.""" |
| prefix_dir = tmp_path / "termux_prefix" |
| prefix_bin = prefix_dir / "bin" |
| prefix_bin.mkdir(parents=True) |
|
|
| home, project, hermes_bin = _setup_doctor_env(monkeypatch, tmp_path) |
|
|
| monkeypatch.setenv("TERMUX_VERSION", "0.118.3") |
| monkeypatch.setenv("PREFIX", str(prefix_dir)) |
| monkeypatch.setattr(Path, "home", lambda: tmp_path) |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" in out |
| assert "$PREFIX/bin" in out |
|
|
| def test_windows_skips_check(self, monkeypatch, tmp_path): |
| """On Windows, the Command Installation section is skipped.""" |
| home = tmp_path / ".hermes" |
| home.mkdir(parents=True, exist_ok=True) |
| (home / "config.yaml").write_text("memory: {}\n", encoding="utf-8") |
|
|
| project = tmp_path / "project" |
| project.mkdir(exist_ok=True) |
|
|
| monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) |
| monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project) |
| monkeypatch.setattr(doctor_mod, "_DHH", str(home)) |
| monkeypatch.setattr(sys, "platform", "win32") |
|
|
| fake_model_tools = types.SimpleNamespace( |
| check_tool_availability=lambda *a, **kw: ([], []), |
| TOOLSET_REQUIREMENTS={}, |
| ) |
| monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) |
| try: |
| from hermes_cli import auth as _auth_mod |
| monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) |
| monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) |
| except Exception: |
| pass |
| try: |
| import httpx |
| monkeypatch.setattr(httpx, "get", lambda *a, **kw: types.SimpleNamespace(status_code=200)) |
| except Exception: |
| pass |
|
|
| out = _run_doctor(fix=False) |
| assert "Command Installation" not in out |
|
|