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