mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
275 lines
11 KiB
Python
275 lines
11 KiB
Python
"""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_agent.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)
|
|
|
|
# Create a fake venv entry point
|
|
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))
|
|
|
|
# Stub model_tools so doctor doesn't fail on import
|
|
fake_model_tools = types.SimpleNamespace(
|
|
check_tool_availability=lambda *a, **kw: ([], []),
|
|
TOOLSET_REQUIREMENTS={},
|
|
)
|
|
monkeypatch.setitem(sys.modules, "hermes_agent.tools.dispatch", fake_model_tools)
|
|
|
|
# Stub auth checks
|
|
try:
|
|
from hermes_agent.cli.auth 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
|
|
|
|
# Stub httpx.get to avoid network calls
|
|
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)
|
|
|
|
# Create the command link dir with correct symlink
|
|
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)
|
|
# Don't create the symlink — it should be missing
|
|
|
|
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
|
|
|
|
# Verify the symlink was actually created
|
|
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)
|
|
|
|
# Create a symlink pointing to the wrong target
|
|
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)
|
|
|
|
# Create a symlink pointing to wrong target
|
|
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
|
|
|
|
# Verify the symlink now points to the correct target
|
|
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)
|
|
# Do NOT create any venv entry point
|
|
|
|
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, "hermes_agent.tools.dispatch", fake_model_tools)
|
|
try:
|
|
from hermes_agent.cli.auth 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")
|
|
|
|
# Create the command link with correct symlink
|
|
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, "hermes_agent.tools.dispatch", fake_model_tools)
|
|
try:
|
|
from hermes_agent.cli.auth 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
|