@@ -3062,12 +3062,13 @@ def test_substitute_core_template_replaces_placeholder(self, project_dir):
30623062
30633063 registrar = CommandRegistrar ()
30643064 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 )
3065+ result , core_fm = _substitute_core_template (body , "specify" , project_dir , registrar )
30663066
30673067 assert "{CORE_TEMPLATE}" not in result
30683068 assert "# Core Specify" in result
30693069 assert "## Pre-Logic" in result
30703070 assert "## Post-Logic" in result
3071+ assert core_fm .get ("description" ) == "core"
30713072
30723073 def test_substitute_core_template_no_op_when_placeholder_absent (self , project_dir ):
30733074 """Returns body unchanged when {CORE_TEMPLATE} is not present."""
@@ -3080,8 +3081,9 @@ 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 , core_fm = _substitute_core_template (body , "specify" , project_dir , registrar )
30843085 assert result == body
3086+ assert core_fm == {}
30853087
30863088 def test_substitute_core_template_no_op_when_core_missing (self , project_dir ):
30873089 """Returns body unchanged when core template file does not exist."""
@@ -3090,9 +3092,10 @@ def test_substitute_core_template_no_op_when_core_missing(self, project_dir):
30903092
30913093 registrar = CommandRegistrar ()
30923094 body = "Pre.\n \n {CORE_TEMPLATE}\n \n Post.\n "
3093- result = _substitute_core_template (body , "nonexistent" , project_dir , registrar )
3095+ result , core_fm = _substitute_core_template (body , "nonexistent" , project_dir , registrar )
30943096 assert result == body
30953097 assert "{CORE_TEMPLATE}" in result
3098+ assert core_fm == {}
30963099
30973100 def test_register_commands_substitutes_core_template_for_wrap_strategy (self , project_dir ):
30983101 """register_commands substitutes {CORE_TEMPLATE} when strategy: wrap."""
@@ -3176,3 +3179,176 @@ def test_end_to_end_wrap_via_self_test_preset(self, project_dir):
31763179 assert "# Core Wrap-Test Body" in written
31773180 assert "preset:self-test wrap-pre" in written
31783181 assert "preset:self-test wrap-post" in written
3182+
3183+ def test_substitute_core_template_returns_core_scripts (self , project_dir ):
3184+ """core_frontmatter in the returned tuple includes scripts/agent_scripts."""
3185+ from specify_cli .presets import _substitute_core_template
3186+ from specify_cli .agents import CommandRegistrar
3187+
3188+ core_dir = project_dir / ".specify" / "templates" / "commands"
3189+ core_dir .mkdir (parents = True , exist_ok = True )
3190+ (core_dir / "specify.md" ).write_text (
3191+ "---\n description: core\n scripts:\n sh: run.sh\n agent_scripts:\n sh: agent-run.sh\n ---\n \n # Body\n "
3192+ )
3193+
3194+ registrar = CommandRegistrar ()
3195+ body = "## Wrapper\n \n {CORE_TEMPLATE}\n "
3196+ result , core_fm = _substitute_core_template (body , "specify" , project_dir , registrar )
3197+
3198+ assert "# Body" in result
3199+ assert core_fm .get ("scripts" ) == {"sh" : "run.sh" }
3200+ assert core_fm .get ("agent_scripts" ) == {"sh" : "agent-run.sh" }
3201+
3202+ def test_register_skills_inherits_scripts_from_core_when_preset_omits_them (self , project_dir ):
3203+ """_register_skills merges scripts/agent_scripts from core when preset lacks them."""
3204+ from specify_cli .presets import PresetManager
3205+ import json
3206+
3207+ # Core template with scripts
3208+ core_dir = project_dir / ".specify" / "templates" / "commands"
3209+ core_dir .mkdir (parents = True , exist_ok = True )
3210+ (core_dir / "wrap-test.md" ).write_text (
3211+ "---\n description: core\n scripts:\n sh: .specify/scripts/run.sh\n ---\n \n "
3212+ "Run: {SCRIPT}\n "
3213+ )
3214+
3215+ # Skills dir for claude
3216+ skills_dir = project_dir / ".claude" / "skills"
3217+ skills_dir .mkdir (parents = True , exist_ok = True )
3218+ skill_subdir = skills_dir / "speckit-wrap-test"
3219+ skill_subdir .mkdir ()
3220+ (skill_subdir / "SKILL.md" ).write_text ("---\n name: speckit-wrap-test\n ---\n \n old\n " )
3221+
3222+ (project_dir / ".specify" / "init-options.json" ).write_text (
3223+ json .dumps ({"ai" : "claude" , "ai_skills" : True })
3224+ )
3225+
3226+ manager = PresetManager (project_dir )
3227+ manager .install_from_directory (SELF_TEST_PRESET_DIR , "0.1.5" )
3228+
3229+ written = (skill_subdir / "SKILL.md" ).read_text ()
3230+ # {SCRIPT} should have been resolved (not left as a literal placeholder)
3231+ assert "{SCRIPT}" not in written
3232+
3233+ def test_register_skills_preset_scripts_take_precedence_over_core (self , project_dir ):
3234+ """preset-defined scripts/agent_scripts are not overwritten by core frontmatter."""
3235+ from specify_cli .presets import _substitute_core_template
3236+ from specify_cli .agents import CommandRegistrar
3237+
3238+ core_dir = project_dir / ".specify" / "templates" / "commands"
3239+ core_dir .mkdir (parents = True , exist_ok = True )
3240+ (core_dir / "specify.md" ).write_text (
3241+ "---\n description: core\n scripts:\n sh: core-run.sh\n ---\n \n Core body.\n "
3242+ )
3243+
3244+ registrar = CommandRegistrar ()
3245+ body = "{CORE_TEMPLATE}"
3246+ _ , core_fm = _substitute_core_template (body , "specify" , project_dir , registrar )
3247+
3248+ # Simulate preset frontmatter that already defines scripts
3249+ preset_fm = {"description" : "preset" , "strategy" : "wrap" , "scripts" : {"sh" : "preset-run.sh" }}
3250+ for key in ("scripts" , "agent_scripts" ):
3251+ if key not in preset_fm and key in core_fm :
3252+ preset_fm [key ] = core_fm [key ]
3253+
3254+ # Preset's scripts must not be overwritten by core
3255+ assert preset_fm ["scripts" ] == {"sh" : "preset-run.sh" }
3256+
3257+ def test_register_commands_inherits_scripts_from_core (self , project_dir ):
3258+ """register_commands merges scripts/agent_scripts from core and normalizes paths."""
3259+ from specify_cli .agents import CommandRegistrar
3260+ import copy
3261+
3262+ core_dir = project_dir / ".specify" / "templates" / "commands"
3263+ core_dir .mkdir (parents = True , exist_ok = True )
3264+ (core_dir / "specify.md" ).write_text (
3265+ "---\n description: core\n scripts:\n sh: .specify/scripts/run.sh {ARGS}\n ---\n \n "
3266+ "Run: {SCRIPT}\n "
3267+ )
3268+
3269+ cmd_dir = project_dir / "preset" / "commands"
3270+ cmd_dir .mkdir (parents = True , exist_ok = True )
3271+ # Preset has strategy: wrap but no scripts of its own
3272+ (cmd_dir / "speckit.specify.md" ).write_text (
3273+ "---\n description: wrap no scripts\n strategy: wrap\n ---\n \n "
3274+ "## Pre\n \n {CORE_TEMPLATE}\n \n ## Post\n "
3275+ )
3276+
3277+ agent_dir = project_dir / ".claude" / "commands"
3278+ agent_dir .mkdir (parents = True , exist_ok = True )
3279+
3280+ registrar = CommandRegistrar ()
3281+ original = copy .deepcopy (registrar .AGENT_CONFIGS )
3282+ registrar .AGENT_CONFIGS ["test-agent" ] = {
3283+ "dir" : str (agent_dir .relative_to (project_dir )),
3284+ "format" : "markdown" ,
3285+ "args" : "$ARGUMENTS" ,
3286+ "extension" : ".md" ,
3287+ "strip_frontmatter_keys" : [],
3288+ }
3289+ try :
3290+ registrar .register_commands (
3291+ "test-agent" ,
3292+ [{"name" : "speckit.specify" , "file" : "commands/speckit.specify.md" }],
3293+ "test-preset" ,
3294+ project_dir / "preset" ,
3295+ project_dir ,
3296+ )
3297+ finally :
3298+ registrar .AGENT_CONFIGS = original
3299+
3300+ written = (agent_dir / "speckit.specify.md" ).read_text ()
3301+ assert "{CORE_TEMPLATE}" not in written
3302+ assert "Run:" in written
3303+ assert "scripts:" in written
3304+ assert "run.sh" in written
3305+
3306+ def test_register_commands_toml_resolves_inherited_scripts (self , project_dir ):
3307+ """TOML agents resolve {SCRIPT} from inherited core scripts when preset omits them."""
3308+ from specify_cli .agents import CommandRegistrar
3309+ import copy
3310+
3311+ core_dir = project_dir / ".specify" / "templates" / "commands"
3312+ core_dir .mkdir (parents = True , exist_ok = True )
3313+ (core_dir / "specify.md" ).write_text (
3314+ "---\n description: core\n scripts:\n sh: .specify/scripts/run.sh {ARGS}\n ---\n \n "
3315+ "Run: {SCRIPT}\n "
3316+ )
3317+
3318+ cmd_dir = project_dir / "preset" / "commands"
3319+ cmd_dir .mkdir (parents = True , exist_ok = True )
3320+ (cmd_dir / "speckit.specify.md" ).write_text (
3321+ "---\n description: toml wrap\n strategy: wrap\n ---\n \n "
3322+ "## Pre\n \n {CORE_TEMPLATE}\n \n ## Post\n "
3323+ )
3324+
3325+ toml_dir = project_dir / ".gemini" / "commands"
3326+ toml_dir .mkdir (parents = True , exist_ok = True )
3327+
3328+ registrar = CommandRegistrar ()
3329+ original = copy .deepcopy (registrar .AGENT_CONFIGS )
3330+ registrar .AGENT_CONFIGS ["test-toml-agent" ] = {
3331+ "dir" : str (toml_dir .relative_to (project_dir )),
3332+ "format" : "toml" ,
3333+ "args" : "{{args}}" ,
3334+ "extension" : ".toml" ,
3335+ "strip_frontmatter_keys" : [],
3336+ }
3337+ try :
3338+ registrar .register_commands (
3339+ "test-toml-agent" ,
3340+ [{"name" : "speckit.specify" , "file" : "commands/speckit.specify.md" }],
3341+ "test-preset" ,
3342+ project_dir / "preset" ,
3343+ project_dir ,
3344+ )
3345+ finally :
3346+ registrar .AGENT_CONFIGS = original
3347+
3348+ written = (toml_dir / "speckit.specify.toml" ).read_text ()
3349+ assert "{CORE_TEMPLATE}" not in written
3350+ assert "{SCRIPT}" not in written
3351+ assert "run.sh" in written
3352+ # args token must use TOML format, not the intermediate $ARGUMENTS
3353+ assert "$ARGUMENTS" not in written
3354+ assert "{{args}}" in written
0 commit comments