"""Regression: a bot profile's system prompt must reflect ITS OWN skills/home, never the launch (default) profile's — even when the agent build runs on a thread that did not bind the HERMES_HOME ContextVar. Root cause this guards (confirmed empirically): ContextVars do not propagate into ``threading.Thread``. ``build_skills_system_prompt`` and the active-profile line resolved the home via the ambient ``get_hermes_home()``, so an unbound build thread fell back to ``~/.hermes`` (default) and leaked default's full skills index + "Active Hermes profile: default" into a bot's prompt, while the live ``skills_list()`` (re-bound per turn) correctly showed the bot's real, empty set. The agent now resolves its own home from its ``_session_db.db_path`` and passes it explicitly. """ import re import threading import pytest def _skills_body(prompt: str) -> str: m = re.search(r"(.*?)", prompt, re.DOTALL) return (m.group(1).strip() if m else "") def test_skills_prompt_scoped_to_override_not_ambient_home(tmp_path, monkeypatch): """An explicit skills_dir_override wins over ambient HERMES_HOME, on a bare thread with no override bound.""" from agent import prompt_builder # A "default" home WITH skills (the thing that must NOT leak). default_home = tmp_path / "default" default_skills = default_home / "skills" / "general" / "leaky-skill" default_skills.mkdir(parents=True) (default_skills / "SKILL.md").write_text( "---\nname: leaky-skill\ndescription: should never appear in a bot prompt\n---\nbody\n", encoding="utf-8", ) # An empty bot profile (no skills dir at all). bot_skills = tmp_path / "profiles" / "emptybot" / "skills" # Bind ambient home to default (mimics a build thread that lost the # bot's override and fell back to launch). monkeypatch.setenv("HERMES_HOME", str(default_home)) prompt_builder.clear_skills_system_prompt_cache(clear_snapshot=False) result = {} def build(): # No set_hermes_home_override on THIS thread — ambient resolves to # default. The override arg must still scope to the empty bot. result["bot"] = _skills_body( prompt_builder.build_skills_system_prompt(skills_dir_override=bot_skills) ) t = threading.Thread(target=build) t.start() t.join() assert result["bot"] == "", ( "empty bot profile leaked skills from the ambient (default) home: " + result["bot"][:200] ) def test_agent_home_resolves_from_session_db_path(tmp_path): """The agent's own home is read from its session_db, independent of any ContextVar.""" from agent import system_prompt bot_home = tmp_path / "profiles" / "mybot" bot_home.mkdir(parents=True) class _DB: db_path = bot_home / "state.db" class _Agent: _session_db = _DB() assert system_prompt._agent_home(_Agent()) == bot_home assert system_prompt._agent_skills_dir(_Agent()) == bot_home / "skills" def test_agent_home_none_without_session_db(): from agent import system_prompt class _Agent: _session_db = None assert system_prompt._agent_home(_Agent()) is None assert system_prompt._agent_skills_dir(_Agent()) is None def test_profile_name_correct_on_bound_profile_session(tmp_path, monkeypatch): """Regression for the fix-of-the-fix: on a CORRECTLY bound profile session the ambient home IS the profile dir, so deriving the profile name with ``get_hermes_home()/profiles`` as the root would never match and every profile would misreport as \"default\". The name must derive from the hermes ROOT (get_default_hermes_root).""" from agent import system_prompt bot_home = tmp_path / "profiles" / "mybot" bot_home.mkdir(parents=True) # Bound session: HERMES_HOME env points at the profile dir itself. monkeypatch.setenv("HERMES_HOME", str(bot_home)) assert system_prompt._profile_name_for_home(bot_home) == "mybot" def test_profile_name_default_when_home_is_root(tmp_path, monkeypatch): from agent import system_prompt monkeypatch.setenv("HERMES_HOME", str(tmp_path)) assert system_prompt._profile_name_for_home(tmp_path) == "default" def test_profile_name_correct_when_ambient_is_another_profile(tmp_path, monkeypatch): """CodeRabbit case: agent belongs to profile A while the ambient home is bound to profile B. The root must derive independently of the ambient home, so A still resolves as 'mybot' (not 'default', and never 'other').""" from agent import system_prompt bot_home = tmp_path / "profiles" / "mybot" bot_home.mkdir(parents=True) other_home = tmp_path / "profiles" / "other" other_home.mkdir(parents=True) monkeypatch.setenv("HERMES_HOME", str(other_home)) assert system_prompt._profile_name_for_home(bot_home) == "mybot"