Skip to content

Commit 443288a

Browse files
committed
fix: review comments
1 parent bcd134f commit 443288a

File tree

2 files changed

+69
-17
lines changed

2 files changed

+69
-17
lines changed

src/specify_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
10761076
if content.startswith("---"):
10771077
parts = content.split("---", 2)
10781078
if len(parts) >= 3:
1079-
frontmatter = yaml.safe_load(parts[1])
1079+
frontmatter = yaml.safe_load(parts[1]) or {}
10801080
body = parts[2].strip()
10811081
else:
10821082
frontmatter = {}

tests/test_ai_skills.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
import pytest
1414
import tempfile
1515
import shutil
16+
import yaml
1617
from pathlib import Path
1718
from unittest.mock import patch
1819

20+
import specify_cli
21+
1922
from 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

Comments
 (0)