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 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-28 00:47:40 -05:00
parent 0acc56417e
commit d3c6af9b00

View File

@@ -155,6 +155,51 @@ class TestEnumerateClaudeSkills(unittest.TestCase):
self.assertEqual(result[0]["description"], "Skill: empty-skill") 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): def test_no_md_files_uses_fallback(self):
"""Skill dir with no markdown files uses fallback description.""" """Skill dir with no markdown files uses fallback description."""
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
@@ -182,122 +227,111 @@ class TestEnumerateClaudeSkills(unittest.TestCase):
self.assertEqual(result[0]["description"], "Skill: broken-skill") self.assertEqual(result[0]["description"], "Skill: broken-skill")
class TestExtractDescription(unittest.TestCase): class TestParseFrontmatter(unittest.TestCase):
"""Tests for _extract_description.""" """Tests for _parse_frontmatter."""
def setUp(self): def setUp(self):
self.handler = DummySkillsHandler() 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): def test_empty_content(self):
self.assertEqual(self.handler._extract_description(""), "") self.assertEqual(self._desc(""), "")
self.assertEqual(self._name(""), "")
def test_plain_text(self): def test_plain_text(self):
self.assertEqual( self.assertEqual(self._desc("Simple description"), "Simple description")
self.handler._extract_description("Simple description"),
"Simple description",
)
def test_yaml_frontmatter_description(self): def test_yaml_frontmatter_description(self):
content = "---\ndescription: A skill for formatting\n---\nBody text" content = "---\ndescription: A skill for formatting\n---\nBody text"
self.assertEqual( self.assertEqual(self._desc(content), "A skill for formatting")
self.handler._extract_description(content),
"A skill for formatting",
)
def test_yaml_frontmatter_quoted_description(self): def test_yaml_frontmatter_quoted_description(self):
content = '---\ndescription: "Quoted desc"\n---\n' content = '---\ndescription: "Quoted desc"\n---\n'
self.assertEqual( self.assertEqual(self._desc(content), "Quoted desc")
self.handler._extract_description(content),
"Quoted desc",
)
def test_yaml_frontmatter_single_quoted_description(self): def test_yaml_frontmatter_single_quoted_description(self):
content = "---\ndescription: 'Single quoted'\n---\n" content = "---\ndescription: 'Single quoted'\n---\n"
self.assertEqual( self.assertEqual(self._desc(content), "Single quoted")
self.handler._extract_description(content),
"Single quoted",
)
def test_yaml_multiline_fold_indicator(self): def test_yaml_multiline_fold_indicator(self):
"""Handles >- style multi-line YAML.""" """Handles >- style multi-line YAML."""
content = "---\ndescription: >-\n Multi-line folded text\n---\n" content = "---\ndescription: >-\n Multi-line folded text\n---\n"
self.assertEqual( self.assertEqual(self._desc(content), "Multi-line folded text")
self.handler._extract_description(content),
"Multi-line folded text",
)
def test_yaml_multiline_literal_indicator(self): def test_yaml_multiline_literal_indicator(self):
"""Handles |- style multi-line YAML.""" """Handles |- style multi-line YAML."""
content = "---\ndescription: |-\n Literal block text\n---\n" content = "---\ndescription: |-\n Literal block text\n---\n"
self.assertEqual( self.assertEqual(self._desc(content), "Literal block text")
self.handler._extract_description(content),
"Literal block text",
)
def test_yaml_multiline_bare_fold(self): def test_yaml_multiline_bare_fold(self):
"""Handles > without trailing dash.""" """Handles > without trailing dash."""
content = "---\ndescription: >\n Bare fold\n---\n" content = "---\ndescription: >\n Bare fold\n---\n"
self.assertEqual( self.assertEqual(self._desc(content), "Bare fold")
self.handler._extract_description(content),
"Bare fold",
)
def test_yaml_multiline_bare_literal(self): def test_yaml_multiline_bare_literal(self):
"""Handles | without trailing dash.""" """Handles | without trailing dash."""
content = "---\ndescription: |\n Bare literal\n---\n" content = "---\ndescription: |\n Bare literal\n---\n"
self.assertEqual( self.assertEqual(self._desc(content), "Bare literal")
self.handler._extract_description(content),
"Bare literal",
)
def test_yaml_empty_description_falls_back_to_body(self): def test_yaml_empty_description_falls_back_to_body(self):
"""Empty description in frontmatter falls back to body text.""" """Empty description in frontmatter falls back to body text."""
content = "---\ndescription:\n---\nFallback body line" content = "---\ndescription:\n---\nFallback body line"
self.assertEqual( self.assertEqual(self._desc(content), "Fallback body line")
self.handler._extract_description(content),
"Fallback body line",
)
def test_skips_headers_and_empty_lines(self): def test_skips_headers_and_empty_lines(self):
content = "# Title\n\n## Section\n\nActual content" content = "# Title\n\n## Section\n\nActual content"
self.assertEqual( self.assertEqual(self._desc(content), "Actual content")
self.handler._extract_description(content),
"Actual content",
)
def test_skips_html_comments(self): def test_skips_html_comments(self):
content = "<!-- comment -->\nReal content" content = "<!-- comment -->\nReal content"
self.assertEqual( self.assertEqual(self._desc(content), "Real content")
self.handler._extract_description(content),
"Real content",
)
def test_truncates_to_100_chars(self): def test_truncates_to_100_chars(self):
long_line = "B" * 150 long_line = "B" * 150
self.assertEqual( self.assertEqual(len(self._desc(long_line)), 100)
len(self.handler._extract_description(long_line)),
100,
)
def test_frontmatter_description_truncated(self): def test_frontmatter_description_truncated(self):
desc = "C" * 150 desc = "C" * 150
content = f"---\ndescription: {desc}\n---\n" content = f"---\ndescription: {desc}\n---\n"
self.assertEqual( self.assertEqual(len(self._desc(content)), 100)
len(self.handler._extract_description(content)),
100,
)
def test_no_closing_frontmatter_extracts_description(self): def test_no_closing_frontmatter_extracts_description(self):
"""Unclosed frontmatter still extracts description from the loop.""" """Unclosed frontmatter still extracts description from the loop."""
content = "---\ndescription: Orphaned\ntitle: Test" content = "---\ndescription: Orphaned\ntitle: Test"
# The frontmatter loop finds "description:" and returns early, self.assertEqual(self._desc(content), "Orphaned")
# even though there's no closing "---"
result = self.handler._extract_description(content)
self.assertEqual(result, "Orphaned")
def test_body_only_headers_returns_empty(self): def test_body_only_headers_returns_empty(self):
content = "# H1\n## H2\n### H3" 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): class TestEnumerateCodexSkills(unittest.TestCase):