@@ -2943,6 +2943,137 @@ def fail_recovered_claude_registration(self, agent_name, *args, **kwargs):
29432943 assert "speckit-early-ext-world" in metadata ["registered_skills" ]
29442944
29452945
2946+
2947+ # ===== Regression test: upgrade-overwrites-copilot-skills (#3849) =====
2948+
2949+ class TestRegisterExtensionSkillsForceFlag :
2950+ """Regression tests for the ``force`` flag on ``_register_extension_skills``.
2951+
2952+ Issue #3849: ``integration upgrade --force`` called ``setup()`` which
2953+ regenerated all core-template SKILL.md files, then called
2954+ ``register_enabled_extensions_for_agent()``. The skip-guard in
2955+ ``_register_extension_skills`` treated the freshly-written core files as
2956+ existing user content and skipped every extension skill, leaving only core
2957+ template content on disk.
2958+
2959+ The fix introduces ``force=True`` in the upgrade path so the guard does not
2960+ fire for core-template files that setup() just wrote.
2961+ """
2962+
2963+ def test_force_false_skips_existing_skill (self , project_dir , temp_dir ):
2964+ """Without force=True the skip guard must still protect existing files."""
2965+ _create_init_options (project_dir , ai = "claude" , ai_skills = True )
2966+ skills_dir = _create_skills_dir (project_dir , ai = "claude" )
2967+ ext_dir = _create_extension_dir (temp_dir )
2968+
2969+ # Manually pre-create a SKILL.md as if setup() had already written it
2970+ skill_subdir = skills_dir / "speckit-test-ext-hello"
2971+ skill_subdir .mkdir (parents = True , exist_ok = True )
2972+ skill_file = skill_subdir / "SKILL.md"
2973+ skill_file .write_text ("core-template content only" , encoding = "utf-8" )
2974+
2975+ manager = ExtensionManager (project_dir )
2976+ manifest = ExtensionManifest (ext_dir / "extension.yml" )
2977+
2978+ # Default (force=False): existing file must not be overwritten
2979+ written = manager ._register_extension_skills (manifest , ext_dir , force = False )
2980+ assert "speckit-test-ext-hello" not in written
2981+ assert skill_file .read_text (encoding = "utf-8" ) == "core-template content only"
2982+
2983+ def test_force_true_overwrites_existing_skill (self , project_dir , temp_dir ):
2984+ """With force=True the function must overwrite the existing SKILL.md.
2985+
2986+ This is the core regression test for #3849: calling
2987+ ``_register_extension_skills(force=True)`` after ``setup()`` has
2988+ written a fresh core-template SKILL.md must replace it with the
2989+ composed extension content.
2990+ """
2991+ _create_init_options (project_dir , ai = "claude" , ai_skills = True )
2992+ skills_dir = _create_skills_dir (project_dir , ai = "claude" )
2993+ ext_dir = _create_extension_dir (temp_dir )
2994+
2995+ # Simulate what setup() writes: a bare core-template SKILL.md
2996+ skill_subdir = skills_dir / "speckit-test-ext-hello"
2997+ skill_subdir .mkdir (parents = True , exist_ok = True )
2998+ skill_file = skill_subdir / "SKILL.md"
2999+ skill_file .write_text ("core-template content only" , encoding = "utf-8" )
3000+
3001+ manager = ExtensionManager (project_dir )
3002+ manifest = ExtensionManifest (ext_dir / "extension.yml" )
3003+
3004+ # Upgrade path (force=True): extension content should replace the core file
3005+ written = manager ._register_extension_skills (manifest , ext_dir , force = True )
3006+ assert "speckit-test-ext-hello" in written , (
3007+ "force=True should overwrite the core-template file and return the skill name"
3008+ )
3009+ content = skill_file .read_text (encoding = "utf-8" )
3010+ assert "Run this to say hello." in content , (
3011+ "Extension command body must appear in the overwritten SKILL.md"
3012+ )
3013+ assert "core-template content only" not in content , (
3014+ "Core-template placeholder must have been replaced by extension content"
3015+ )
3016+
3017+ def test_register_enabled_extensions_for_agent_force_flag_threads_through (
3018+ self , project_dir , temp_dir
3019+ ):
3020+ """force=True on register_enabled_extensions_for_agent must reach _register_extension_skills.
3021+
3022+ End-to-end check: after an upgrade writes a fresh core-template SKILL.md,
3023+ ``register_enabled_extensions_for_agent(force=True)`` must produce a
3024+ SKILL.md that contains the extension content.
3025+ """
3026+ _create_init_options (project_dir , ai = "claude" , ai_skills = True )
3027+ skills_dir = _create_skills_dir (project_dir , ai = "claude" )
3028+ ext_dir = _create_extension_dir (temp_dir )
3029+
3030+ manager = ExtensionManager (project_dir )
3031+ # Install extension so it is in the registry
3032+ ext_manifest = manager .install_from_directory (
3033+ ext_dir , "0.1.0" , register_commands = False
3034+ )
3035+
3036+ # Simulate a freshly-regenerated core-template SKILL.md (as setup() would write)
3037+ skill_file = skills_dir / "speckit-test-ext-hello" / "SKILL.md"
3038+ skill_file .write_text ("core-template content only" , encoding = "utf-8" )
3039+
3040+ # Re-register with force=True (upgrade path)
3041+ manager .register_enabled_extensions_for_agent ("claude" , force = True )
3042+
3043+ content = skill_file .read_text (encoding = "utf-8" )
3044+ assert "Run this to say hello." in content , (
3045+ "After register_enabled_extensions_for_agent(force=True), the SKILL.md "
3046+ "must contain the extension body, not just the core-template stub."
3047+ )
3048+
3049+ def test_force_true_with_preexisting_dir_but_no_skill_file (
3050+ self , project_dir , temp_dir
3051+ ):
3052+ """force=True must write into a pre-existing directory with no SKILL.md.
3053+
3054+ The second skip guard (``elif skill_dir_preexists``) should also be
3055+ bypassed by force=True so an upgrade can create a missing SKILL.md
3056+ even when the skill sub-directory already exists.
3057+ """
3058+ _create_init_options (project_dir , ai = "claude" , ai_skills = True )
3059+ skills_dir = _create_skills_dir (project_dir , ai = "claude" )
3060+ ext_dir = _create_extension_dir (temp_dir )
3061+
3062+ # Create the skill directory without the SKILL.md file
3063+ skill_subdir = skills_dir / "speckit-test-ext-hello"
3064+ skill_subdir .mkdir (parents = True , exist_ok = True )
3065+ skill_file = skill_subdir / "SKILL.md"
3066+ assert not skill_file .exists ()
3067+
3068+ manager = ExtensionManager (project_dir )
3069+ manifest = ExtensionManifest (ext_dir / "extension.yml" )
3070+
3071+ written = manager ._register_extension_skills (manifest , ext_dir , force = True )
3072+ assert "speckit-test-ext-hello" in written
3073+ assert skill_file .exists ()
3074+ assert "Run this to say hello." in skill_file .read_text (encoding = "utf-8" )
3075+
3076+
29463077# ===== Extension Skill Unregistration Tests =====
29473078
29483079class TestExtensionSkillUnregistration :
0 commit comments