mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
"""Tests for hermes_cli/skills_config.py and skills_tool disabled filtering."""
|
|
from unittest.mock import patch
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_disabled_skills
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetDisabledSkills:
|
|
def test_empty_config(self):
|
|
from hermes_cli.skills_config import get_disabled_skills
|
|
assert get_disabled_skills({}) == set()
|
|
|
|
|
|
|
|
|
|
def test_null_skills_section(self):
|
|
"""``skills:`` with no value (YAML null) must not crash (#13026)."""
|
|
from hermes_cli.skills_config import get_disabled_skills
|
|
assert get_disabled_skills({"skills": None}) == set()
|
|
assert get_disabled_skills({"skills": None}, platform="telegram") == set()
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# save_disabled_skills
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSaveDisabledSkills:
|
|
@patch("hermes_cli.skills_config.save_config")
|
|
def test_saves_global_sorted(self, mock_save):
|
|
from hermes_cli.skills_config import save_disabled_skills
|
|
config = {}
|
|
save_disabled_skills(config, {"skill-z", "skill-a"})
|
|
assert config["skills"]["disabled"] == ["skill-a", "skill-z"]
|
|
mock_save.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_skill_disabled
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsSkillDisabled:
|
|
|
|
|
|
@patch("hermes_cli.config.load_config")
|
|
def test_platform_disabled(self, mock_load):
|
|
mock_load.return_value = {"skills": {
|
|
"disabled": [],
|
|
"platform_disabled": {"telegram": ["tg-skill"]}
|
|
}}
|
|
from tools.skills_tool import _is_skill_disabled
|
|
assert _is_skill_disabled("tg-skill", platform="telegram") is True
|
|
|
|
|
|
|
|
@patch("hermes_cli.config.load_config")
|
|
@patch.dict("os.environ", {"HERMES_PLATFORM": "discord"})
|
|
def test_env_var_platform(self, mock_load):
|
|
mock_load.return_value = {"skills": {
|
|
"platform_disabled": {"discord": ["discord-skill"]}
|
|
}}
|
|
from tools.skills_tool import _is_skill_disabled
|
|
assert _is_skill_disabled("discord-skill") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_disabled_skill_names — explicit platform param & env var fallback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetDisabledSkillNames:
|
|
"""Tests for agent.skill_utils.get_disabled_skill_names."""
|
|
|
|
def test_explicit_platform_param(self, tmp_path, monkeypatch):
|
|
"""Explicit platform= parameter should resolve per-platform list."""
|
|
config = tmp_path / "config.yaml"
|
|
config.write_text(
|
|
"skills:\n"
|
|
" disabled:\n"
|
|
" - global-skill\n"
|
|
" platform_disabled:\n"
|
|
" telegram:\n"
|
|
" - tg-only-skill\n"
|
|
)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.delenv("HERMES_PLATFORM", raising=False)
|
|
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
|
|
|
from agent.skill_utils import get_disabled_skill_names
|
|
result = get_disabled_skill_names(platform="telegram")
|
|
assert result == {"tg-only-skill", "global-skill"}
|
|
|
|
def test_session_platform_env_var(self, tmp_path, monkeypatch):
|
|
"""HERMES_SESSION_PLATFORM should be used when HERMES_PLATFORM is unset."""
|
|
config = tmp_path / "config.yaml"
|
|
config.write_text(
|
|
"skills:\n"
|
|
" disabled:\n"
|
|
" - global-skill\n"
|
|
" platform_disabled:\n"
|
|
" discord:\n"
|
|
" - discord-skill\n"
|
|
)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.delenv("HERMES_PLATFORM", raising=False)
|
|
monkeypatch.setenv("HERMES_SESSION_PLATFORM", "discord")
|
|
|
|
from agent.skill_utils import get_disabled_skill_names
|
|
result = get_disabled_skill_names()
|
|
assert result == {"discord-skill", "global-skill"}
|
|
|
|
def test_hermes_platform_takes_precedence(self, tmp_path, monkeypatch):
|
|
"""HERMES_PLATFORM should win over HERMES_SESSION_PLATFORM."""
|
|
config = tmp_path / "config.yaml"
|
|
config.write_text(
|
|
"skills:\n"
|
|
" platform_disabled:\n"
|
|
" telegram:\n"
|
|
" - tg-skill\n"
|
|
" discord:\n"
|
|
" - discord-skill\n"
|
|
)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setenv("HERMES_PLATFORM", "telegram")
|
|
monkeypatch.setenv("HERMES_SESSION_PLATFORM", "discord")
|
|
|
|
from agent.skill_utils import get_disabled_skill_names
|
|
result = get_disabled_skill_names()
|
|
assert result == {"tg-skill"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _find_all_skills — disabled filtering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFindAllSkillsFiltering:
|
|
@patch("tools.skills_tool._get_disabled_skill_names", return_value={"my-skill"})
|
|
@patch("tools.skills_tool.skill_matches_platform", return_value=True)
|
|
def test_disabled_skill_excluded(self, mock_platform, mock_disabled, tmp_path, monkeypatch):
|
|
skill_dir = tmp_path / "my-skill"
|
|
skill_dir.mkdir()
|
|
skill_md = skill_dir / "SKILL.md"
|
|
skill_md.write_text("---\nname: my-skill\ndescription: A test skill\n---\nContent")
|
|
# Point SKILLS_DIR at the real tempdir so iter_skill_index_files
|
|
# (which uses os.walk) can actually find the file.
|
|
import tools.skills_tool as _st
|
|
import agent.skill_utils as _su
|
|
monkeypatch.setattr(_st, "SKILLS_DIR", tmp_path)
|
|
monkeypatch.setattr(_su, "get_external_skills_dirs", lambda: [])
|
|
from tools.skills_tool import _find_all_skills
|
|
skills = _find_all_skills()
|
|
assert not any(s["name"] == "my-skill" for s in skills)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_categories
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetCategories:
|
|
def test_extracts_unique_categories(self):
|
|
from hermes_cli.skills_config import _get_categories
|
|
skills = [
|
|
{"name": "a", "category": "mlops", "description": ""},
|
|
{"name": "b", "category": "coding", "description": ""},
|
|
{"name": "c", "category": "mlops", "description": ""},
|
|
]
|
|
cats = _get_categories(skills)
|
|
assert cats == ["coding", "mlops"]
|
|
|
|
def test_none_becomes_uncategorized(self):
|
|
from hermes_cli.skills_config import _get_categories
|
|
skills = [{"name": "a", "category": None, "description": ""}]
|
|
assert "uncategorized" in _get_categories(skills)
|