Files
hermes-webui/tests/test_issue3066_disabled_read_profile.py

219 lines
8.4 KiB
Python

"""Regression tests for issue #3066: skills panel disabled-state read path
must resolve against the active WebUI profile, not the process-global
HERMES_HOME.
"""
import yaml
from pathlib import Path
from tests.conftest import requires_agent_modules
def _write_config(config_path: Path, data: dict) -> None:
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(yaml.dump(data), encoding="utf-8")
def _write_skill(skills_dir: Path, name: str) -> None:
skill_dir = skills_dir / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: A test skill\n---\n\n# {name}\n",
encoding="utf-8",
)
def test_disabled_read_uses_active_profile_config(tmp_path, monkeypatch):
"""_get_disabled_skill_names_for_profile reads from the active profile's
config.yaml (via _get_config_path), not from HERMES_HOME."""
from api import routes
profile_home = tmp_path / "profiles" / "work"
config_path = profile_home / "config.yaml"
_write_config(config_path, {"skills": {"disabled": ["skill-x", "skill-y"]}})
# Point _get_config_path at the profile config
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == {"skill-x", "skill-y"}
def test_disabled_read_prefers_platform_disabled_webui(tmp_path, monkeypatch):
"""When platform_disabled.webui exists, it takes precedence over the
global disabled list."""
from api import routes
config_path = tmp_path / "config.yaml"
_write_config(config_path, {
"skills": {
"disabled": ["global-disabled"],
"platform_disabled": {
"webui": ["webui-disabled-a", "webui-disabled-b"],
"telegram": ["telegram-disabled"],
},
}
})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == {"webui-disabled-a", "webui-disabled-b"}
assert "global-disabled" not in result
def test_disabled_read_falls_back_to_global_disabled(tmp_path, monkeypatch):
"""When platform_disabled.webui is absent, falls back to skills.disabled."""
from api import routes
config_path = tmp_path / "config.yaml"
_write_config(config_path, {"skills": {"disabled": ["fallback-skill"]}})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == {"fallback-skill"}
def test_disabled_read_empty_when_no_config(tmp_path, monkeypatch):
"""Returns empty set when config.yaml does not exist."""
from api import routes
config_path = tmp_path / "nonexistent" / "config.yaml"
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == set()
def test_disabled_read_empty_when_no_skills_section(tmp_path, monkeypatch):
"""Returns empty set when config has no skills section."""
from api import routes
config_path = tmp_path / "config.yaml"
_write_config(config_path, {"model": "gpt-4"})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == set()
@requires_agent_modules
def test_skills_list_disabled_reflects_active_profile(tmp_path, monkeypatch):
"""_skills_list_from_dir marks skills as disabled based on the active
profile's config, not the process-global HERMES_HOME."""
from api import routes
# Set up skills directory with two skills
skills_dir = tmp_path / "skills"
_write_skill(skills_dir, "skill-a")
_write_skill(skills_dir, "skill-b")
# Profile config disables only skill-a
config_path = tmp_path / "config.yaml"
_write_config(config_path, {"skills": {"disabled": ["skill-a"]}})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
monkeypatch.setattr("api.routes._active_skills_dir", lambda: skills_dir)
result = routes._skills_list_from_dir(skills_dir)
skills = {s["name"]: s for s in result["skills"]}
assert skills["skill-a"]["disabled"] is True
assert skills["skill-b"]["disabled"] is False
@requires_agent_modules
def test_profile_switch_changes_disabled_state(tmp_path, monkeypatch):
"""Simulates switching profiles: disabled state should follow the active
profile's config, not remain stuck on the original profile."""
from api import routes
# Two profile configs with different disabled lists
profile_a_config = tmp_path / "profile-a" / "config.yaml"
profile_b_config = tmp_path / "profile-b" / "config.yaml"
_write_config(profile_a_config, {"skills": {"disabled": ["skill-x"]}})
_write_config(profile_b_config, {"skills": {"disabled": ["skill-y"]}})
# Skills dir with both skills
skills_dir = tmp_path / "skills"
_write_skill(skills_dir, "skill-x")
_write_skill(skills_dir, "skill-y")
monkeypatch.setattr("api.routes._active_skills_dir", lambda: skills_dir)
# "Active" profile A
monkeypatch.setattr("api.routes._get_config_path", lambda: profile_a_config)
result_a = routes._skills_list_from_dir(skills_dir)
skills_a = {s["name"]: s for s in result_a["skills"]}
assert skills_a["skill-x"]["disabled"] is True
assert skills_a["skill-y"]["disabled"] is False
# "Switch" to profile B
monkeypatch.setattr("api.routes._get_config_path", lambda: profile_b_config)
result_b = routes._skills_list_from_dir(skills_dir)
skills_b = {s["name"]: s for s in result_b["skills"]}
assert skills_b["skill-x"]["disabled"] is False
assert skills_b["skill-y"]["disabled"] is True
def test_normalize_disabled_set_handles_edge_cases():
"""_normalize_disabled_set handles None, str, list, and whitespace."""
from api.routes import _normalize_disabled_set
assert _normalize_disabled_set(None) == set()
assert _normalize_disabled_set("single") == {"single"}
assert _normalize_disabled_set(["a", "b"]) == {"a", "b"}
assert _normalize_disabled_set([" spaced ", " "]) == {"spaced"}
assert _normalize_disabled_set([]) == set()
def test_disabled_read_decodes_json_array_string(tmp_path, monkeypatch):
"""Issue #7120: skills.disabled stored as a JSON-array string (the shape
produced by `hermes config set skills.disabled ...`) must decode to the
real names, not one literal entry."""
from api import routes
config_path = tmp_path / "config.yaml"
_write_config(config_path, {"skills": {"disabled": '["skill-x", "skill-y"]'}})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == {"skill-x", "skill-y"}
def test_disabled_read_platform_webui_decodes_json_array_string(tmp_path, monkeypatch):
"""Issue #7120: platform_disabled.webui stored as a JSON-array string is
decoded the same way as the global disabled list."""
from api import routes
config_path = tmp_path / "config.yaml"
_write_config(config_path, {
"skills": {
"disabled": ["global-disabled"],
"platform_disabled": {"webui": '["webui-disabled-a", "webui-disabled-b"]'},
}
})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
result = routes._get_disabled_skill_names_for_profile()
assert result == {"webui-disabled-a", "webui-disabled-b"}
assert "global-disabled" not in result
@requires_agent_modules
def test_skills_list_disabled_decodes_json_array_string(tmp_path, monkeypatch):
"""Issue #7120: the Skills panel (skills list endpoint) must report both
skills as disabled when config.yaml stores disabled as a JSON-array string."""
from api import routes
skills_dir = tmp_path / "skills"
_write_skill(skills_dir, "skill-a")
_write_skill(skills_dir, "skill-b")
config_path = tmp_path / "config.yaml"
_write_config(config_path, {"skills": {"disabled": '["skill-a", "skill-b"]'}})
monkeypatch.setattr("api.routes._get_config_path", lambda: config_path)
monkeypatch.setattr("api.routes._active_skills_dir", lambda: skills_dir)
result = routes._skills_list_from_dir(skills_dir)
skills = {s["name"]: s for s in result["skills"]}
assert skills["skill-a"]["disabled"] is True
assert skills["skill-b"]["disabled"] is True