@@ -3043,6 +3043,7 @@ def test_bundled_preset_missing_locally_cli_error(self, project_dir):
30433043 assert result .exit_code == 1
30443044 output = strip_ansi (result .output ).lower ()
30453045 assert "bundled" in output , result .output
3046+ assert "reinstall" in output , result .output
30463047
30473048
30483049class TestWrapStrategy :
@@ -3062,7 +3063,7 @@ def test_substitute_core_template_replaces_placeholder(self, project_dir):
30623063
30633064 registrar = CommandRegistrar ()
30643065 body = "## Pre-Logic\n \n Before stuff.\n \n {CORE_TEMPLATE}\n \n ## Post-Logic\n \n After stuff.\n "
3065- result = _substitute_core_template (body , "specify" , project_dir , registrar )
3066+ result , _ = _substitute_core_template (body , "speckit. specify" , project_dir , registrar )
30663067
30673068 assert "{CORE_TEMPLATE}" not in result
30683069 assert "# Core Specify" in result
@@ -3080,7 +3081,7 @@ def test_substitute_core_template_no_op_when_placeholder_absent(self, project_di
30803081
30813082 registrar = CommandRegistrar ()
30823083 body = "## No placeholder here.\n "
3083- result = _substitute_core_template (body , "specify" , project_dir , registrar )
3084+ result , _ = _substitute_core_template (body , "speckit. specify" , project_dir , registrar )
30843085 assert result == body
30853086
30863087 def test_substitute_core_template_no_op_when_core_missing (self , project_dir ):
@@ -3090,9 +3091,10 @@ def test_substitute_core_template_no_op_when_core_missing(self, project_dir):
30903091
30913092 registrar = CommandRegistrar ()
30923093 body = "Pre.\n \n {CORE_TEMPLATE}\n \n Post.\n "
3093- result = _substitute_core_template (body , "nonexistent" , project_dir , registrar )
3094+ result , core_fm = _substitute_core_template (body , "speckit. nonexistent" , project_dir , registrar )
30943095 assert result == body
30953096 assert "{CORE_TEMPLATE}" in result
3097+ assert core_fm == {}
30963098
30973099 def test_register_commands_substitutes_core_template_for_wrap_strategy (self , project_dir ):
30983100 """register_commands substitutes {CORE_TEMPLATE} when strategy: wrap."""
@@ -3136,7 +3138,7 @@ def test_register_commands_substitutes_core_template_for_wrap_strategy(self, pro
31363138 project_dir / "preset" , project_dir
31373139 )
31383140 finally :
3139- registrar .AGENT_CONFIGS = original
3141+ CommandRegistrar .AGENT_CONFIGS = original
31403142
31413143 written = (agent_dir / "speckit.specify.md" ).read_text ()
31423144 assert "{CORE_TEMPLATE}" not in written
@@ -3176,3 +3178,105 @@ def test_end_to_end_wrap_via_self_test_preset(self, project_dir):
31763178 assert "# Core Wrap-Test Body" in written
31773179 assert "preset:self-test wrap-pre" in written
31783180 assert "preset:self-test wrap-post" in written
3181+
3182+ def test_substitute_core_template_returns_core_frontmatter (self , project_dir ):
3183+ """_substitute_core_template returns core frontmatter as second tuple element."""
3184+ from specify_cli .presets import _substitute_core_template
3185+ from specify_cli .agents import CommandRegistrar
3186+
3187+ core_dir = project_dir / ".specify" / "templates" / "commands"
3188+ core_dir .mkdir (parents = True , exist_ok = True )
3189+ (core_dir / "specify.md" ).write_text (
3190+ "---\n description: core\n scripts:\n sh: scripts/bash/setup.sh\n ---\n \n # Core Specify\n "
3191+ )
3192+
3193+ registrar = CommandRegistrar ()
3194+ body = "{CORE_TEMPLATE}\n \n ## Post\n "
3195+ result_body , core_fm = _substitute_core_template (body , "speckit.specify" , project_dir , registrar )
3196+
3197+ assert "{CORE_TEMPLATE}" not in result_body
3198+ assert "# Core Specify" in result_body
3199+ assert core_fm .get ("scripts" ) == {"sh" : "scripts/bash/setup.sh" }
3200+
3201+ def test_substitute_core_template_no_core_returns_empty_frontmatter (self , project_dir ):
3202+ """Returns empty dict as frontmatter when core template does not exist."""
3203+ from specify_cli .presets import _substitute_core_template
3204+ from specify_cli .agents import CommandRegistrar
3205+
3206+ registrar = CommandRegistrar ()
3207+ body = "{CORE_TEMPLATE}\n \n ## Post\n "
3208+ result_body , core_fm = _substitute_core_template (body , "speckit.nonexistent" , project_dir , registrar )
3209+
3210+ assert result_body == body
3211+ assert core_fm == {}
3212+
3213+ def test_substitute_core_template_resolves_extension_command (self , project_dir ):
3214+ """_substitute_core_template resolves {CORE_TEMPLATE} from an extension command directory."""
3215+ from specify_cli .presets import _substitute_core_template
3216+ from specify_cli .agents import CommandRegistrar
3217+
3218+ # Simulate an installed extension
3219+ ext_cmd_dir = project_dir / ".specify" / "extensions" / "git" / "commands"
3220+ ext_cmd_dir .mkdir (parents = True , exist_ok = True )
3221+ (ext_cmd_dir / "speckit.git.feature.md" ).write_text (
3222+ "---\n description: git feature core\n handoffs:\n - label: Next\n agent: speckit.plan\n ---\n \n # Core Git Feature\n "
3223+ )
3224+
3225+ registrar = CommandRegistrar ()
3226+ body = "## Pre\n \n {CORE_TEMPLATE}\n \n ## Post\n "
3227+ result_body , core_fm = _substitute_core_template (
3228+ body , "speckit.git.feature" , project_dir , registrar
3229+ )
3230+
3231+ assert "{CORE_TEMPLATE}" not in result_body
3232+ assert "# Core Git Feature" in result_body
3233+ assert core_fm .get ("handoffs" ) is not None
3234+
3235+ def test_wrap_strategy_inherits_core_scripts_in_skills (self , project_dir ):
3236+ """_register_skills merges core scripts into preset frontmatter for {SCRIPT} resolution."""
3237+ import json
3238+ from specify_cli .presets import PresetManager
3239+ from pathlib import Path
3240+
3241+ # Set up core command template with scripts frontmatter
3242+ core_dir = project_dir / ".specify" / "templates" / "commands"
3243+ core_dir .mkdir (parents = True , exist_ok = True )
3244+ (core_dir / "specify.md" ).write_text (
3245+ "---\n description: core specify\n scripts:\n sh: scripts/bash/check.sh\n ---\n \n "
3246+ "Run: {SCRIPT}\n \n # Core Specify\n "
3247+ )
3248+
3249+ # Write init-options
3250+ (project_dir / ".specify" / "init-options.json" ).write_text (
3251+ json .dumps ({"ai" : "claude" , "ai_skills" : True })
3252+ )
3253+
3254+ # Create a preset with a wrap command that has no scripts of its own
3255+ preset_dir = project_dir / "test-preset"
3256+ (preset_dir / "commands" ).mkdir (parents = True )
3257+ (preset_dir / "commands" / "speckit.specify.md" ).write_text (
3258+ "---\n description: preset wrap\n strategy: wrap\n ---\n \n {CORE_TEMPLATE}\n \n ## Post\n "
3259+ )
3260+ (preset_dir / "preset.yml" ).write_text (
3261+ "schema_version: '1.0'\n "
3262+ "preset:\n id: test-scripts-preset\n name: Test\n version: 1.0.0\n "
3263+ " description: Test\n author: test\n "
3264+ "requires:\n speckit_version: '>=0.5.0'\n "
3265+ "provides:\n templates:\n "
3266+ " - type: command\n name: speckit.specify\n "
3267+ " file: commands/speckit.specify.md\n description: wrap\n "
3268+ )
3269+
3270+ # Create the skill dir so _register_skills will overwrite it
3271+ skills_dir = project_dir / ".claude" / "skills" / "speckit-specify"
3272+ skills_dir .mkdir (parents = True )
3273+ (skills_dir / "SKILL.md" ).write_text ("---\n name: speckit-specify\n ---\n \n old\n " )
3274+
3275+ manager = PresetManager (project_dir )
3276+ manager .install_from_directory (preset_dir , "1.0.0" )
3277+
3278+ written = (skills_dir / "SKILL.md" ).read_text ()
3279+ # {SCRIPT} should have been resolved using the core template's scripts
3280+ assert "{SCRIPT}" not in written
3281+ assert "scripts/bash/check.sh" in written
3282+
0 commit comments