1313import pytest
1414import tempfile
1515import shutil
16+ import yaml
1617from pathlib import Path
1718from unittest .mock import patch
1819
20+ import specify_cli
21+
1922from specify_cli import (
2023 _get_skills_dir ,
2124 install_ai_skills ,
@@ -88,6 +91,17 @@ def templates_dir(temp_dir):
8891 encoding = "utf-8" ,
8992 )
9093
94+ # Template with empty YAML frontmatter (yaml.safe_load returns None)
95+ (tpl_root / "empty_fm.md" ).write_text (
96+ "---\n "
97+ "---\n "
98+ "\n "
99+ "# Empty Frontmatter Command\n "
100+ "\n "
101+ "Body with empty frontmatter.\n " ,
102+ encoding = "utf-8" ,
103+ )
104+
91105 return tpl_root
92106
93107
@@ -172,10 +186,6 @@ class TestInstallAiSkills:
172186
173187 def test_skills_installed_with_correct_structure (self , project_dir , templates_dir ):
174188 """Verify SKILL.md files have correct agentskills.io structure."""
175- # Directly call install_ai_skills with a patched templates dir path
176- import specify_cli
177-
178- orig_file = specify_cli .__file__
179189 # We need to make Path(__file__).parent.parent.parent resolve to temp root
180190 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
181191 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -191,7 +201,10 @@ def test_skills_installed_with_correct_structure(self, project_dir, templates_di
191201
192202 # Check that skill directories were created
193203 skill_dirs = sorted ([d .name for d in skills_dir .iterdir () if d .is_dir ()])
194- assert skill_dirs == ["speckit-plan" , "speckit-specify" , "speckit-tasks" ]
204+ assert "speckit-plan" in skill_dirs
205+ assert "speckit-specify" in skill_dirs
206+ assert "speckit-tasks" in skill_dirs
207+ assert "speckit-empty_fm" in skill_dirs
195208
196209 # Verify SKILL.md content for speckit-specify
197210 skill_file = skills_dir / "speckit-specify" / "SKILL.md"
@@ -211,9 +224,48 @@ def test_skills_installed_with_correct_structure(self, project_dir, templates_di
211224 assert "# Speckit Specify Skill" in content
212225 assert "Run this to create a spec." in content
213226
227+ def test_generated_skill_has_parseable_yaml (self , project_dir , templates_dir ):
228+ """Generated SKILL.md should contain valid, parseable YAML frontmatter."""
229+ # We need to make Path(__file__).parent.parent.parent resolve to temp root
230+ fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
231+ fake_init .parent .mkdir (parents = True , exist_ok = True )
232+ fake_init .touch ()
233+
234+ with patch .object (specify_cli , "__file__" , str (fake_init )):
235+ install_ai_skills (project_dir , "claude" )
236+
237+ skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
238+ content = skill_file .read_text ()
239+
240+ # Extract and parse frontmatter
241+ assert content .startswith ("---\n " )
242+ parts = content .split ("---" , 2 )
243+ assert len (parts ) >= 3
244+ parsed = yaml .safe_load (parts [1 ])
245+ assert isinstance (parsed , dict )
246+ assert "name" in parsed
247+ assert parsed ["name" ] == "speckit-specify"
248+ assert "description" in parsed
249+
250+ def test_empty_yaml_frontmatter (self , project_dir , templates_dir ):
251+ """Templates with empty YAML frontmatter (---\n ---) should not crash."""
252+ fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
253+ fake_init .parent .mkdir (parents = True , exist_ok = True )
254+ fake_init .touch ()
255+
256+ with patch .object (specify_cli , "__file__" , str (fake_init )):
257+ result = install_ai_skills (project_dir , "claude" )
258+
259+ assert result is True
260+
261+ skill_file = project_dir / ".claude" / "skills" / "speckit-empty_fm" / "SKILL.md"
262+ assert skill_file .exists ()
263+ content = skill_file .read_text ()
264+ assert "name: speckit-empty_fm" in content
265+ assert "Body with empty frontmatter." in content
266+
214267 def test_enhanced_descriptions_used_when_available (self , project_dir , templates_dir ):
215268 """SKILL_DESCRIPTIONS take precedence over template frontmatter descriptions."""
216- import specify_cli
217269
218270 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
219271 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -231,7 +283,7 @@ def test_enhanced_descriptions_used_when_available(self, project_dir, templates_
231283
232284 def test_template_without_frontmatter (self , project_dir , templates_dir ):
233285 """Templates without YAML frontmatter should still produce valid skills."""
234- import specify_cli
286+
235287
236288 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
237289 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -250,7 +302,7 @@ def test_template_without_frontmatter(self, project_dir, templates_dir):
250302
251303 def test_missing_templates_directory (self , project_dir ):
252304 """Returns False when templates/commands directory doesn't exist."""
253- import specify_cli
305+
254306
255307 # Point to a non-existent directory
256308 fake_init = project_dir / "nonexistent" / "src" / "specify_cli" / "__init__.py"
@@ -268,7 +320,7 @@ def test_missing_templates_directory(self, project_dir):
268320
269321 def test_empty_templates_directory (self , project_dir , temp_dir ):
270322 """Returns False when templates/commands has no .md files."""
271- import specify_cli
323+
272324
273325 # Create empty templates/commands
274326 empty_tpl = temp_dir / "empty_root" / "templates" / "commands"
@@ -284,7 +336,7 @@ def test_empty_templates_directory(self, project_dir, temp_dir):
284336
285337 def test_malformed_yaml_frontmatter (self , project_dir , temp_dir ):
286338 """Malformed YAML in a template should be handled gracefully, not crash."""
287- import specify_cli
339+
288340
289341 tpl_dir = temp_dir / "bad_root" / "templates" / "commands"
290342 tpl_dir .mkdir (parents = True )
@@ -313,7 +365,7 @@ def test_malformed_yaml_frontmatter(self, project_dir, temp_dir):
313365
314366 def test_additive_does_not_overwrite_other_files (self , project_dir , templates_dir ):
315367 """Installing skills should not remove non-speckit files in the skills dir."""
316- import specify_cli
368+
317369
318370 # Pre-create a custom skill
319371 custom_dir = project_dir / ".claude" / "skills" / "my-custom-skill"
@@ -334,7 +386,7 @@ def test_additive_does_not_overwrite_other_files(self, project_dir, templates_di
334386
335387 def test_return_value (self , project_dir , templates_dir ):
336388 """install_ai_skills returns True when skills installed, False otherwise."""
337- import specify_cli
389+
338390
339391 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
340392 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -364,7 +416,7 @@ class TestCommandCoexistence:
364416
365417 def test_existing_commands_preserved_claude (self , project_dir , templates_dir , commands_dir_claude ):
366418 """install_ai_skills must NOT remove pre-existing .claude/commands files."""
367- import specify_cli
419+
368420
369421 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
370422 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -382,7 +434,7 @@ def test_existing_commands_preserved_claude(self, project_dir, templates_dir, co
382434
383435 def test_existing_commands_preserved_gemini (self , project_dir , templates_dir , commands_dir_gemini ):
384436 """install_ai_skills must NOT remove pre-existing .gemini/commands files."""
385- import specify_cli
437+
386438
387439 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
388440 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -398,7 +450,7 @@ def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, co
398450
399451 def test_commands_dir_not_removed (self , project_dir , templates_dir , commands_dir_claude ):
400452 """install_ai_skills must not remove the commands directory."""
401- import specify_cli
453+
402454
403455 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
404456 fake_init .parent .mkdir (parents = True , exist_ok = True )
@@ -411,7 +463,7 @@ def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir
411463
412464 def test_no_commands_dir_no_error (self , project_dir , templates_dir ):
413465 """No error when agent has no commands directory at all."""
414- import specify_cli
466+
415467
416468 fake_init = templates_dir .parent .parent / "src" / "specify_cli" / "__init__.py"
417469 fake_init .parent .mkdir (parents = True , exist_ok = True )
0 commit comments