From d3c6af9b00e9c7a377f5fb49d4d6ff02125c5611 Mon Sep 17 00:00:00 2001 From: teernisse Date: Sat, 28 Feb 2026 00:47:40 -0500 Subject: [PATCH] test(skills): add frontmatter name override tests Add comprehensive test coverage for skill name resolution priority: - test_frontmatter_name_overrides_dir_name: Verifies that the 'name' field in YAML frontmatter takes precedence over directory name, enabling namespaced skills like "doodle:bug-hunter" - test_name_preserved_across_fallback_files: Confirms that when SKILL.md provides a name but README.md provides the description, the name from SKILL.md is preserved (important for multi-file skill definitions) - test_no_frontmatter_name_uses_dir_name: Validates fallback behavior when no name is specified in frontmatter This ensures the skill autocomplete system correctly handles both simple directory-named skills and explicitly namespaced skills from skill packs. Co-Authored-By: Claude Opus 4.5 --- tests/test_skills.py | 154 ++++++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/tests/test_skills.py b/tests/test_skills.py index 47c19c7..51757ef 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -155,6 +155,51 @@ class TestEnumerateClaudeSkills(unittest.TestCase): self.assertEqual(result[0]["description"], "Skill: empty-skill") + def test_frontmatter_name_overrides_dir_name(self): + """Frontmatter name field takes priority over directory name.""" + with tempfile.TemporaryDirectory() as tmpdir: + skill_dir = Path(tmpdir) / ".claude/skills/bug-hunter" + skill_dir.mkdir(parents=True) + content = '---\nname: "doodle:bug-hunter"\ndescription: Find bugs\n---\n' + (skill_dir / "SKILL.md").write_text(content) + + with patch.object(Path, "home", return_value=Path(tmpdir)): + result = self.handler._enumerate_claude_skills() + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["name"], "doodle:bug-hunter") + self.assertEqual(result[0]["description"], "Find bugs") + + def test_name_preserved_across_fallback_files(self): + """Name from SKILL.md is kept even when description comes from README.md.""" + with tempfile.TemporaryDirectory() as tmpdir: + skill_dir = Path(tmpdir) / ".claude/skills/bug-hunter" + skill_dir.mkdir(parents=True) + # SKILL.md has name but no extractable description (headers only) + (skill_dir / "SKILL.md").write_text( + '---\nname: "doodle:bug-hunter"\n---\n# Bug Hunter\n## Overview' + ) + # README.md provides the description + (skill_dir / "README.md").write_text("Find and fix obvious bugs") + + with patch.object(Path, "home", return_value=Path(tmpdir)): + result = self.handler._enumerate_claude_skills() + + self.assertEqual(result[0]["name"], "doodle:bug-hunter") + self.assertEqual(result[0]["description"], "Find and fix obvious bugs") + + def test_no_frontmatter_name_uses_dir_name(self): + """Without name in frontmatter, falls back to directory name.""" + with tempfile.TemporaryDirectory() as tmpdir: + skill_dir = Path(tmpdir) / ".claude/skills/plain-skill" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("---\ndescription: No name field\n---\n") + + with patch.object(Path, "home", return_value=Path(tmpdir)): + result = self.handler._enumerate_claude_skills() + + self.assertEqual(result[0]["name"], "plain-skill") + def test_no_md_files_uses_fallback(self): """Skill dir with no markdown files uses fallback description.""" with tempfile.TemporaryDirectory() as tmpdir: @@ -182,122 +227,111 @@ class TestEnumerateClaudeSkills(unittest.TestCase): self.assertEqual(result[0]["description"], "Skill: broken-skill") -class TestExtractDescription(unittest.TestCase): - """Tests for _extract_description.""" +class TestParseFrontmatter(unittest.TestCase): + """Tests for _parse_frontmatter.""" def setUp(self): self.handler = DummySkillsHandler() + def _desc(self, content: str) -> str: + return self.handler._parse_frontmatter(content)["description"] + + def _name(self, content: str) -> str: + return self.handler._parse_frontmatter(content)["name"] + def test_empty_content(self): - self.assertEqual(self.handler._extract_description(""), "") + self.assertEqual(self._desc(""), "") + self.assertEqual(self._name(""), "") def test_plain_text(self): - self.assertEqual( - self.handler._extract_description("Simple description"), - "Simple description", - ) + self.assertEqual(self._desc("Simple description"), "Simple description") def test_yaml_frontmatter_description(self): content = "---\ndescription: A skill for formatting\n---\nBody text" - self.assertEqual( - self.handler._extract_description(content), - "A skill for formatting", - ) + self.assertEqual(self._desc(content), "A skill for formatting") def test_yaml_frontmatter_quoted_description(self): content = '---\ndescription: "Quoted desc"\n---\n' - self.assertEqual( - self.handler._extract_description(content), - "Quoted desc", - ) + self.assertEqual(self._desc(content), "Quoted desc") def test_yaml_frontmatter_single_quoted_description(self): content = "---\ndescription: 'Single quoted'\n---\n" - self.assertEqual( - self.handler._extract_description(content), - "Single quoted", - ) + self.assertEqual(self._desc(content), "Single quoted") def test_yaml_multiline_fold_indicator(self): """Handles >- style multi-line YAML.""" content = "---\ndescription: >-\n Multi-line folded text\n---\n" - self.assertEqual( - self.handler._extract_description(content), - "Multi-line folded text", - ) + self.assertEqual(self._desc(content), "Multi-line folded text") def test_yaml_multiline_literal_indicator(self): """Handles |- style multi-line YAML.""" content = "---\ndescription: |-\n Literal block text\n---\n" - self.assertEqual( - self.handler._extract_description(content), - "Literal block text", - ) + self.assertEqual(self._desc(content), "Literal block text") def test_yaml_multiline_bare_fold(self): """Handles > without trailing dash.""" content = "---\ndescription: >\n Bare fold\n---\n" - self.assertEqual( - self.handler._extract_description(content), - "Bare fold", - ) + self.assertEqual(self._desc(content), "Bare fold") def test_yaml_multiline_bare_literal(self): """Handles | without trailing dash.""" content = "---\ndescription: |\n Bare literal\n---\n" - self.assertEqual( - self.handler._extract_description(content), - "Bare literal", - ) + self.assertEqual(self._desc(content), "Bare literal") def test_yaml_empty_description_falls_back_to_body(self): """Empty description in frontmatter falls back to body text.""" content = "---\ndescription:\n---\nFallback body line" - self.assertEqual( - self.handler._extract_description(content), - "Fallback body line", - ) + self.assertEqual(self._desc(content), "Fallback body line") def test_skips_headers_and_empty_lines(self): content = "# Title\n\n## Section\n\nActual content" - self.assertEqual( - self.handler._extract_description(content), - "Actual content", - ) + self.assertEqual(self._desc(content), "Actual content") def test_skips_html_comments(self): content = "\nReal content" - self.assertEqual( - self.handler._extract_description(content), - "Real content", - ) + self.assertEqual(self._desc(content), "Real content") def test_truncates_to_100_chars(self): long_line = "B" * 150 - self.assertEqual( - len(self.handler._extract_description(long_line)), - 100, - ) + self.assertEqual(len(self._desc(long_line)), 100) def test_frontmatter_description_truncated(self): desc = "C" * 150 content = f"---\ndescription: {desc}\n---\n" - self.assertEqual( - len(self.handler._extract_description(content)), - 100, - ) + self.assertEqual(len(self._desc(content)), 100) def test_no_closing_frontmatter_extracts_description(self): """Unclosed frontmatter still extracts description from the loop.""" content = "---\ndescription: Orphaned\ntitle: Test" - # The frontmatter loop finds "description:" and returns early, - # even though there's no closing "---" - result = self.handler._extract_description(content) - self.assertEqual(result, "Orphaned") + self.assertEqual(self._desc(content), "Orphaned") def test_body_only_headers_returns_empty(self): content = "# H1\n## H2\n### H3" - self.assertEqual(self.handler._extract_description(content), "") + self.assertEqual(self._desc(content), "") + + # --- name field tests --- + + def test_extracts_name_from_frontmatter(self): + content = "---\nname: doodle:bug-hunter\ndescription: Find bugs\n---\n" + self.assertEqual(self._name(content), "doodle:bug-hunter") + self.assertEqual(self._desc(content), "Find bugs") + + def test_name_quoted(self): + content = '---\nname: "bmad:brainstorm"\n---\nBody' + self.assertEqual(self._name(content), "bmad:brainstorm") + + def test_name_single_quoted(self): + content = "---\nname: 'my-prefix:skill'\n---\nBody" + self.assertEqual(self._name(content), "my-prefix:skill") + + def test_no_name_field_returns_empty(self): + content = "---\ndescription: Just a description\n---\n" + self.assertEqual(self._name(content), "") + + def test_name_truncated_to_100(self): + long_name = "N" * 150 + content = f"---\nname: {long_name}\n---\n" + self.assertEqual(len(self._name(content)), 100) class TestEnumerateCodexSkills(unittest.TestCase):