@@ -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 :
@@ -3139,7 +3140,7 @@ def test_register_commands_substitutes_core_template_for_wrap_strategy(self, pro
31393140 project_dir / "preset" , project_dir
31403141 )
31413142 finally :
3142- registrar .AGENT_CONFIGS = original
3143+ CommandRegistrar .AGENT_CONFIGS = original
31433144
31443145 written = (agent_dir / "speckit.specify.md" ).read_text ()
31453146 assert "{CORE_TEMPLATE}" not in written
@@ -3295,7 +3296,7 @@ def test_register_commands_inherits_scripts_from_core(self, project_dir):
32953296 project_dir ,
32963297 )
32973298 finally :
3298- registrar .AGENT_CONFIGS = original
3299+ CommandRegistrar .AGENT_CONFIGS = original
32993300
33003301 written = (agent_dir / "speckit.specify.md" ).read_text ()
33013302 assert "{CORE_TEMPLATE}" not in written
@@ -3343,7 +3344,7 @@ def test_register_commands_toml_resolves_inherited_scripts(self, project_dir):
33433344 project_dir ,
33443345 )
33453346 finally :
3346- registrar .AGENT_CONFIGS = original
3347+ CommandRegistrar .AGENT_CONFIGS = original
33473348
33483349 written = (toml_dir / "speckit.specify.toml" ).read_text ()
33493350 assert "{CORE_TEMPLATE}" not in written
@@ -3352,3 +3353,41 @@ def test_register_commands_toml_resolves_inherited_scripts(self, project_dir):
33523353 # args token must use TOML format, not the intermediate $ARGUMENTS
33533354 assert "$ARGUMENTS" not in written
33543355 assert "{{args}}" in written
3356+
3357+ def test_extension_command_resolves_dot_separated_template (self , project_dir ):
3358+ """Extension command names like speckit.git.feature look up git.feature.md, not git-feature.md.
3359+
3360+ Covers both _register_skills (via _substitute_core_template with raw_short_name)
3361+ and register_commands (via short_name stripped of speckit. prefix).
3362+ """
3363+ from specify_cli .presets import _substitute_core_template
3364+ from specify_cli .agents import CommandRegistrar
3365+
3366+ core_dir = project_dir / ".specify" / "templates" / "commands"
3367+ core_dir .mkdir (parents = True , exist_ok = True )
3368+ # Template uses dot-separated name — must NOT be named git-feature.md
3369+ (core_dir / "git.feature.md" ).write_text (
3370+ "---\n description: core git feature\n ---\n \n # Git Feature Core\n "
3371+ )
3372+ # Ensure the hyphenated variant does NOT exist to prove we pick the right file
3373+ assert not (core_dir / "git-feature.md" ).exists ()
3374+
3375+ registrar = CommandRegistrar ()
3376+ body = "## Wrapper\n \n {CORE_TEMPLATE}\n "
3377+
3378+ # Simulate register_commands: strips "speckit." only
3379+ short_name_commands = "speckit.git.feature"
3380+ if short_name_commands .startswith ("speckit." ):
3381+ short_name_commands = short_name_commands [len ("speckit." ):]
3382+ result_commands , _ = _substitute_core_template (body , short_name_commands , project_dir , registrar )
3383+
3384+ # Simulate _register_skills: strips "speckit." then hyphenates for skill dirs,
3385+ # but passes raw_short_name (pre-hyphenation) to _substitute_core_template
3386+ raw_short_name = "speckit.git.feature"
3387+ if raw_short_name .startswith ("speckit." ):
3388+ raw_short_name = raw_short_name [len ("speckit." ):]
3389+ result_skills , _ = _substitute_core_template (body , raw_short_name , project_dir , registrar )
3390+
3391+ assert "# Git Feature Core" in result_commands , "register_commands path did not resolve extension template"
3392+ assert "# Git Feature Core" in result_skills , "_register_skills path did not resolve extension template"
3393+ assert result_commands == result_skills , "register_commands and _register_skills resolved differently"
0 commit comments