mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
feat: implement preset wrap strategy (#2189)
* feat: implement strategy: wrap * fix: resolve merge conflict for strategy wrap correctness * feat: multi-preset composable wrapping with priority ordering Implements comment #4 from PR review: multiple installed wrap presets now compose in priority order rather than overwriting each other. Key changes: - PresetResolver.resolve() gains skip_presets flag; resolve_core() wraps it to skip tier 2, preventing accidental nesting during replay - _replay_wraps_for_command() recomposed all enabled wrap presets for a command in ascending priority order (innermost-first) after any install or remove - _replay_skill_override() keeps SKILL.md in sync with the recomposed command body for ai-skills-enabled projects - install_from_directory() detects strategy: wrap commands, stores wrap_commands in the registry entry, and calls replay after install - remove() reads wrap_commands before deletion, removes registry entry before rmtree so replay sees post-removal state, then replays remaining wraps or unregisters when none remain Tests: TestResolveCore (5), TestReplayWrapsForCommand (5), TestInstallRemoveWrapLifecycle (5), plus 2 skill/alias regression tests * fix: resolve extension commands via manifest file mapping PresetResolver.resolve_extension_command_via_manifest() consults each installed extension.yml to find the actual file declared for a command name, rather than assuming the file is named <cmd_name>.md. This fixes _substitute_core_template for extensions like selftest where the manifest maps speckit.selftest.extension → commands/selftest.md. Resolution order in _substitute_core_template is now: 1. resolve_core(cmd_name) — project overrides win, then name-based lookup 2. resolve_extension_command_via_manifest(cmd_name) — manifest fallback 3. resolve_core(short_name) — core template short-name fallback Path traversal guard mirrors the containment check already present in ExtensionManager to reject absolute paths or paths escaping the extension root. * fix: add bundled core_pack as Priority 5 in PresetResolver.resolve() resolve_core() was returning None for built-in commands (implement, specify, etc.) because PresetResolver only checked .specify/templates/ commands/ (Priority 4), which is never populated for commands in a normal project. strategy:wrap presets rely on resolve_core() to fetch the {CORE_TEMPLATE} body, so the wrap was silently skipped and SKILL.md was never updated. Priority 5 now checks core_pack/commands/ (wheel install) or repo_root/templates/commands/ (source checkout), mirroring the pattern used by _locate_core_pack() elsewhere. Updated two tests whose assertions assumed resolve_core() always returned None when .specify/templates/commands/ was absent. * fix: harden preset wrap replay removal * fix: stabilize existing directory error output * fix: track outermost_pack_id from contributing preset; use Path.parts in tests - outermost_pack_id now updates alongside outermost_frontmatter inside the wrap loop, so it reflects the actual last contributing preset rather than always taking wrap_presets[0] (which may have been skipped) - Replace str(path) substring checks in TestResolveCore with Path.parts tuple comparisons for correct behaviour on Windows (CI runs windows-latest) * fix: guard against non-mapping YAML manifests; apply integration post-processing in replay - ExtensionManifest._load raises ValidationError for non-dict YAML roots instead of TypeError - PresetManager._replay_wraps_for_command calls integration.post_process_skill_content, matching _register_skills behaviour - PresetResolver skips extensions that raise OSError/TypeError/AttributeError on manifest load - Tests: non-mapping YAML, OSError manifest skip, and replay integration post-processing
This commit is contained in:
@@ -217,6 +217,14 @@ class TestExtensionManifest:
|
||||
with pytest.raises(ValidationError, match="Missing required field"):
|
||||
ExtensionManifest(manifest_path)
|
||||
|
||||
def test_non_mapping_yaml_raises_validation_error(self, temp_dir):
|
||||
"""Manifest whose YAML root is a scalar or list raises ValidationError, not TypeError."""
|
||||
manifest_path = temp_dir / "extension.yml"
|
||||
for bad_content in ("42\n", "[]\n", "null\n"):
|
||||
manifest_path.write_text(bad_content)
|
||||
with pytest.raises(ValidationError, match="YAML mapping"):
|
||||
ExtensionManifest(manifest_path)
|
||||
|
||||
def test_invalid_extension_id(self, temp_dir, valid_manifest_data):
|
||||
"""Test manifest with invalid extension ID format."""
|
||||
import yaml
|
||||
|
||||
Reference in New Issue
Block a user