from __future__ import annotations import os from pathlib import Path import pytest from beaver.plugins.hashing import hash_plugin_skill_tree def test_skill_tree_hash_changes_when_supporting_file_changes(tmp_path: Path) -> None: root = tmp_path / "skill" root.mkdir() (root / "SKILL.md").write_text("# Skill\n", encoding="utf-8") (root / "templates").mkdir() template = root / "templates" / "report.md" template.write_text("v1", encoding="utf-8") first = hash_plugin_skill_tree(root) template.write_text("v2", encoding="utf-8") second = hash_plugin_skill_tree(root) assert first.skill_content_hash == second.skill_content_hash assert first.skill_tree_hash != second.skill_tree_hash def test_skill_tree_hash_changes_when_path_changes(tmp_path: Path) -> None: root = tmp_path / "skill" root.mkdir() (root / "SKILL.md").write_text("# Skill\n", encoding="utf-8") (root / "a.txt").write_text("same", encoding="utf-8") first = hash_plugin_skill_tree(root) (root / "b.txt").write_text((root / "a.txt").read_text(encoding="utf-8"), encoding="utf-8") (root / "a.txt").unlink() second = hash_plugin_skill_tree(root) assert first.skill_tree_hash != second.skill_tree_hash def test_skill_tree_hash_tracks_executable_bit_but_not_other_mode_bits(tmp_path: Path) -> None: root = tmp_path / "skill" root.mkdir() script = root / "script.sh" (root / "SKILL.md").write_text("# Skill\n", encoding="utf-8") script.write_text("#!/bin/sh\n", encoding="utf-8") script.chmod(0o644) first = hash_plugin_skill_tree(root) script.chmod(0o600) non_exec_changed = hash_plugin_skill_tree(root) script.chmod(0o700) exec_changed = hash_plugin_skill_tree(root) assert first.skill_tree_hash == non_exec_changed.skill_tree_hash assert first.skill_tree_hash != exec_changed.skill_tree_hash def test_skill_tree_hash_ignores_mtime_and_beaver_metadata(tmp_path: Path) -> None: root = tmp_path / "skill" root.mkdir() skill = root / "SKILL.md" skill.write_text("# Skill\n", encoding="utf-8") (root / "version.json").write_text('{"ignored": true}', encoding="utf-8") (root / "upstream.json").write_text('{"ignored": true}', encoding="utf-8") first = hash_plugin_skill_tree(root) os.utime(skill, (skill.stat().st_atime + 20, skill.stat().st_mtime + 20)) (root / "version.json").write_text('{"ignored": false}', encoding="utf-8") (root / "upstream.json").write_text('{"ignored": false}', encoding="utf-8") second = hash_plugin_skill_tree(root) assert first.skill_tree_hash == second.skill_tree_hash def test_skill_tree_hash_rejects_symlinks(tmp_path: Path) -> None: root = tmp_path / "skill" root.mkdir() (root / "SKILL.md").write_text("# Skill\n", encoding="utf-8") (root / "linked").symlink_to(root / "SKILL.md") with pytest.raises(ValueError, match="symlink"): hash_plugin_skill_tree(root)