Files
hermes-agent/tests/tools/test_skills_runtime_cache.py

162 lines
6.7 KiB
Python

"""Runtime caches must not take ownership of updater-managed skill packages."""
import hashlib
import py_compile
import pytest
from tools import skills_sync as ss
from tools.skills_sync_bundled_ops import diff_bundled_skill, list_user_modified_bundled_skills
from tools.skills_sync_optional import _skill_file_list
def _legacy_hash(directory):
digest = hashlib.md5()
for path in sorted(directory.rglob("*")):
if path.is_file():
digest.update(str(path.relative_to(directory)).encode("utf-8"))
digest.update(path.read_bytes())
return digest.hexdigest()
def _write(root, rel, text):
target = root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding="utf-8")
return target
@pytest.fixture
def skill_tree(tmp_path, monkeypatch):
base = tmp_path / "profile"
bundled = base / "bundled"
src = bundled / "coding" / "demo"
_write(src, "SKILL.md", "---\nname: demo\n---\n# Demo\n")
_write(src, "scripts/helper.py", "ANSWER = 1\n")
skills = base / "skills"
monkeypatch.setattr(ss, "HERMES_HOME", base)
monkeypatch.setattr(ss, "SKILLS_DIR", skills)
monkeypatch.setattr(ss, "MANIFEST_FILE", skills / ".bundled_manifest")
monkeypatch.setattr(ss, "_get_bundled_dir", lambda: bundled)
monkeypatch.setattr(ss, "_get_optional_dir", lambda: base / "no-optional")
monkeypatch.setattr(ss, "_build_external_skill_index", set)
monkeypatch.setattr(ss, "_read_suppressed_names", set)
assert ss.sync_skills(quiet=True)["copied"] == ["demo"]
return src, skills / "coding" / "demo"
def test_real_python_compilation_does_not_freeze_update(skill_tree):
src, dest = skill_tree
origin = ss._read_manifest()["demo"]
py_compile.compile(str(dest / "scripts/helper.py"), doraise=True)
assert list((dest / "scripts/__pycache__").glob("*.pyc"))
assert ss._dir_hash(dest) == origin
assert list_user_modified_bundled_skills() == []
assert diff_bundled_skill("demo")["diffs"] == []
_write(src, "scripts/helper.py", "ANSWER = 2\n")
result = ss.sync_skills(quiet=True)
assert result["updated"] == ["demo"]
assert (dest / "scripts/helper.py").read_text() == "ANSWER = 2\n"
assert ss._read_manifest()["demo"] == ss._dir_hash(src)
@pytest.mark.parametrize("cache_path", [
"scripts/__pycache__/helper.cpython-311.pyc",
".pytest_cache/v/cache/nodeids",
"scripts/.mypy_cache/3.11/helper.data.json",
"scripts/.ruff_cache/0.15.0/abc",
"scripts/helper.pyc", "scripts/helper.pyo",
])
def test_runtime_cache_is_not_hash_or_diff_content(skill_tree, cache_path):
src, dest = skill_tree
_write(dest, cache_path, "generated")
assert ss._dir_hash(dest) == ss._dir_hash(src)
assert cache_path not in _skill_file_list(dest)
assert diff_bundled_skill("demo")["modified"] is False
@pytest.mark.parametrize("edited_path", ["SKILL.md", "scripts/helper.py", "references/notes.md", "cache/data.json", "scripts/bytecode_only.pyc"])
def test_genuine_edits_next_to_cache_are_preserved(skill_tree, edited_path):
src, dest = skill_tree
_write(dest, "scripts/__pycache__/helper.cpython-311.pyc", "generated")
_write(dest, edited_path, "user-owned content")
_write(src, "references/new-upstream.md", "new upstream documentation")
assert [entry["name"] for entry in list_user_modified_bundled_skills()] == ["demo"]
result = ss.sync_skills(quiet=True)
assert result["user_modified"] == ["demo"]
assert (dest / edited_path).read_text() == "user-owned content"
assert not (dest / "references/new-upstream.md").exists()
def test_source_cache_is_neither_seeded_nor_recorded(skill_tree):
src, dest = skill_tree
_write(src, "scripts/__pycache__/helper.cpython-311.pyc", "source generated")
_write(src, "SKILL.md", "---\nname: demo\n---\n# Changed upstream\n")
assert ss.sync_skills(quiet=True)["updated"] == ["demo"]
assert not (dest / "scripts/__pycache__").exists()
ss._rmtree_writable(dest)
ss._write_manifest({})
assert ss.sync_skills(quiet=True)["copied"] == ["demo"]
assert not (dest / "scripts/__pycache__").exists()
def test_legacy_cache_hash_migrates_only_with_matching_origin(skill_tree):
src, dest = skill_tree
_write(dest, "scripts/__pycache__/helper.cpython-311.pyc", "legacy generated")
ss._write_manifest({"demo": _legacy_hash(dest)})
assert list_user_modified_bundled_skills() == []
_write(src, "scripts/helper.py", "ANSWER = 2\n")
assert ss.sync_skills(quiet=True)["updated"] == ["demo"]
assert ss._read_manifest()["demo"] == ss._dir_hash(src)
def test_legacy_hash_mismatch_never_rebaselines_user_edits(skill_tree):
src, dest = skill_tree
cache = _write(dest, "scripts/__pycache__/helper.cpython-311.pyc", "legacy generated")
origin = _legacy_hash(dest)
ss._write_manifest({"demo": origin})
cache.unlink()
_write(dest, "scripts/helper.py", "user edit\n")
_write(src, "scripts/helper.py", "upstream edit\n")
assert ss.sync_skills(quiet=True)["user_modified"] == ["demo"]
assert ss._read_manifest()["demo"] == origin
assert (dest / "scripts/helper.py").read_text() == "user edit\n"
def test_clean_manifest_hash_is_backwards_compatible(skill_tree):
src, dest = skill_tree
assert ss._dir_hash(src) == _legacy_hash(src)
assert ss._dir_hash(dest) == _legacy_hash(dest)
def test_runtime_cache_does_not_hide_dotfile_edit(skill_tree):
src, dest = skill_tree
_write(dest, ".settings.json", "user settings")
_write(dest, ".pytest_cache/v/cache/nodeids", "[]")
assert ss._dir_hash(dest) != ss._dir_hash(src)
assert ".settings.json" in _skill_file_list(dest)
assert [e["path"] for e in diff_bundled_skill("demo")["diffs"]] == [".settings.json"]
def test_legacy_cache_origin_allows_rename_without_freezing(skill_tree):
src, dest = skill_tree
_write(dest, "scripts/__pycache__/helper.cpython-311.pyc", "legacy generated")
ss._write_manifest({"demo": _legacy_hash(dest)})
new_src = src.parent.parent / "recategorized" / "demo"
new_src.parent.mkdir(parents=True)
src.rename(new_src)
_write(new_src, "scripts/helper.py", "ANSWER = 3\n")
result = ss.sync_skills(quiet=True)
assert result["updated"] == ["demo"]
assert not dest.exists()
assert (ss._skills_dir() / "recategorized/demo/scripts/helper.py").read_text() == "ANSWER = 3\n"
def test_hash_filter_is_skill_relative(tmp_path):
# A Python environment/install prefix can itself contain a cache name.
src = tmp_path / "__pycache__" / "demo"
_write(src, "SKILL.md", "real skill content")
_write(src, "scripts/helper.py", "ANSWER = 1\n")
assert ss._dir_hash(src) == _legacy_hash(src)
assert set(_skill_file_list(src)) == {"SKILL.md", "scripts/helper.py"}