"""Tests for tools/skills_hub.py — source adapters, lock file, taps, dedup logic.""" import json import time from typing import List, Optional from unittest.mock import patch, MagicMock import httpx import pytest from tools.skills_hub import ( GitHubAuth, GitHubSource, LobeHubSource, SkillsShSource, UrlSource, WellKnownSkillSource, OptionalSkillSource, SkillSource, SkillBundle, SkillMeta, HubLockFile, TapsManager, bundle_content_hash, check_for_skill_updates, create_source_router, parallel_search_sources, unified_search, append_audit_log, quarantine_bundle, _referenced_support_paths, ) # --------------------------------------------------------------------------- # GitHubSource._parse_frontmatter_quick # --------------------------------------------------------------------------- class TestParseFrontmatterQuick: def test_valid_frontmatter_including_nested_yaml(self): content = "---\nname: test-skill\ndescription: A test.\n---\n\n# Body\n" fm = GitHubSource._parse_frontmatter_quick(content) assert fm["name"] == "test-skill" assert fm["description"] == "A test." nested = "---\nname: test\nmetadata:\n hermes:\n tags: [a, b]\n---\n\nBody.\n" assert GitHubSource._parse_frontmatter_quick(nested)["metadata"]["hermes"]["tags"] == ["a", "b"] def test_degenerate_frontmatter_returns_empty(self): for content in ( "# Just a heading\nSome body text.\n", # no frontmatter at all "---\nname: test\nno closing here\n", # unterminated block "", # empty document "---\n: : : invalid{{\n---\n\nBody.\n", # unparseable YAML "---\n- just a list\n- of items\n---\n\nBody.\n", # non-dict YAML ): assert GitHubSource._parse_frontmatter_quick(content) == {}, repr(content) # --------------------------------------------------------------------------- # GitHubSource skills.sh.json grouping sidecar (category support) # --------------------------------------------------------------------------- class TestSkillsShGroupings: """Parsing + stamping of the skills.sh.json grouping sidecar. A tap can ship a repo-root ``skills.sh.json`` declaring category groupings; we flatten it to {skill_name: title} and stamp the title onto each SkillMeta's ``extra["category"]``. This is the generic cross-ecosystem mechanism behind NVIDIA-style categorization — not NVIDIA-specific. """ def test_parse_basic_groupings(self): content = json.dumps({ "$schema": "https://skills.sh/schemas/skills.sh.schema.json", "groupings": [ {"title": "Inference AI", "skills": ["dynamo-router", "dynamo-recipe"]}, {"title": "Decision Optimization", "skills": ["cuopt-developer"]}, ], }) mapping = GitHubSource._parse_skillsh_groupings(content) assert mapping == { "dynamo-router": "Inference AI", "dynamo-recipe": "Inference AI", "cuopt-developer": "Decision Optimization", } def test_list_skills_stamps_category_from_sidecar(self): auth = MagicMock() src = GitHubSource(auth=auth) meta = SkillMeta( name="cuopt-developer", description="d", source="github", identifier="NVIDIA/skills/skills/cuopt-developer", trust_level="trusted", ) contents = [{"type": "dir", "name": "cuopt-developer"}] groupings = {"cuopt-developer": "Decision Optimization"} resp = MagicMock() resp.status_code = 200 resp.json.return_value = contents with patch.object(src, "_read_cache", return_value=None), \ patch.object(src, "_write_cache"), \ patch.object(src, "_get_skillsh_groupings", return_value=groupings), \ patch.object(src, "inspect", return_value=meta), \ patch("tools.skills_hub.httpx.get", return_value=resp): skills = src._list_skills_in_repo("NVIDIA/skills", "skills/") assert len(skills) == 1 assert skills[0].extra["category"] == "Decision Optimization" # --------------------------------------------------------------------------- # GitHubSource.trust_level_for # --------------------------------------------------------------------------- class TestTrustLevelFor: def _source(self): auth = MagicMock(spec=GitHubAuth) return GitHubSource(auth=auth) def test_trusted_repo(self): src = self._source() # TRUSTED_REPOS is imported from skills_guard, test with known trusted repo from tools.skills_guard import TRUSTED_REPOS if TRUSTED_REPOS: repo = next(iter(TRUSTED_REPOS)) assert src.trust_level_for(f"{repo}/some-skill") == "trusted" def test_browseable_trusted_repos_have_taps(self): # General invariant covering all current and future trusted repos # that publish under a single `skills/`-style path. openai/skills # is the deliberate exception — it has two taps (`.curated/` and # `.system/`) — so we just assert membership not path equality. from tools.skills_guard import TRUSTED_REPOS tap_repos = {tap["repo"] for tap in GitHubSource.DEFAULT_TAPS} for repo in TRUSTED_REPOS: assert repo in tap_repos, ( f"Trusted repo {repo!r} is in TRUSTED_REPOS but missing " "from GitHubSource.DEFAULT_TAPS — its skills will not be " "browsable via `hermes skills browse`." ) class TestGitHubSourceFileFetch: def test_quotes_decoded_support_path_before_contents_api_fetch(self): src = GitHubSource(auth=MagicMock(spec=GitHubAuth)) src._github_get = MagicMock(return_value=None) src._fetch_file_bytes("owner/repo", "skill/references/foo#bar.md") assert src._github_get.call_args.args[0] == ( "https://api.github.com/repos/owner/repo/contents/" "skill/references/foo%23bar.md" ) # --------------------------------------------------------------------------- # SkillsShSource # --------------------------------------------------------------------------- class TestSkillsShSource: def _source(self): auth = MagicMock(spec=GitHubAuth) return SkillsShSource(auth=auth) @patch("tools.skills_hub._write_index_cache") @patch("tools.skills_hub._read_index_cache", return_value=None) @patch("tools.skills_hub.httpx.get") def test_search_maps_skills_sh_results_to_prefixed_identifiers(self, mock_get, _mock_read_cache, _mock_write_cache): mock_get.return_value = MagicMock( status_code=200, json=lambda: { "skills": [ { "id": "vercel-labs/agent-skills/vercel-react-best-practices", "skillId": "vercel-react-best-practices", "name": "vercel-react-best-practices", "installs": 207679, "source": "vercel-labs/agent-skills", } ] }, ) results = self._source().search("react", limit=5) assert len(results) == 1 assert results[0].source == "skills.sh" assert results[0].identifier == "skills-sh/vercel-labs/agent-skills/vercel-react-best-practices" assert "skills.sh" in results[0].description assert results[0].repo == "vercel-labs/agent-skills" assert results[0].path == "vercel-react-best-practices" assert results[0].extra["installs"] == 207679 @patch("tools.skills_hub._write_index_cache") @patch("tools.skills_hub._read_index_cache", return_value=None) @patch("tools.skills_hub.httpx.get") @patch.object(GitHubSource, "fetch") def test_fetch_falls_back_to_tree_search_for_deeply_nested_skills( self, mock_fetch, mock_get, _mock_read_cache, _mock_write_cache, ): """Skills in deeply nested dirs (e.g. cli-tool/components/skills/dev/my-skill/) are found via the GitHub Trees API when candidate paths and shallow scan fail.""" tree_entries = [ {"path": "README.md", "type": "blob"}, {"path": "cli-tool/components/skills/development/my-skill/SKILL.md", "type": "blob"}, {"path": "cli-tool/components/skills/development/other-skill/SKILL.md", "type": "blob"}, ] def _httpx_get_side_effect(url, **kwargs): resp = MagicMock() if "/api/search" in url: resp.status_code = 404 return resp if url.endswith("/contents/"): # Root listing for shallow scan — return empty so it falls through resp.status_code = 200 resp.json = lambda: [] return resp if "/contents/" in url: # All contents API calls fail (candidate paths miss) resp.status_code = 404 return resp if url.endswith("owner/repo"): # Repo info → default branch resp.status_code = 200 resp.json = lambda: {"default_branch": "main"} return resp if "/git/trees/main" in url: resp.status_code = 200 resp.json = lambda: {"tree": tree_entries} return resp # skills.sh detail page resp.status_code = 200 resp.text = "

my-skill

" return resp mock_get.side_effect = _httpx_get_side_effect resolved_bundle = SkillBundle( name="my-skill", files={"SKILL.md": "# My Skill"}, source="github", identifier="owner/repo/cli-tool/components/skills/development/my-skill", trust_level="community", ) mock_fetch.side_effect = lambda ident: resolved_bundle if "cli-tool/components" in ident else None bundle = self._source().fetch("skills-sh/owner/repo/my-skill") assert bundle is not None assert bundle.source == "skills.sh" assert bundle.files["SKILL.md"] == "# My Skill" # Verify the tree-resolved identifier was used for the final GitHub fetch mock_fetch.assert_any_call("owner/repo/cli-tool/components/skills/development/my-skill") class TestFindSkillInRepoTree: """Tests for GitHubSource._find_skill_in_repo_tree.""" def _source(self): auth = MagicMock(spec=GitHubAuth) auth.get_headers.return_value = {"Accept": "application/vnd.github.v3+json"} return GitHubSource(auth=auth) @patch("tools.skills_hub.httpx.get") def test_finds_deeply_nested_skill(self, mock_get): tree_entries = [ {"path": "README.md", "type": "blob"}, {"path": "cli-tool/components/skills/development/senior-backend/SKILL.md", "type": "blob"}, {"path": "cli-tool/components/skills/development/other/SKILL.md", "type": "blob"}, ] def _side_effect(url, **kwargs): resp = MagicMock() if url.endswith("/davila7/claude-code-templates"): resp.status_code = 200 resp.json = lambda: {"default_branch": "main"} elif "/git/trees/main" in url: resp.status_code = 200 resp.json = lambda: {"tree": tree_entries} else: resp.status_code = 404 return resp mock_get.side_effect = _side_effect result = self._source()._find_skill_in_repo_tree("davila7/claude-code-templates", "senior-backend") assert result == "davila7/claude-code-templates/cli-tool/components/skills/development/senior-backend" @patch("tools.skills_hub.httpx.get") def test_returns_none_when_repo_api_fails(self, mock_get): mock_get.return_value = MagicMock(status_code=404) result = self._source()._find_skill_in_repo_tree("owner/repo", "my-skill") assert result is None class TestWellKnownSkillSource: @pytest.fixture(autouse=True) def _allow_public_skill_fetches(self, monkeypatch): monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True) monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None) def _source(self): return WellKnownSkillSource() @patch("tools.skills_hub._write_index_cache") @patch("tools.skills_hub._read_index_cache", return_value=None) @patch("tools.skills_hub._ssrf_safe_http_get") def test_search_reads_index_from_well_known_url(self, mock_get, _mock_read_cache, _mock_write_cache): mock_get.return_value = MagicMock( status_code=200, json=lambda: { "skills": [ {"name": "git-workflow", "description": "Git rules", "files": ["SKILL.md"]}, {"name": "code-review", "description": "Review code", "files": ["SKILL.md", "references/checklist.md"]}, ] }, ) results = self._source().search("https://example.com/.well-known/skills/index.json", limit=10) assert [r.identifier for r in results] == [ "well-known:https://example.com/.well-known/skills/git-workflow", "well-known:https://example.com/.well-known/skills/code-review", ] assert all(r.source == "well-known" for r in results) @patch("tools.skills_hub._write_index_cache") @patch("tools.skills_hub._read_index_cache", return_value=None) @patch("tools.skills_hub.httpx.get") def test_fetch_rejects_unsafe_file_paths_from_well_known_endpoint(self, mock_get, _mock_read_cache, _mock_write_cache): def fake_get(url, *args, **kwargs): if url.endswith("/index.json"): return MagicMock(status_code=200, json=lambda: { "skills": [{ "name": "code-review", "description": "Review code", "files": ["SKILL.md", "../../../escape.txt"], }] }) if url.endswith("/code-review/SKILL.md"): return MagicMock(status_code=200, text="# Code Review\n") raise AssertionError(url) mock_get.side_effect = fake_get bundle = self._source().fetch("well-known:https://example.com/.well-known/skills/code-review") assert bundle is None class TestUrlSource: @pytest.fixture(autouse=True) def _allow_public_skill_fetches(self, monkeypatch): monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True) monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None) def _source(self): return UrlSource() @pytest.mark.parametrize( "glob_reference", [ "references/*.md", "references/file?", "references/file?.md", "references/agent.v?.md", "references/data.file?.md", "references/backup.2026?.md", "references/file?x.md", "references/agent.v?x.md", "references/file?[ab].md", "references/file??.md", "references/[ab].md", "references/%2A.md", "references/file%3F.md", "references/%5Bab%5D.md", ], ) def test_support_path_extraction_ignores_glob_shaped_prose( self, glob_reference ): skill_md = f""" See [the web reference](references/web.md) for details. Complex cases are documented under `{glob_reference}`. """ assert _referenced_support_paths(skill_md) == {"references/web.md"} @pytest.mark.parametrize( ("reference", "expected"), [ ("templates/report.md?raw=1", "templates/report.md"), ("references/LICENSE?download", "references/LICENSE"), ("references/LICENSE?raw", "references/LICENSE"), ("references/guide.md?view", "references/guide.md"), ("references/guide.md?inline", "references/guide.md"), ("references/guide.md?plain", "references/guide.md"), ("references/guide.md?preview-mode", "references/guide.md"), ("references/guide.md?view&inline&theme=dark", "references/guide.md"), ("references/LICENSE?plain&download=1", "references/LICENSE"), ("references/LICENSE?.well-known=1", "references/LICENSE"), ("references/guide.md#usage", "references/guide.md"), ("references/my%20guide.md", "references/my guide.md"), ("references/foo%23bar.md", "references/foo#bar.md"), ], ) def test_support_path_extraction_preserves_concrete_url_suffixes( self, reference, expected ): assert _referenced_support_paths(f"Use `{reference}`.") == {expected} # ── _matches ──────────────────────────────────────────────────────── def test_matches_bare_md_url(self): assert self._source()._matches("https://example.com/path/SKILL.md") is True def test_rejects_well_known_url(self): # Leave these for WellKnownSkillSource. assert self._source()._matches( "https://example.com/.well-known/skills/git-workflow/SKILL.md" ) is False assert self._source()._matches( "https://example.com/.well-known/skills/index.json" ) is False # ── inspect ───────────────────────────────────────────────────────── @patch("tools.skills_hub._ssrf_safe_http_get") @patch("tools.skills_hub.check_website_access", return_value=None) @patch("tools.skills_hub.is_safe_url", return_value=False) def test_inspect_blocks_private_url(self, _mock_safe, _mock_policy, mock_get): assert self._source().inspect("http://127.0.0.1/SKILL.md") is None mock_get.assert_not_called() # ── fetch ─────────────────────────────────────────────────────────── @patch("tools.skills_hub._ssrf_safe_http_get") def test_fetch_skips_missing_support_file_instead_of_aborting(self, mock_get): # One referenced support file 404s; the other is reachable. The # install should still succeed with the file that could be fetched, # rather than aborting the whole fetch (regression for a bug where # any single missing companion file failed the entire URL install). skill_md = ( "---\n" "name: my-skill\n" "description: Has support files.\n" "---\n\n" "See `references/good.md` and `references/missing.md`.\n" ) def _side_effect(url, **kwargs): if url.endswith("SKILL.md"): return MagicMock(status_code=200, text=skill_md) if url.endswith("references/good.md"): return MagicMock(status_code=200, content=b"good content") if url.endswith("references/missing.md"): return MagicMock(status_code=404) raise AssertionError(f"unexpected URL: {url}") mock_get.side_effect = _side_effect bundle = self._source().fetch("https://example.com/my-skill/SKILL.md") assert bundle is not None assert bundle.name == "my-skill" assert bundle.files["SKILL.md"] == skill_md assert bundle.files["references/good.md"] == b"good content" assert "references/missing.md" not in bundle.files @patch("tools.skills_hub._ssrf_safe_http_get") @patch("tools.skills_hub.check_website_access", return_value=None) @patch("tools.skills_hub.is_safe_url", side_effect=[True, False]) def test_fetch_blocks_redirect_to_private_url(self, _mock_safe, _mock_policy, mock_get): redirect = MagicMock(status_code=302) redirect.headers = {"location": "http://127.0.0.1/private/SKILL.md"} mock_get.return_value = redirect assert self._source().fetch("https://example.com/SKILL.md") is None assert mock_get.call_count == 1 @patch("tools.skills_hub._ssrf_safe_http_get") @patch("tools.skills_hub.check_website_access", return_value=None) @patch("tools.skills_hub.is_safe_url", return_value=False) def test_fetch_blocks_private_url(self, _mock_safe, _mock_policy, mock_get): assert self._source().fetch("http://127.0.0.1/SKILL.md") is None mock_get.assert_not_called() def test_is_valid_skill_name_rejects_sentinel_and_garbage(self): invalid = [ "", "SKILL", "skill", "README", "readme", "INDEX", "index", "unnamed-skill", "../evil", "a/b", "has space", "has.dot", "-leading-dash", "1-leading-digit", None, 123, ["list"], ] for name in invalid: assert not UrlSource._is_valid_skill_name(name), f"should reject {name!r}" class TestCheckForSkillUpdates: def test_bundle_content_hash_matches_installed_content_hash(self, tmp_path): from tools.skills_guard import content_hash bundle = SkillBundle( name="demo-skill", files={ "SKILL.md": "same content", "references/checklist.md": "- [ ] security\n", }, source="github", identifier="owner/repo/demo-skill", trust_level="community", ) skill_dir = tmp_path / "demo-skill" skill_dir.mkdir() (skill_dir / "SKILL.md").write_text("same content") (skill_dir / "references").mkdir() (skill_dir / "references" / "checklist.md").write_text("- [ ] security\n") assert bundle_content_hash(bundle) == content_hash(skill_dir) def test_reports_update_when_remote_hash_differs(self): lock = MagicMock() lock.list_installed.return_value = [{ "name": "demo-skill", "source": "github", "identifier": "owner/repo/demo-skill", "content_hash": "oldhash", "install_path": "demo-skill", }] source = MagicMock() source.source_id.return_value = "github" source.fetch.return_value = SkillBundle( name="demo-skill", files={"SKILL.md": "new content"}, source="github", identifier="owner/repo/demo-skill", trust_level="community", ) results = check_for_skill_updates(lock=lock, sources=[source]) assert len(results) == 1 assert results[0]["name"] == "demo-skill" assert results[0]["status"] == "update_available" class TestCreateSourceRouter: def test_url_source_runs_before_github_source(self): # UrlSource must win over GitHubSource when both could claim a URL. sources = create_source_router(auth=MagicMock(spec=GitHubAuth)) url_idx = next(i for i, src in enumerate(sources) if isinstance(src, UrlSource)) gh_idx = next(i for i, src in enumerate(sources) if isinstance(src, GitHubSource)) assert url_idx < gh_idx # --------------------------------------------------------------------------- # HubLockFile # --------------------------------------------------------------------------- class TestHubLockFile: def test_load_corrupt_json(self, tmp_path): lock_file = tmp_path / "lock.json" lock_file.write_text("not json{{{") lock = HubLockFile(path=lock_file) data = lock.load() assert data == {"version": 1, "installed": {}} def test_list_installed(self, tmp_path): lock = HubLockFile(path=tmp_path / "lock.json") lock.record_install( name="s1", source="github", identifier="x", trust_level="trusted", scan_verdict="pass", skill_hash="h1", install_path="s1", files=["SKILL.md"], ) lock.record_install( name="s2", source="clawhub", identifier="y", trust_level="community", scan_verdict="pass", skill_hash="h2", install_path="s2", files=["SKILL.md"], ) installed = lock.list_installed() assert len(installed) == 2 names = {e["name"] for e in installed} assert names == {"s1", "s2"} # --------------------------------------------------------------------------- # TapsManager # --------------------------------------------------------------------------- class TestTapsManager: def test_load_corrupt_json(self, tmp_path): taps_file = tmp_path / "taps.json" taps_file.write_text("bad json") mgr = TapsManager(path=taps_file) assert mgr.load() == [] def test_remove_existing_tap(self, tmp_path): mgr = TapsManager(path=tmp_path / "taps.json") mgr.add("owner/repo") assert mgr.remove("owner/repo") is True assert mgr.load() == [] # --------------------------------------------------------------------------- # LobeHubSource._convert_to_skill_md # --------------------------------------------------------------------------- class TestConvertToSkillMd: def test_basic_conversion(self): agent_data = { "identifier": "test-agent", "meta": { "title": "Test Agent", "description": "A test agent.", "tags": ["testing", "demo"], }, "config": { "systemRole": "You are a helpful test agent.", }, } result = LobeHubSource._convert_to_skill_md(agent_data) assert "---" in result assert "name: test-agent" in result assert "description: A test agent." in result assert "tags: [testing, demo]" in result assert "# Test Agent" in result assert "You are a helpful test agent." in result # --------------------------------------------------------------------------- # unified_search — dedup logic # --------------------------------------------------------------------------- class TestUnifiedSearchDedup: def _make_source(self, source_id, results): """Create a mock SkillSource that returns fixed results.""" src = MagicMock() src.source_id.return_value = source_id src.search.return_value = results return src def test_dedup_keeps_first_seen(self): # Same identifier from two sources — only the first (community) is kept when equal trust. s1 = SkillMeta(name="skill", description="from A", source="a", identifier="shared/skill", trust_level="community") s2 = SkillMeta(name="skill", description="from B", source="b", identifier="shared/skill", trust_level="community") src_a = self._make_source("a", [s1]) src_b = self._make_source("b", [s2]) results = unified_search("skill", [src_a, src_b]) assert len(results) == 1 assert results[0].description == "from A" def test_dedup_prefers_builtin_over_trusted(self): """Regression: builtin must not be overwritten by trusted.""" builtin = SkillMeta(name="skill", description="builtin", source="a", identifier="shared/skill", trust_level="builtin") trusted = SkillMeta(name="skill", description="trusted", source="b", identifier="shared/skill", trust_level="trusted") src_a = self._make_source("a", [builtin]) src_b = self._make_source("b", [trusted]) results = unified_search("skill", [src_a, src_b]) assert len(results) == 1 assert results[0].trust_level == "builtin" def test_source_error_handled(self): failing = MagicMock() failing.source_id.return_value = "fail" failing.search.side_effect = RuntimeError("boom") ok = self._make_source("ok", [ SkillMeta(name="s1", description="d", source="ok", identifier="x", trust_level="community") ]) results = unified_search("query", [failing, ok]) assert len(results) == 1 # --------------------------------------------------------------------------- # GitHub tap provider labeling + index search/filter # --------------------------------------------------------------------------- class TestGithubProviderLabeling: def test_inspect_stamps_provider_in_extra(self): gs = GitHubSource(auth=GitHubAuth()) skill_md = ( "---\nname: accelerated-computing-cudf\n" "description: NVIDIA cuDF GPU DataFrames.\n---\n# body\n" ) gs._fetch_file_content = lambda repo, path: skill_md meta = gs.inspect("NVIDIA/skills/skills/accelerated-computing-cudf") assert meta is not None # source stays "github" (no churn to dedup/floor/skip logic) ... assert meta.source == "github" # ... but the per-tap provider label rides along in extra assert meta.extra.get("provider") == "NVIDIA" def _make_index_source(skills): """Build a HermesIndexSource pre-loaded with a fixed skill list.""" from tools.skills_hub import HermesIndexSource src = HermesIndexSource(auth=GitHubAuth()) src._index = {"skills": skills} src._loaded = True return src class TestHermesIndexSearch: def test_search_matches_identifier_and_provider(self): # NVIDIA skill whose name/description does NOT contain "nvidia" — only # the identifier and the provider label do. The old substring-only # search over name/description/tags would miss it entirely. skills = [ { "name": "accelerated-computing-cudf", "description": "GPU DataFrames.", "source": "github", "identifier": "NVIDIA/skills/skills/accelerated-computing-cudf", "tags": [], "extra": {"provider": "NVIDIA"}, }, { "name": "unrelated", "description": "nothing here", "source": "clawhub", "identifier": "clawhub/unrelated", "tags": [], }, ] src = _make_index_source(skills) hits = src.search("nvidia", limit=25) ids = [h.identifier for h in hits] assert "NVIDIA/skills/skills/accelerated-computing-cudf" in ids assert "clawhub/unrelated" not in ids class TestProviderFilter: def test_filter_results_by_provider_narrows_exactly(self): from tools.skills_hub import _filter_results_by_provider results = [ SkillMeta(name="a", description="", source="github", identifier="NVIDIA/skills/a", trust_level="trusted", extra={"provider": "NVIDIA"}), SkillMeta(name="b", description="", source="github", identifier="openai/skills/b", trust_level="trusted", extra={"provider": "OpenAI"}), SkillMeta(name="c", description="", source="official", identifier="official/c", trust_level="builtin"), ] nv = _filter_results_by_provider(results, "nvidia") assert [r.identifier for r in nv] == ["NVIDIA/skills/a"] oai = _filter_results_by_provider(results, "openai") assert [r.identifier for r in oai] == ["openai/skills/b"] def test_unified_search_provider_filter_keeps_index_source(self): # A provider filter must NOT be treated as a real source id (which would # exclude every source and return nothing). It selects sources like # "all", then narrows the merged results by provider. nv = SkillMeta(name="cuda", description="gpu", source="github", identifier="NVIDIA/skills/cuda", trust_level="trusted", extra={"provider": "NVIDIA"}) other = SkillMeta(name="cuda-clone", description="gpu", source="clawhub", identifier="clawhub/cuda-clone", trust_level="community") src = MagicMock() src.source_id.return_value = "hermes-index" src.is_available = True src.search.return_value = [nv, other] results = unified_search("cuda", [src], source_filter="nvidia", limit=25) assert [r.identifier for r in results] == ["NVIDIA/skills/cuda"] # --------------------------------------------------------------------------- # append_audit_log # --------------------------------------------------------------------------- class TestAppendAuditLog: def test_creates_log_entry(self, tmp_path): log_file = tmp_path / "audit.log" with patch("tools.skills_hub.AUDIT_LOG", log_file): append_audit_log("INSTALL", "test-skill", "github", "trusted", "pass") content = log_file.read_text() assert "INSTALL" in content assert "test-skill" in content assert "github:trusted" in content assert "pass" in content # --------------------------------------------------------------------------- # Official skills / binary assets # --------------------------------------------------------------------------- class TestOptionalSkillSourceMetadata: def test_scan_all_emits_repo_root_relative_metadata(self, tmp_path): optional_root = tmp_path / "optional-skills" skill_dir = optional_root / "finance" / "3-statement-model" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( "---\nname: 3-statement-model\ndescription: test\n---\n\nBody\n", encoding="utf-8", ) src = OptionalSkillSource() src._optional_dir = optional_root meta = src.inspect("official/finance/3-statement-model") assert meta is not None assert meta.repo == "NousResearch/hermes-agent" assert meta.path == "optional-skills/finance/3-statement-model" def test_scan_all_accepts_install_prefix_but_rejects_nested_support_skills(self, tmp_path): optional_root = tmp_path / "venv" / "lib" / "site-packages" / "optional-skills" real = optional_root / "research" / "real-skill" nested = real / "references" / "archived-skill" nested.mkdir(parents=True) (real / "SKILL.md").write_text( "---\nname: real-skill\ndescription: real\n---\n", encoding="utf-8" ) (nested / "SKILL.md").write_text( "---\nname: archived-skill\ndescription: nested\n---\n", encoding="utf-8" ) src = OptionalSkillSource() src._optional_dir = optional_root assert [meta.name for meta in src._scan_all()] == ["real-skill"] assert src._find_skill_dir("archived-skill") is None class TestOptionalSkillSourceBinaryAssets: def test_fetch_preserves_binary_assets(self, tmp_path): optional_root = tmp_path / "optional-skills" skill_dir = optional_root / "mlops" / "models" / "neutts" (skill_dir / "assets" / "neutts-cli" / "samples").mkdir(parents=True) (skill_dir / "SKILL.md").write_text( "---\nname: neutts\ndescription: test\n---\n\nBody\n", encoding="utf-8", ) wav_bytes = b"RIFF\x00\x01fakewav" (skill_dir / "assets" / "neutts-cli" / "samples" / "jo.wav").write_bytes( wav_bytes ) (skill_dir / "assets" / "neutts-cli" / "samples" / "jo.txt").write_text( "hello\n", encoding="utf-8" ) pycache_dir = skill_dir / "assets" / "neutts-cli" / "src" / "neutts_cli" / "__pycache__" pycache_dir.mkdir(parents=True) (pycache_dir / "cli.cpython-312.pyc").write_bytes(b"junk") src = OptionalSkillSource() src._optional_dir = optional_root bundle = src.fetch("official/mlops/models/neutts") assert bundle is not None assert bundle.files["assets/neutts-cli/samples/jo.wav"] == wav_bytes assert bundle.files["assets/neutts-cli/samples/jo.txt"] == b"hello\n" assert "assets/neutts-cli/src/neutts_cli/__pycache__/cli.cpython-312.pyc" not in bundle.files def test_fetch_rejects_sibling_directory_traversal(self, tmp_path): optional_root = tmp_path / "optional-skills" sibling_skill_dir = tmp_path / "optional-skills-escape" / "pwned" optional_root.mkdir() sibling_skill_dir.mkdir(parents=True) (sibling_skill_dir / "SKILL.md").write_text( "---\nname: pwned\ndescription: traversal\n---\n\nBody\n", encoding="utf-8", ) src = OptionalSkillSource() src._optional_dir = optional_root bundle = src.fetch("official/../optional-skills-escape/pwned") assert bundle is None class TestOptionalSkillSourceLiveRepoFallback: """Skills merged to main after the local install was cut must still be searchable and installable without `hermes update` (live-repo fallback).""" def _make_source(self, tmp_path, remote_dirs): optional_root = tmp_path / "optional-skills" optional_root.mkdir(exist_ok=True) src = OptionalSkillSource() src._optional_dir = optional_root src._remote_dirs = dict.fromkeys(remote_dirs, True) return src @staticmethod def _fake_github_with_tree(remote_dirs, extra_files=()): """MagicMock GitHubSource whose repo tree contains each skill dir's SKILL.md plus any extra files, served byte-exact by _fetch_file_bytes.""" entries = [] contents = {} for rel_dir in remote_dirs: p = f"optional-skills/{rel_dir}/SKILL.md" entries.append({"type": "blob", "path": p, "mode": "100644"}) contents[p] = b"---\nname: " + rel_dir.rsplit("/", 1)[-1].encode() + b"\n---\nBody" for rel_path, data in extra_files: entries.append({"type": "blob", "path": rel_path, "mode": "100644"}) contents[rel_path] = data fake = MagicMock() fake._get_repo_tree.return_value = ("main", entries) fake._fetch_file_bytes.side_effect = lambda repo, path: contents.get(path) return fake def test_fetch_falls_back_to_live_repo_when_missing_locally(self, tmp_path): src = self._make_source(tmp_path, ["software-development/ast-grep"]) src._github = self._fake_github_with_tree( ["software-development/ast-grep"], extra_files=[ ("optional-skills/software-development/ast-grep/install.sh", b"#!/bin/sh\n"), ("optional-skills/software-development/ast-grep/LICENSE", b"MIT"), ], ) bundle = src.fetch("official/software-development/ast-grep") assert bundle is not None # Provenance is rewritten to official/builtin assert bundle.source == "official" assert bundle.identifier == "official/software-development/ast-grep" assert bundle.trust_level == "builtin" # FULL directory arrives — including root-level files GitHubSource.fetch drops assert bundle.files["install.sh"] == b"#!/bin/sh\n" assert bundle.files["LICENSE"] == b"MIT" def test_fetch_bare_name_resolves_via_remote_tree(self, tmp_path): src = self._make_source(tmp_path, ["software-development/ast-grep"]) src._github = self._fake_github_with_tree(["software-development/ast-grep"]) bundle = src.fetch("official/ast-grep") assert bundle is not None assert bundle.identifier == "official/software-development/ast-grep" def test_fetch_ambiguous_bare_name_refuses(self, tmp_path): src = self._make_source( tmp_path, ["security/scanner", "devops/scanner"] ) fake_github = MagicMock() src._github = fake_github assert src.fetch("official/scanner") is None fake_github.fetch.assert_not_called() def test_local_checkout_wins_over_remote(self, tmp_path): src = self._make_source(tmp_path, ["research/local-skill"]) skill_dir = src._optional_dir / "research" / "local-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( "---\nname: local-skill\ndescription: local\n---\nBody", encoding="utf-8", ) fake_github = MagicMock() src._github = fake_github bundle = src.fetch("official/research/local-skill") assert bundle is not None fake_github.fetch.assert_not_called() def test_fallback_rejects_traversal_rel(self, tmp_path): src = self._make_source(tmp_path, ["security/whatever"]) fake_github = MagicMock() src._github = fake_github assert src._fetch_from_live_repo("../../etc/passwd") is None fake_github.fetch.assert_not_called() def test_search_surfaces_remote_only_skills(self, tmp_path): src = self._make_source(tmp_path, ["software-development/ast-grep"]) results = src.search("ast-grep") assert any( r.identifier == "official/software-development/ast-grep" and r.trust_level == "builtin" for r in results ) def test_inspect_surfaces_remote_only_skill(self, tmp_path): src = self._make_source(tmp_path, ["software-development/ast-grep"]) meta = src.inspect("official/software-development/ast-grep") assert meta is not None assert meta.repo == "NousResearch/hermes-agent" assert meta.path == "optional-skills/software-development/ast-grep" def test_offline_degrades_to_local_only(self, tmp_path): src = self._make_source(tmp_path, []) fake_github = MagicMock() src._github = fake_github assert src.fetch("official/never-heard-of-it") is None assert src.search("never-heard-of-it") == [] class TestQuarantineBundleBinaryAssets: def test_quarantine_bundle_writes_binary_files(self, tmp_path): import tools.skills_hub as hub hub_dir = tmp_path / "skills" / ".hub" with patch.object(hub, "SKILLS_DIR", tmp_path / "skills"), \ patch.object(hub, "HUB_DIR", hub_dir), \ patch.object(hub, "LOCK_FILE", hub_dir / "lock.json"), \ patch.object(hub, "QUARANTINE_DIR", hub_dir / "quarantine"), \ patch.object(hub, "AUDIT_LOG", hub_dir / "audit.log"), \ patch.object(hub, "TAPS_FILE", hub_dir / "taps.json"), \ patch.object(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache"): bundle = SkillBundle( name="neutts", files={ "SKILL.md": "---\nname: neutts\n---\n", "assets/neutts-cli/samples/jo.wav": b"RIFF\x00\x01fakewav", }, source="official", identifier="official/mlops/models/neutts", trust_level="builtin", ) q_path = quarantine_bundle(bundle) assert (q_path / "SKILL.md").read_text(encoding="utf-8").startswith("---") assert (q_path / "assets" / "neutts-cli" / "samples" / "jo.wav").read_bytes() == b"RIFF\x00\x01fakewav" def test_quarantine_bundle_rejects_traversal_file_paths(self, tmp_path): import tools.skills_hub as hub hub_dir = tmp_path / "skills" / ".hub" with patch.object(hub, "SKILLS_DIR", tmp_path / "skills"), \ patch.object(hub, "HUB_DIR", hub_dir), \ patch.object(hub, "LOCK_FILE", hub_dir / "lock.json"), \ patch.object(hub, "QUARANTINE_DIR", hub_dir / "quarantine"), \ patch.object(hub, "AUDIT_LOG", hub_dir / "audit.log"), \ patch.object(hub, "TAPS_FILE", hub_dir / "taps.json"), \ patch.object(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache"): bundle = SkillBundle( name="demo", files={ "SKILL.md": "---\nname: demo\n---\n", "../../../escape.txt": "owned", }, source="well-known", identifier="well-known:https://example.com/.well-known/skills/demo", trust_level="community", ) with pytest.raises(ValueError, match="Unsafe bundle file path"): quarantine_bundle(bundle) assert not (tmp_path / "skills" / "escape.txt").exists() def test_quarantine_bundle_rejects_absolute_file_paths(self, tmp_path): import tools.skills_hub as hub hub_dir = tmp_path / "skills" / ".hub" absolute_target = tmp_path / "outside.txt" with patch.object(hub, "SKILLS_DIR", tmp_path / "skills"), \ patch.object(hub, "HUB_DIR", hub_dir), \ patch.object(hub, "LOCK_FILE", hub_dir / "lock.json"), \ patch.object(hub, "QUARANTINE_DIR", hub_dir / "quarantine"), \ patch.object(hub, "AUDIT_LOG", hub_dir / "audit.log"), \ patch.object(hub, "TAPS_FILE", hub_dir / "taps.json"), \ patch.object(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache"): bundle = SkillBundle( name="demo", files={ "SKILL.md": "---\nname: demo\n---\n", str(absolute_target): "owned", }, source="well-known", identifier="well-known:https://example.com/.well-known/skills/demo", trust_level="community", ) with pytest.raises(ValueError, match="Unsafe bundle file path"): quarantine_bundle(bundle) assert not absolute_target.exists() def test_quarantine_bundle_rejects_ads_colon_file_paths(self, tmp_path): """F-02: a bundle member with a colon in a component (``file.py:payload``) is an NTFS Alternate Data Stream marker — the visible file passes ``rglob``-based review while hidden, scanner-invisible bytes are written into it. Reject it before it reaches disk, on any OS.""" import tools.skills_hub as hub hub_dir = tmp_path / "skills" / ".hub" with patch.object(hub, "SKILLS_DIR", tmp_path / "skills"), \ patch.object(hub, "HUB_DIR", hub_dir), \ patch.object(hub, "LOCK_FILE", hub_dir / "lock.json"), \ patch.object(hub, "QUARANTINE_DIR", hub_dir / "quarantine"), \ patch.object(hub, "AUDIT_LOG", hub_dir / "audit.log"), \ patch.object(hub, "TAPS_FILE", hub_dir / "taps.json"), \ patch.object(hub, "INDEX_CACHE_DIR", hub_dir / "index-cache"): bundle = SkillBundle( name="demo", files={ "SKILL.md": "---\nname: demo\n---\n", "scripts/helper.py:payload": "print(24680)", }, source="well-known", identifier="well-known:https://example.com/.well-known/skills/demo", trust_level="community", ) with pytest.raises(ValueError, match="Unsafe bundle file path"): quarantine_bundle(bundle) assert not (tmp_path / "skills" / "scripts").exists() def test_normalize_bundle_path_rejects_colon_anywhere(self): """The colon guard covers the whole class, not just ``helper.py:payload``: a colon in any component (leading drive letter, mid-path, or bare) is rejected, while ordinary portable paths still normalize.""" from tools.skills_hub import _normalize_bundle_path rejected = ( "scripts/helper.py:payload", # trailing-component ADS marker "scripts/a:b.py", # mid-component colon "a:b/scripts/helper.py", # leading-component colon "scripts/helper.py:", # empty stream name "C:", # bare Windows drive letter "C:/Windows/System32", # drive-qualified absolute-ish path ) for bad in rejected: with pytest.raises(ValueError, match="Unsafe bundle file path"): _normalize_bundle_path(bad, field_name="bundle file path", allow_nested=True) # Legitimate portable paths are unaffected. assert _normalize_bundle_path( "scripts/helper.py", field_name="bundle file path", allow_nested=True ) == "scripts/helper.py" assert _normalize_bundle_path( "assets/data/sample.wav", field_name="bundle file path", allow_nested=True ) == "assets/data/sample.wav" # --------------------------------------------------------------------------- # GitHubSource._download_directory — tree API + fallback (#2940) # --------------------------------------------------------------------------- class TestDownloadDirectoryViaTree: """Tests for the Git Trees API path in _download_directory.""" def _source(self): auth = MagicMock(spec=GitHubAuth) auth.get_headers.return_value = {} return GitHubSource(auth=auth) @patch.object(GitHubSource, "_fetch_file_content") @patch("tools.skills_hub.httpx.get") def test_tree_api_downloads_subdirectories(self, mock_get, mock_fetch): """Tree API returns files from nested subdirectories.""" repo_resp = MagicMock(status_code=200, json=lambda: {"default_branch": "main"}) tree_resp = MagicMock(status_code=200, json=lambda: { "truncated": False, "tree": [ {"type": "blob", "path": "skills/my-skill/SKILL.md"}, {"type": "blob", "path": "skills/my-skill/scripts/run.py"}, {"type": "blob", "path": "skills/my-skill/references/api.md"}, {"type": "tree", "path": "skills/my-skill/scripts"}, {"type": "blob", "path": "other/file.txt"}, ], }) mock_get.side_effect = [repo_resp, tree_resp] mock_fetch.side_effect = lambda repo, path: f"content-of-{path}" src = self._source() files = src._download_directory("owner/repo", "skills/my-skill") assert "SKILL.md" in files assert "scripts/run.py" in files assert "references/api.md" in files assert "other/file.txt" not in files # outside target path assert len(files) == 3 @patch.object(GitHubSource, "_download_directory_recursive", return_value={"SKILL.md": "# ok"}) @patch("tools.skills_hub.httpx.get") def test_falls_back_on_truncated_tree(self, mock_get, mock_fallback): """When tree is truncated, fall back to recursive Contents API.""" repo_resp = MagicMock(status_code=200, json=lambda: {"default_branch": "main"}) tree_resp = MagicMock(status_code=200, json=lambda: {"truncated": True, "tree": []}) mock_get.side_effect = [repo_resp, tree_resp] src = self._source() files = src._download_directory("owner/repo", "skills/my-skill") assert files == {"SKILL.md": "# ok"} mock_fallback.assert_called_once_with("owner/repo", "skills/my-skill") class TestDownloadDirectoryRecursive: """Tests for the Contents API fallback path.""" def _source(self): auth = MagicMock(spec=GitHubAuth) auth.get_headers.return_value = {} return GitHubSource(auth=auth) @patch.object(GitHubSource, "_fetch_file_content") @patch("tools.skills_hub.httpx.get") def test_recursive_downloads_subdirectories(self, mock_get, mock_fetch): """Contents API recursion includes subdirectories.""" root_resp = MagicMock(status_code=200, json=lambda: [ {"name": "SKILL.md", "type": "file", "path": "skill/SKILL.md"}, {"name": "scripts", "type": "dir", "path": "skill/scripts"}, ]) sub_resp = MagicMock(status_code=200, json=lambda: [ {"name": "run.py", "type": "file", "path": "skill/scripts/run.py"}, ]) mock_get.side_effect = [root_resp, sub_resp] mock_fetch.side_effect = lambda repo, path: f"content-of-{path}" src = self._source() files = src._download_directory_recursive("owner/repo", "skill") assert "SKILL.md" in files assert "scripts/run.py" in files # --------------------------------------------------------------------------- # Install-path safety (lock-file → uninstall rmtree boundary) # --------------------------------------------------------------------------- class TestInstallPathSafety: """Guard the lock-file → ``uninstall_skill`` rmtree path. The destructive boundary is ``shutil.rmtree(SKILLS_DIR / install_path)``. Lock-file ``install_path`` values that are absolute, contain ``..``, point at the skills root itself, or are redirected via a symlink/junction inside ``skills/`` must be rejected before they reach rmtree. """ @pytest.fixture def isolated_skills_dir(self, tmp_path): import tools.skills_hub as hub skills_dir = tmp_path / "skills" skills_dir.mkdir() # SKILLS_DIR is a PEP 562 dynamic attribute (resolved per-access via # module __getattr__). monkeypatch.setattr must NOT be used here: it # captures __getattr__'s live-resolved Path as the "original" value # and re-installs it as a REAL module attribute on teardown, which # permanently shadows dynamic resolution — every later test in the # process then sees this test's tmp dir as the skills root. Set the # override attribute directly and delete it on teardown so the # module returns to dynamic resolution. setattr(hub, "SKILLS_DIR", skills_dir) try: yield skills_dir finally: try: delattr(hub, "SKILLS_DIR") except AttributeError: pass @pytest.fixture def patch_lock_file(self, monkeypatch): """Redirect HubLockFile's default path to a test-controlled file. HubLockFile.__init__ captures LOCK_FILE as a default arg at class definition time, so monkeypatching the module-level LOCK_FILE doesn't affect later HubLockFile() calls. Patch __defaults__ instead. """ def _apply(lock_path): monkeypatch.setattr(HubLockFile.__init__, "__defaults__", (lock_path,)) return _apply def test_record_install_rejects_unsafe_paths(self, tmp_path): """record_install must reject malformed install_path values at write time.""" lock = HubLockFile(path=tmp_path / "lock.json") for bad_install_path in ( "", ".", "..", "../../etc/passwd", "/etc/passwd", "skills/../../tmp", "C:/Windows/System32", ): with pytest.raises(ValueError, match="Unsafe"): lock.record_install( name="evil", source="github", identifier="x", trust_level="trusted", scan_verdict="pass", skill_hash="h1", install_path=bad_install_path, files=["SKILL.md"], ) def test_uninstall_rejects_poisoned_absolute_path(self, tmp_path, isolated_skills_dir, patch_lock_file): """Hand-edited lock.json with absolute install_path must not delete anything.""" from tools.skills_hub import uninstall_skill lock_path = tmp_path / "lock.json" target = tmp_path / "victim" target.mkdir() (target / "file.txt").write_text("important") # Bypass record_install's validator to simulate a poisoned lock file. lock_path.write_text(json.dumps({ "installed": { "evil": { "source": "github", "identifier": "x", "trust_level": "trusted", "scan_verdict": "pass", "content_hash": "h", "install_path": str(target), "files": [], "metadata": {}, "installed_at": "now", "updated_at": "now", } } })) patch_lock_file(lock_path) ok, msg = uninstall_skill("evil") assert ok is False assert "Unsafe" in msg or "Refusing" in msg assert target.exists() assert (target / "file.txt").read_text() == "important" def test_uninstall_rejects_traversal(self, tmp_path, isolated_skills_dir, patch_lock_file): from tools.skills_hub import uninstall_skill lock_path = tmp_path / "lock.json" sibling = tmp_path / "sibling" sibling.mkdir() (sibling / "data").write_text("nope") lock_path.write_text(json.dumps({ "installed": { "evil": { "source": "github", "identifier": "x", "trust_level": "trusted", "scan_verdict": "pass", "content_hash": "h", "install_path": "../sibling", "files": [], "metadata": {}, "installed_at": "now", "updated_at": "now", } } })) patch_lock_file(lock_path) ok, msg = uninstall_skill("evil") assert ok is False assert sibling.exists() assert (sibling / "data").read_text() == "nope" def test_uninstall_rejects_empty_install_path(self, tmp_path, isolated_skills_dir, patch_lock_file): """Empty install_path resolves to SKILLS_DIR itself — must be refused.""" from tools.skills_hub import uninstall_skill # Put a sibling skill alongside to prove rmtree doesn't fire. (isolated_skills_dir / "bystander").mkdir() (isolated_skills_dir / "bystander" / "SKILL.md").write_text("safe") lock_path = tmp_path / "lock.json" lock_path.write_text(json.dumps({ "installed": { "evil": { "source": "github", "identifier": "x", "trust_level": "trusted", "scan_verdict": "pass", "content_hash": "h", "install_path": "", "files": [], "metadata": {}, "installed_at": "now", "updated_at": "now", } } })) patch_lock_file(lock_path) ok, msg = uninstall_skill("evil") assert ok is False assert (isolated_skills_dir / "bystander" / "SKILL.md").read_text() == "safe" def test_install_from_quarantine_rejects_symlinks(self, tmp_path): """Skill install must not follow symlinks that leak file contents from outside the quarantine directory.""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: bad-skill\n---\n") secret = tmp_path / "secret.txt" secret.write_text("data exfiltration payload\n") leak = q_dir / "leak.txt" try: leak.symlink_to(secret) except (OSError, NotImplementedError): pytest.skip("symlink creation unsupported on this platform") bundle = hub.SkillBundle( name="bad-skill", files={"SKILL.md": "---\nname: bad-skill\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="bad-skill", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root), \ patch("tools.skill_usage.record_installed") as record_installed: with pytest.raises(ValueError, match="symlink"): hub.install_from_quarantine( q_dir, "bad-skill", "", bundle, scan_result, ) record_installed.assert_not_called() assert not (skills_dir / "bad-skill" / "leak.txt").exists() assert secret.read_text() == "data exfiltration payload\n" def test_install_from_quarantine_rejects_category_bucket_overwrite(self, tmp_path): """Installing a skill whose name matches an existing category directory that contains other skills must NOT silently wipe that entire directory. Regression test for GitHub issue #75983: ``hermes skills install … --name research`` deleted the whole ``skills/research/`` category bucket, destroying 16 unrelated skills. """ import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" skills_dir.mkdir() # Simulate a user-created category bucket with multiple skills. category = skills_dir / "research" category.mkdir() for skill in ("alpha", "bravo", "charlie"): (category / skill).mkdir() (category / skill / "SKILL.md").write_text(f"name: {skill}") quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: research\n---\n") bundle = hub.SkillBundle( name="research", files={"SKILL.md": "---\nname: research\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="research", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): with pytest.raises(ValueError, match="Refusing to overwrite category directory"): hub.install_from_quarantine( q_dir, "research", "", bundle, scan_result, ) # Verify the category bucket and its skills are intact. assert (category / "alpha" / "SKILL.md").exists() assert (category / "bravo" / "SKILL.md").exists() assert (category / "charlie" / "SKILL.md").exists() def test_install_from_quarantine_allows_existing_skill_overwrite(self, tmp_path): """Installing over an existing skill directory (containing SKILL.md) is still allowed — that scenario is already guarded by the lock-file check in do_install().""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" skills_dir.mkdir() # Existing skill directory with SKILL.md. existing = skills_dir / "my-skill" existing.mkdir() (existing / "SKILL.md").write_text("old content") (existing / "refs").mkdir() (existing / "refs" / "guide.md").write_text("old guide") quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: my-skill\n---\nnew") (q_dir / "refs").mkdir() (q_dir / "refs" / "guide.md").write_text("new guide") bundle = hub.SkillBundle( name="my-skill", files={"SKILL.md": "---\nname: my-skill\n---\nnew"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="my-skill", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): installed = hub.install_from_quarantine( q_dir, "my-skill", "", bundle, scan_result, ) # The old directory was replaced by the new one. assert installed.exists() assert (installed / "SKILL.md").read_text().strip() == "---\nname: my-skill\n---\nnew" def test_install_from_quarantine_allows_empty_category_dir(self, tmp_path): """Installing into an existing but empty category directory is allowed (no sibling skills to destroy).""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" skills_dir.mkdir() # Empty category directory (no skills inside). category = skills_dir / "research" category.mkdir() quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: research\n---\n") bundle = hub.SkillBundle( name="research", files={"SKILL.md": "---\nname: research\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="research", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): installed = hub.install_from_quarantine( q_dir, "research", "", bundle, scan_result, ) assert installed.exists() assert (installed / "SKILL.md").exists() def test_install_from_quarantine_rejects_nested_only_category(self, tmp_path): """A category whose skills live only in sub-categories (depth >= 2, e.g. ``mlops/training/``) must also be protected — the guard must not be fooled by the absence of direct-child skills (#75983).""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) # Nested-only category: no skill directly under mlops/, skills at depth 2. for sub, name in (("training", "trl"), ("inference", "vllm")): skill = skills_dir / "mlops" / sub / name skill.mkdir(parents=True) (skill / "SKILL.md").write_text(f"---\nname: {name}\n---\n") q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: mlops\n---\n") bundle = hub.SkillBundle( name="mlops", files={"SKILL.md": "---\nname: mlops\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="mlops", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): with pytest.raises(ValueError, match="category directory"): hub.install_from_quarantine( q_dir, "mlops", "", bundle, scan_result, ) # Every nested skill must survive. assert (skills_dir / "mlops" / "training" / "trl" / "SKILL.md").is_file() assert (skills_dir / "mlops" / "inference" / "vllm" / "SKILL.md").is_file() def test_install_from_quarantine_rejects_category_inside_skill(self, tmp_path): """Installing with --category naming an existing *skill* directory must be refused: it would create a hybrid skill-plus-category dir whose later update/uninstall rmtree would destroy the nested skill (#75983 sibling).""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) # Existing normal skill directory. outer = skills_dir / "devops" outer.mkdir(parents=True) (outer / "SKILL.md").write_text("---\nname: devops\n---\n") q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: docker\n---\n") bundle = hub.SkillBundle( name="docker", files={"SKILL.md": "---\nname: docker\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="docker", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): with pytest.raises(ValueError, match="existing skill directory"): hub.install_from_quarantine( q_dir, "docker", "devops", bundle, scan_result, ) # The outer skill is untouched and no hybrid child was created. assert (outer / "SKILL.md").is_file() assert not (outer / "docker").exists() def test_install_from_quarantine_rejects_regular_file_collision(self, tmp_path): """A stray regular file at the install path must produce the caller's ValueError contract, not an uncaught NotADirectoryError from iterdir/rmtree.""" import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" quarantine_root = skills_dir / ".hub" / "quarantine" quarantine_root.mkdir(parents=True) (skills_dir / "notes").write_text("not a directory") q_dir = quarantine_root / "pending" q_dir.mkdir() (q_dir / "SKILL.md").write_text("---\nname: notes\n---\n") bundle = hub.SkillBundle( name="notes", files={"SKILL.md": "---\nname: notes\n---\n"}, source="community", identifier="x", trust_level="community", ) scan_result = ScanResult( skill_name="notes", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root): with pytest.raises(ValueError, match="not a directory"): hub.install_from_quarantine( q_dir, "notes", "", bundle, scan_result, ) assert (skills_dir / "notes").read_text() == "not a directory" def test_install_from_quarantine_records_successful_install(self, tmp_path): import tools.skills_hub as hub from tools.skills_guard import ScanResult skills_dir = tmp_path / "skills" quarantine_root = skills_dir / ".hub" / "quarantine" q_dir = quarantine_root / "pending" q_dir.mkdir(parents=True) skill_md = "---\nname: good-skill\n---\n\n# Good skill\n" (q_dir / "SKILL.md").write_text(skill_md, encoding="utf-8") bundle = hub.SkillBundle( name="good-skill", files={"SKILL.md": skill_md}, source="community", identifier="good/source", trust_level="community", ) scan_result = ScanResult( skill_name="good-skill", source="community", trust_level="community", verdict="safe", ) with patch.object(hub, "SKILLS_DIR", skills_dir), \ patch.object(hub, "QUARANTINE_DIR", quarantine_root), \ patch("tools.skill_usage.record_installed") as record_installed: installed = hub.install_from_quarantine( q_dir, "good-skill", "", bundle, scan_result, ) assert installed == skills_dir / "good-skill" assert installed.is_dir() record_installed.assert_called_once_with("good-skill") # --------------------------------------------------------------------------- # parallel_search_sources — overall_timeout must be honoured even when a # source blocks for far longer than the budget (regression: the executor used # `with ... as pool`, whose __exit__ calls shutdown(wait=True) and blocked the # caller on the slow worker, making overall_timeout a no-op). # --------------------------------------------------------------------------- class _FakeSource(SkillSource): def __init__(self, sid: str, sleep: float = 0.0, results=None): self._sid = sid self._sleep = sleep self._results = results or [] def source_id(self) -> str: return self._sid def search(self, query: str, limit: int = 10) -> List[SkillMeta]: if self._sleep: time.sleep(self._sleep) return list(self._results) def fetch(self, identifier: str) -> Optional[SkillBundle]: return None def inspect(self, identifier: str) -> Optional[SkillMeta]: return None class TestParallelSearchSourcesTimeout: def _meta(self, sid: str) -> SkillMeta: return SkillMeta( name=f"{sid}-skill", description="x", source=sid, identifier=f"{sid}/x", trust_level="community", ) def test_slow_source_does_not_block_caller(self): """A source sleeping well past overall_timeout must not stall the return. Before the fix the executor's `with` block waited on the slow worker (~5s); now the call returns promptly and reports the source as timed out.""" fast = _FakeSource("fast", sleep=0.0, results=[self._meta("fast")]) slow = _FakeSource("slow", sleep=0.3, results=[self._meta("slow")]) start = time.monotonic() all_results, source_counts, timed_out_ids = parallel_search_sources( [fast, slow], query="q", overall_timeout=0.05, ) elapsed = time.monotonic() - start # Must return long before the slow source's sleep finishes. assert elapsed < 1.0, f"call blocked for {elapsed:.2f}s (timeout not honoured)" assert "slow" in timed_out_ids # Fast source still delivered its result and is not flagged timed out. assert source_counts.get("fast") == 1 assert "fast" not in timed_out_ids assert any(r.source == "fast" for r in all_results) def test_all_fast_sources_complete_without_timeout(self): """Happy path: when every source finishes within budget, none are flagged and all results are collected.""" a = _FakeSource("a", results=[self._meta("a")]) b = _FakeSource("b", results=[self._meta("b")]) all_results, source_counts, timed_out_ids = parallel_search_sources( [a, b], query="q", overall_timeout=5.0, ) assert timed_out_ids == [] assert source_counts.get("a") == 1 assert source_counts.get("b") == 1 assert len(all_results) == 2 # --------------------------------------------------------------------------- # _load_hermes_index — centralized index fetch (Browse-hub landing / search) # --------------------------------------------------------------------------- class TestLoadHermesIndex: """Regression coverage for the Skills-Hub index fetch. The centralized index is a large body served with Content-Encoding: br. httpx's streaming Brotli decoder (brotlicffi 1.2.0.1, pinned for Discord attachment decoding) raises DecodingError on payloads this size, which used to cascade into a silently-empty Skills Hub. The fetch must therefore (a) not ask for Brotli, and (b) survive a DecodingError by retrying uncompressed instead of blanking the hub. """ @staticmethod def _isolate_cache(monkeypatch, tmp_path): """Point the on-disk cache at an empty tmp dir so no real cache leaks in.""" import tools.skills_hub as hub cache_file = tmp_path / "hermes-index.json" monkeypatch.setattr(hub, "_hermes_index_cache_file", lambda: cache_file) return cache_file def test_fetch_does_not_request_brotli(self, monkeypatch, tmp_path): """The index fetch must not negotiate Brotli (the broken decoder path).""" import tools.skills_hub as hub self._isolate_cache(monkeypatch, tmp_path) captured = {} def fake_get(url, *args, **kwargs): captured["headers"] = kwargs.get("headers", {}) resp = MagicMock() resp.status_code = 200 resp.json.return_value = {"skills": [{"name": "x"}]} return resp monkeypatch.setattr(hub.httpx, "get", fake_get) data = hub._load_hermes_index() assert data == {"skills": [{"name": "x"}]} accept = captured["headers"].get("Accept-Encoding", "") assert "br" not in [tok.strip() for tok in accept.split(",")], ( f"index fetch must not request Brotli, got Accept-Encoding={accept!r}" ) def test_persistent_decoding_error_falls_back_to_stale_cache( self, monkeypatch, tmp_path ): """If every attempt fails to decode, serve the stale cache rather than None.""" import tools.skills_hub as hub cache_file = self._isolate_cache(monkeypatch, tmp_path) cache_file.write_text(json.dumps({"skills": [{"name": "stale"}]})) # Force the cache to look expired so the network path runs. old = time.time() - (hub.HERMES_INDEX_TTL + 100) import os os.utime(cache_file, (old, old)) def fake_get(url, *args, **kwargs): raise httpx.DecodingError("brotli boom") monkeypatch.setattr(hub.httpx, "get", fake_get) data = hub._load_hermes_index() assert data == {"skills": [{"name": "stale"}]} # --------------------------------------------------------------------------- # Referenced-path extraction & missing support files (regression: a prose # glob or a repo-only dev tool referenced in SKILL.md must not abort install) # --------------------------------------------------------------------------- class TestReferencedSupportPaths: def test_ignores_globs_placeholders_and_truncated_tokens(self): md = ( "Load `references/type-*.md` before drawing, and " "`references/type-.md` for the matching type.\n" "Real files: [flowchart](references/type-flowchart.md) and " "`references/type-architecture.md`.\n" ) assert _referenced_support_paths(md) == { "references/type-flowchart.md", "references/type-architecture.md", } def test_keeps_real_backtick_references(self): md = "Run `python3 scripts/self_check.py ` and see `references/guide.md`.\n" assert _referenced_support_paths(md) == { "scripts/self_check.py", "references/guide.md", } class TestGitHubSourceFetchMissingReferencedFile: def _source(self): auth = MagicMock(spec=GitHubAuth) return GitHubSource(auth=auth) def test_fetch_skips_missing_referenced_file_and_keeps_the_rest(self): md = ( "---\nname: demo\ndescription: demo\n---\n\n" "See [guide](references/guide.md) and `references/missing.md`.\n" ) tree_entries = [ {"path": "skills/demo/references/guide.md", "type": "blob", "mode": "100644"}, ] source = self._source() with patch.object(source, "_fetch_file_content", return_value=md), \ patch.object(source, "_get_repo_tree", return_value=("main", tree_entries)), \ patch.object(source, "_fetch_file_bytes", return_value=b"# guide"): bundle = source.fetch("owner/repo/skills/demo") assert bundle is not None assert bundle.name == "demo" # The present referenced file is bundled… assert bundle.files["references/guide.md"] == b"# guide" # …and the missing one is warned about and skipped, not fatal. assert "references/missing.md" not in bundle.files class TestUrlSourceFetchMissingReferencedFile: def test_fetch_skips_missing_referenced_file(self): md = "---\nname: demo\ndescription: demo\n---\n\nSee `references/missing.md`.\n" source = UrlSource() with patch.object(source, "_fetch_text", return_value=md), \ patch.object(source, "_fetch_bytes", return_value=None): bundle = source.fetch("https://example.com/skills/demo/SKILL.md") assert bundle is not None assert bundle.name == "demo" assert "references/missing.md" not in bundle.files