From a6ddfa5769fef5e28a185989af0c8f9162b145e3 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Mon, 17 Aug 2026 22:22:17 +0700 Subject: [PATCH] test(skills): cover same-name inspect/install provenance mixup Pin ClawHub to GitHub-style identifiers and keep catalog metadata from being paired with a foreign same-named bundle. --- tests/hermes_cli/test_skills_hub.py | 95 ++++++++++++++++++++++++++ tests/tools/test_skills_hub_clawhub.py | 56 +++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/tests/hermes_cli/test_skills_hub.py b/tests/hermes_cli/test_skills_hub.py index 9507fb9c6b..d3287f9a06 100644 --- a/tests/hermes_cli/test_skills_hub.py +++ b/tests/hermes_cli/test_skills_hub.py @@ -186,6 +186,101 @@ def test_check_for_skill_updates_does_not_fall_back_across_registries(): assert "bundle" not in results[0], "must not carry a foreign registry's bundle" +def test_resolve_does_not_pair_catalog_meta_with_foreign_same_name_bundle(): + """Inspect metadata from one registry must not ship with another registry's files. + + skills.sh can inspect ``owner/repo/skills/skillopt`` while ClawHub used to + fetch by last path segment ``skillopt`` and return a different author's + SKILL.md. The header then showed the requested identifier and the preview + showed the wrong skill. + """ + from hermes_cli.skills_hub import _resolve_source_meta_and_bundle + from tools.skills_hub import SkillBundle, SkillMeta + + class CatalogSource: + def inspect(self, identifier): + return SkillMeta( + name="skillopt", + description="kanban-based pipelines", + source="skills.sh", + identifier="skills-sh/latipun7/agent-skill-collections/skills/skillopt", + trust_level="community", + ) + + def fetch(self, identifier): + return None + + class ForeignSlugSource: + def inspect(self, identifier): + return SkillMeta( + name="skillopt", + description="Train, evaluate, and improve Agent skill files", + source="clawhub", + identifier="skillopt", + trust_level="community", + ) + + def fetch(self, identifier): + return SkillBundle( + name="skillopt", + files={"SKILL.md": "# Train, evaluate, and improve Agent skill files\n"}, + source="clawhub", + identifier="skillopt", + trust_level="community", + ) + + meta, bundle, matched = _resolve_source_meta_and_bundle( + "latipun7/agent-skill-collections/skills/skillopt", + [CatalogSource(), ForeignSlugSource()], + ) + + assert bundle is not None + assert bundle.source == "clawhub" + assert meta is not None + assert meta.source == "clawhub" + assert meta.identifier == "skillopt" + assert "kanban-based" not in (meta.description or "") + assert matched is not None + assert matched.__class__ is ForeignSlugSource + + +def test_resolve_keeps_catalog_meta_when_later_sources_do_not_fetch(): + from hermes_cli.skills_hub import _resolve_source_meta_and_bundle + from tools.skills_hub import SkillMeta + + class CatalogSource: + def inspect(self, identifier): + return SkillMeta( + name="skillopt", + description="kanban-based pipelines", + source="skills.sh", + identifier="skills-sh/latipun7/agent-skill-collections/skills/skillopt", + trust_level="community", + ) + + def fetch(self, identifier): + return None + + class QuietSource: + def inspect(self, identifier): + return None + + def fetch(self, identifier): + return None + + meta, bundle, matched = _resolve_source_meta_and_bundle( + "latipun7/agent-skill-collections/skills/skillopt", + [CatalogSource(), QuietSource()], + ) + + assert bundle is None + assert meta is not None + assert meta.source == "skills.sh" + assert meta.identifier.endswith("latipun7/agent-skill-collections/skills/skillopt") + assert matched is not None + assert matched.__class__ is CatalogSource + + # --------------------------------------------------------------------------- diff --git a/tests/tools/test_skills_hub_clawhub.py b/tests/tools/test_skills_hub_clawhub.py index 9e6d306dc5..599fe02d8a 100644 --- a/tests/tools/test_skills_hub_clawhub.py +++ b/tests/tools/test_skills_hub_clawhub.py @@ -370,6 +370,62 @@ class TestClawHubSource(unittest.TestCase): self.assertEqual(results[0].identifier, "only-skill") mock_write_cache.assert_called_once() + def test_parse_identifier_accepts_clawhub_shapes(self): + self.assertEqual(ClawHubSource._parse_identifier("skillopt"), ("skillopt", None)) + self.assertEqual(ClawHubSource._parse_identifier("clawhub/skillopt"), ("skillopt", None)) + self.assertEqual( + ClawHubSource._parse_identifier("@harrylabsj/skillopt"), + ("skillopt", "harrylabsj"), + ) + self.assertEqual( + ClawHubSource._parse_identifier("harrylabsj/skills/skillopt"), + ("skillopt", "harrylabsj"), + ) + + def test_parse_identifier_rejects_github_style_paths(self): + self.assertIsNone( + ClawHubSource._parse_identifier("latipun7/agent-skill-collections/skillopt") + ) + self.assertIsNone( + ClawHubSource._parse_identifier( + "latipun7/agent-skill-collections/skills/skillopt" + ) + ) + self.assertIsNone( + ClawHubSource._parse_identifier( + "skills-sh/latipun7/agent-skill-collections/skills/skillopt" + ) + ) + + @patch("tools.skills_hub.httpx.get") + def test_inspect_does_not_claim_github_style_identifier(self, mock_get): + meta = self.src.inspect("latipun7/agent-skill-collections/skills/skillopt") + self.assertIsNone(meta) + mock_get.assert_not_called() + + @patch("tools.skills_hub.httpx.get") + def test_fetch_does_not_claim_github_style_identifier(self, mock_get): + bundle = self.src.fetch("latipun7/agent-skill-collections/skillopt") + self.assertIsNone(bundle) + mock_get.assert_not_called() + + @patch("tools.skills_hub.httpx.get") + def test_inspect_rejects_owner_mismatch_on_clawhub_url_path(self, mock_get): + mock_get.return_value = _MockResponse( + status_code=200, + json_data={ + "slug": "skillopt", + "displayName": "SkillOpt", + "summary": "Train, evaluate, and improve Agent skill files", + "owner": {"handle": "harrylabsj"}, + }, + ) + + meta = self.src.inspect("latipun7/skills/skillopt") + + self.assertIsNone(meta) + mock_get.assert_called_once() + class TestClawHubCatalogWalkBounded(unittest.TestCase): """max_items bounds the walk so browse's cold-start fallback renders one