mirror of
https://github.com/github/spec-kit.git
synced 2026-03-18 19:33:09 +00:00
feat(presets): propagate command overrides to skills via init-options
- Add save_init_options() / load_init_options() helpers that persist CLI flags from 'specify init' to .specify/init-options.json - PresetManager._register_skills() overwrites SKILL.md files when --ai-skills was used during init and corresponding skill dirs exist - PresetManager._unregister_skills() restores core template content on preset removal - registered_skills stored in preset registry metadata - 8 new tests covering skill override, skip conditions, and restore
This commit is contained in:
@@ -1566,3 +1566,145 @@ class TestSelfTestPreset:
|
||||
|
||||
cmd_file = claude_dir / "speckit.fakeext.cmd.md"
|
||||
assert cmd_file.exists(), "Command not registered despite extension being present"
|
||||
|
||||
|
||||
# ===== Init Options and Skills Tests =====
|
||||
|
||||
|
||||
class TestInitOptions:
|
||||
"""Tests for save_init_options / load_init_options helpers."""
|
||||
|
||||
def test_save_and_load_round_trip(self, project_dir):
|
||||
from specify_cli import save_init_options, load_init_options
|
||||
|
||||
opts = {"ai": "claude", "ai_skills": True, "here": False}
|
||||
save_init_options(project_dir, opts)
|
||||
|
||||
loaded = load_init_options(project_dir)
|
||||
assert loaded["ai"] == "claude"
|
||||
assert loaded["ai_skills"] is True
|
||||
|
||||
def test_load_returns_empty_when_missing(self, project_dir):
|
||||
from specify_cli import load_init_options
|
||||
|
||||
assert load_init_options(project_dir) == {}
|
||||
|
||||
def test_load_returns_empty_on_invalid_json(self, project_dir):
|
||||
from specify_cli import load_init_options
|
||||
|
||||
opts_file = project_dir / ".specify" / "init-options.json"
|
||||
opts_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
opts_file.write_text("{bad json")
|
||||
|
||||
assert load_init_options(project_dir) == {}
|
||||
|
||||
|
||||
class TestPresetSkills:
|
||||
"""Tests for preset skill registration and unregistration."""
|
||||
|
||||
def _write_init_options(self, project_dir, ai="claude", ai_skills=True):
|
||||
from specify_cli import save_init_options
|
||||
|
||||
save_init_options(project_dir, {"ai": ai, "ai_skills": ai_skills})
|
||||
|
||||
def _create_skill(self, skills_dir, skill_name, body="original body"):
|
||||
skill_dir = skills_dir / skill_name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"---\nname: {skill_name}\n---\n\n{body}\n"
|
||||
)
|
||||
return skill_dir
|
||||
|
||||
def test_skill_overridden_on_preset_install(self, project_dir, temp_dir):
|
||||
"""When --ai-skills was used, a preset command override should update the skill."""
|
||||
# Simulate --ai-skills having been used: write init-options + create skill
|
||||
self._write_init_options(project_dir, ai="claude")
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify")
|
||||
|
||||
# Also create the claude commands dir so commands get registered
|
||||
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Install self-test preset (has a command override for speckit.specify)
|
||||
manager = PresetManager(project_dir)
|
||||
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
|
||||
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")
|
||||
|
||||
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
|
||||
assert skill_file.exists()
|
||||
content = skill_file.read_text()
|
||||
assert "preset:self-test" in content, "Skill should reference preset source"
|
||||
|
||||
# Verify it was recorded in registry
|
||||
metadata = manager.registry.get("self-test")
|
||||
assert "speckit-specify" in metadata.get("registered_skills", [])
|
||||
|
||||
def test_skill_not_updated_when_ai_skills_disabled(self, project_dir, temp_dir):
|
||||
"""When --ai-skills was NOT used, preset install should not touch skills."""
|
||||
self._write_init_options(project_dir, ai="claude", ai_skills=False)
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify", body="untouched")
|
||||
|
||||
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manager = PresetManager(project_dir)
|
||||
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
|
||||
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")
|
||||
|
||||
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
|
||||
content = skill_file.read_text()
|
||||
assert "untouched" in content, "Skill should not be modified when ai_skills=False"
|
||||
|
||||
def test_skill_not_updated_without_init_options(self, project_dir, temp_dir):
|
||||
"""When no init-options.json exists, preset install should not touch skills."""
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify", body="untouched")
|
||||
|
||||
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manager = PresetManager(project_dir)
|
||||
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
|
||||
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")
|
||||
|
||||
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
|
||||
content = skill_file.read_text()
|
||||
assert "untouched" in content
|
||||
|
||||
def test_skill_restored_on_preset_remove(self, project_dir, temp_dir):
|
||||
"""When a preset is removed, skills should be restored from core templates."""
|
||||
self._write_init_options(project_dir, ai="claude")
|
||||
skills_dir = project_dir / ".claude" / "skills"
|
||||
self._create_skill(skills_dir, "speckit-specify")
|
||||
|
||||
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manager = PresetManager(project_dir)
|
||||
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
|
||||
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")
|
||||
|
||||
# Verify preset content is in the skill
|
||||
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
|
||||
assert "preset:self-test" in skill_file.read_text()
|
||||
|
||||
# Remove the preset
|
||||
manager.remove("self-test")
|
||||
|
||||
# Skill should be restored (core specify.md template exists)
|
||||
assert skill_file.exists(), "Skill should still exist after preset removal"
|
||||
content = skill_file.read_text()
|
||||
assert "preset:self-test" not in content, "Preset content should be gone"
|
||||
assert "templates/commands/specify.md" in content, "Should reference core template"
|
||||
|
||||
def test_no_skills_registered_when_no_skill_dir_exists(self, project_dir, temp_dir):
|
||||
"""Skills should not be created when no existing skill dir is found."""
|
||||
self._write_init_options(project_dir, ai="claude")
|
||||
# Don't create skills dir — simulate --ai-skills never created them
|
||||
|
||||
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manager = PresetManager(project_dir)
|
||||
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
|
||||
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")
|
||||
|
||||
metadata = manager.registry.get("self-test")
|
||||
assert metadata.get("registered_skills", []) == []
|
||||
|
||||
Reference in New Issue
Block a user