/ tests / test_hermes_home_profile_warning.py
test_hermes_home_profile_warning.py
  1  """Tests for get_hermes_home() profile-mode fallback warning.
  2  
  3  Regression test for https://github.com/NousResearch/hermes-agent/issues/18594.
  4  
  5  When HERMES_HOME is unset but an active_profile file indicates a non-default
  6  profile is active, get_hermes_home() should:
  7    1. STILL return ~/.hermes (raising would brick 30+ module-level callers)
  8    2. Emit a loud one-shot warning to stderr so operators can diagnose
  9       cross-profile data contamination after the fact.
 10  
 11  The warning goes to stderr directly (not through logging) because this
 12  function is called at module-import time from 30+ sites, often before the
 13  logging subsystem has been configured.
 14  """
 15  
 16  from pathlib import Path
 17  
 18  import pytest
 19  
 20  
 21  @pytest.fixture
 22  def fresh_constants(monkeypatch, tmp_path):
 23      """Import hermes_constants fresh and reset the one-shot warn flag."""
 24      import importlib
 25      import hermes_constants
 26      importlib.reload(hermes_constants)
 27      monkeypatch.setattr(Path, "home", lambda: tmp_path)
 28      monkeypatch.delenv("HERMES_HOME", raising=False)
 29      return hermes_constants
 30  
 31  
 32  class TestGetHermesHomeProfileWarning:
 33      def test_classic_mode_no_active_profile_no_warning(
 34          self, fresh_constants, tmp_path, capsys
 35      ):
 36          """Classic mode: no active_profile file → silent, returns ~/.hermes."""
 37          result = fresh_constants.get_hermes_home()
 38          assert result == tmp_path / ".hermes"
 39          assert "HERMES_HOME fallback" not in capsys.readouterr().err
 40  
 41      def test_default_active_profile_no_warning(
 42          self, fresh_constants, tmp_path, capsys
 43      ):
 44          """active_profile=default → still no warning, returns ~/.hermes."""
 45          hermes_dir = tmp_path / ".hermes"
 46          hermes_dir.mkdir()
 47          (hermes_dir / "active_profile").write_text("default\n")
 48          result = fresh_constants.get_hermes_home()
 49          assert result == tmp_path / ".hermes"
 50          assert "HERMES_HOME fallback" not in capsys.readouterr().err
 51  
 52      def test_named_profile_unset_home_warns_once(
 53          self, fresh_constants, tmp_path, capsys
 54      ):
 55          """active_profile=coder + HERMES_HOME unset → warn loudly, still return fallback."""
 56          hermes_dir = tmp_path / ".hermes"
 57          hermes_dir.mkdir()
 58          (hermes_dir / "active_profile").write_text("coder\n")
 59  
 60          result = fresh_constants.get_hermes_home()
 61  
 62          # 1. Still returns the fallback — no import-time crash
 63          assert result == tmp_path / ".hermes"
 64          # 2. Stderr got the warning exactly once
 65          err = capsys.readouterr().err
 66          assert err.count("HERMES_HOME fallback") == 1
 67          assert "'coder'" in err
 68          assert "#18594" in err
 69  
 70          # 3. One-shot: second and third calls don't re-warn
 71          fresh_constants.get_hermes_home()
 72          fresh_constants.get_hermes_home()
 73          err2 = capsys.readouterr().err
 74          assert "HERMES_HOME fallback" not in err2
 75  
 76      def test_hermes_home_set_suppresses_warning(
 77          self, fresh_constants, tmp_path, capsys, monkeypatch
 78      ):
 79          """Even if active_profile is 'coder', setting HERMES_HOME suppresses warning."""
 80          profile_dir = tmp_path / ".hermes" / "profiles" / "coder"
 81          profile_dir.mkdir(parents=True)
 82          (tmp_path / ".hermes" / "active_profile").write_text("coder\n")
 83          monkeypatch.setenv("HERMES_HOME", str(profile_dir))
 84  
 85          result = fresh_constants.get_hermes_home()
 86  
 87          assert result == profile_dir
 88          assert "HERMES_HOME fallback" not in capsys.readouterr().err
 89  
 90      def test_unreadable_active_profile_no_crash(
 91          self, fresh_constants, tmp_path, capsys
 92      ):
 93          """active_profile that can't be decoded → fall through silently."""
 94          hermes_dir = tmp_path / ".hermes"
 95          hermes_dir.mkdir()
 96          # Write bytes that aren't valid utf-8
 97          (hermes_dir / "active_profile").write_bytes(b"\xff\xfe\x00\x00")
 98  
 99          result = fresh_constants.get_hermes_home()
100  
101          assert result == tmp_path / ".hermes"
102          # Shouldn't crash; shouldn't warn either (can't tell what profile was intended)
103          assert "HERMES_HOME fallback" not in capsys.readouterr().err
104  
105      def test_empty_active_profile_no_warning(
106          self, fresh_constants, tmp_path, capsys
107      ):
108          """Empty active_profile file → treated as default, no warning."""
109          hermes_dir = tmp_path / ".hermes"
110          hermes_dir.mkdir()
111          (hermes_dir / "active_profile").write_text("")
112  
113          result = fresh_constants.get_hermes_home()
114  
115          assert result == tmp_path / ".hermes"
116          assert "HERMES_HOME fallback" not in capsys.readouterr().err