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.
79 lines
2.9 KiB
Python
79 lines
2.9 KiB
Python
"""Tests for discovering and diffing user-modified bundled skills.
|
|
|
|
`hermes update` keeps (does not overwrite) bundled skills the user edited
|
|
locally, but historically only printed a *count* — there was no way to find
|
|
which skills, or see what changed. These tests cover the two helpers that close
|
|
that gap, exercising the real sync pipeline (no mocks of the comparison logic):
|
|
|
|
* ``list_user_modified_bundled_skills()`` — the discovery half of the exact
|
|
test the sync loop uses to decide what to skip.
|
|
* ``diff_bundled_skill()`` — a unified diff of the user copy vs the stock copy.
|
|
|
|
Revert already exists (``reset_bundled_skill``); the last test confirms it
|
|
clears the modified state so the two stay consistent.
|
|
"""
|
|
|
|
from contextlib import ExitStack
|
|
from unittest.mock import patch
|
|
|
|
from tools.skills_sync import (
|
|
sync_skills,
|
|
reset_bundled_skill,
|
|
list_user_modified_bundled_skills,
|
|
diff_bundled_skill,
|
|
)
|
|
|
|
|
|
def _make_bundled(tmp_path):
|
|
"""A fake bundled skills tree with one skill: category/foo."""
|
|
bundled = tmp_path / "bundled_skills"
|
|
foo = bundled / "category" / "foo"
|
|
foo.mkdir(parents=True)
|
|
(foo / "SKILL.md").write_text("---\nname: foo\n---\n# Foo Skill\n")
|
|
(foo / "helper.py").write_text("print('stock')\n")
|
|
return bundled
|
|
|
|
|
|
def _patches(bundled, skills_dir, manifest_file):
|
|
stack = ExitStack()
|
|
stack.enter_context(
|
|
patch("tools.skills_sync._get_bundled_dir", return_value=bundled)
|
|
)
|
|
stack.enter_context(
|
|
patch(
|
|
"tools.skills_sync._get_optional_dir",
|
|
return_value=bundled.parent / "optional-skills",
|
|
)
|
|
)
|
|
stack.enter_context(patch("tools.skills_sync.SKILLS_DIR", skills_dir))
|
|
stack.enter_context(patch("tools.skills_sync.MANIFEST_FILE", manifest_file))
|
|
return stack
|
|
|
|
|
|
def _env(tmp_path):
|
|
bundled = _make_bundled(tmp_path)
|
|
skills_dir = tmp_path / "user_skills"
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
return bundled, skills_dir, manifest_file
|
|
|
|
|
|
def test_pristine_skill_is_not_listed_as_modified(tmp_path):
|
|
bundled, skills_dir, manifest_file = _env(tmp_path)
|
|
with _patches(bundled, skills_dir, manifest_file):
|
|
sync_skills(quiet=True)
|
|
assert list_user_modified_bundled_skills() == []
|
|
|
|
|
|
def test_reset_clears_modified_state(tmp_path):
|
|
"""Revert (existing) and discovery (new) must agree: after reset, not modified."""
|
|
bundled, skills_dir, manifest_file = _env(tmp_path)
|
|
with _patches(bundled, skills_dir, manifest_file):
|
|
sync_skills(quiet=True)
|
|
(skills_dir / "category" / "foo" / "helper.py").write_text("print('mine')\n")
|
|
assert [m["name"] for m in list_user_modified_bundled_skills()] == ["foo"]
|
|
|
|
# Restore from the stock source, then it must no longer be flagged.
|
|
result = reset_bundled_skill("foo", restore=True)
|
|
assert result["ok"] is True
|
|
assert list_user_modified_bundled_skills() == []
|