@@ -31,6 +31,16 @@ def _init_project(tmp_path, integration="copilot"):
3131 return project
3232
3333
34+ def _run_in_project (project , args ):
35+ """Run a CLI command from inside a generated project."""
36+ old_cwd = os .getcwd ()
37+ try :
38+ os .chdir (project )
39+ return runner .invoke (app , args , catch_exceptions = False )
40+ finally :
41+ os .chdir (old_cwd )
42+
43+
3444# ── list ─────────────────────────────────────────────────────────────
3545
3646
@@ -334,6 +344,142 @@ def test_switch_between_integrations(self, tmp_path):
334344 data = json .loads ((project / ".specify" / "integration.json" ).read_text (encoding = "utf-8" ))
335345 assert data ["integration" ] == "copilot"
336346
347+ def test_switch_migrates_extension_commands (self , tmp_path ):
348+ """Switching should migrate extension commands to the new agent directory."""
349+ project = _init_project (tmp_path , "kimi" )
350+
351+ # Install the bundled git extension
352+ result = _run_in_project (project , ["extension" , "add" , "git" ])
353+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
354+
355+ # Verify git extension skills exist for kimi
356+ kimi_git_feature = project / ".kimi" / "skills" / "speckit-git-feature" / "SKILL.md"
357+ assert kimi_git_feature .exists (), "Git extension skill should exist for kimi"
358+
359+ result = _run_in_project (project , [
360+ "integration" , "switch" , "opencode" ,
361+ "--script" , "sh" ,
362+ ])
363+ assert result .exit_code == 0 , result .output
364+
365+ # Git extension commands should exist for opencode
366+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
367+ assert opencode_git_feature .exists (), "Git extension command should exist for opencode"
368+
369+ # Old kimi extension skills should be removed
370+ assert not kimi_git_feature .exists (), "Old kimi extension skill should be removed"
371+
372+ # Extension registry should be updated
373+ registry = json .loads (
374+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
375+ )
376+ registered_commands = registry ["extensions" ]["git" ]["registered_commands" ]
377+ assert "opencode" in registered_commands
378+ assert "kimi" not in registered_commands
379+
380+ # Switch to claude
381+ result = _run_in_project (project , [
382+ "integration" , "switch" , "claude" ,
383+ "--script" , "sh" ,
384+ ])
385+ assert result .exit_code == 0 , result .output
386+
387+ # Git extension skills should exist for claude
388+ claude_git_feature = project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md"
389+ assert claude_git_feature .exists (), "Git extension skill should exist for claude"
390+
391+ # Old opencode extension commands should be removed
392+ assert not opencode_git_feature .exists (), "Old opencode extension command should be removed"
393+
394+ # Extension registry should be updated
395+ registry = json .loads (
396+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
397+ )
398+ registered_commands = registry ["extensions" ]["git" ]["registered_commands" ]
399+ assert "claude" in registered_commands
400+ assert "opencode" not in registered_commands
401+
402+ def test_switch_migrates_copilot_skills_extension_commands (self , tmp_path ):
403+ """Copilot --skills should receive extension skills, not .agent.md files."""
404+ project = _init_project (tmp_path , "opencode" )
405+
406+ result = _run_in_project (project , ["extension" , "add" , "git" ])
407+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
408+
409+ result = _run_in_project (project , [
410+ "integration" , "switch" , "copilot" ,
411+ "--script" , "sh" ,
412+ "--integration-options" , "--skills" ,
413+ ])
414+ assert result .exit_code == 0 , result .output
415+
416+ copilot_git_feature = project / ".github" / "skills" / "speckit-git-feature" / "SKILL.md"
417+ copilot_agent_file = project / ".github" / "agents" / "speckit.git.feature.agent.md"
418+ assert copilot_git_feature .exists (), "Git extension skill should exist for Copilot skills mode"
419+ assert not copilot_agent_file .exists (), "Copilot skills mode should not create extension .agent.md files"
420+
421+ # Verify Copilot-specific frontmatter: mode field should map from
422+ # skill name (speckit-git-feature) back to dot notation (speckit.git-feature)
423+ skill_content = copilot_git_feature .read_text (encoding = "utf-8" )
424+ assert "mode: speckit.git-feature" in skill_content , (
425+ "Copilot skill frontmatter should contain mode mapped from skill name"
426+ )
427+
428+ registry = json .loads (
429+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
430+ )
431+ git_meta = registry ["extensions" ]["git" ]
432+ assert "speckit-git-feature" in git_meta ["registered_skills" ]
433+ assert "copilot" not in git_meta ["registered_commands" ]
434+
435+ result = _run_in_project (project , [
436+ "integration" , "switch" , "opencode" ,
437+ "--script" , "sh" ,
438+ ])
439+ assert result .exit_code == 0 , result .output
440+
441+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
442+ assert opencode_git_feature .exists (), "Git extension command should exist for opencode"
443+ assert not copilot_git_feature .exists (), "Old Copilot extension skill should be removed"
444+
445+ registry = json .loads (
446+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
447+ )
448+ git_meta = registry ["extensions" ]["git" ]
449+ assert git_meta ["registered_skills" ] == []
450+ assert "opencode" in git_meta ["registered_commands" ]
451+ assert "copilot" not in git_meta ["registered_commands" ]
452+
453+ def test_switch_does_not_register_disabled_extensions (self , tmp_path ):
454+ """Disabled extensions should stay disabled and should not migrate commands."""
455+ project = _init_project (tmp_path , "opencode" )
456+
457+ result = _run_in_project (project , ["extension" , "add" , "git" ])
458+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
459+ result = _run_in_project (project , ["extension" , "disable" , "git" ])
460+ assert result .exit_code == 0 , result .output
461+
462+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
463+ assert opencode_git_feature .exists (), "Disabled extension command remains until integration switch"
464+
465+ result = _run_in_project (project , [
466+ "integration" , "switch" , "claude" ,
467+ "--script" , "sh" ,
468+ ])
469+ assert result .exit_code == 0 , result .output
470+
471+ claude_git_feature = project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md"
472+ assert not claude_git_feature .exists (), "Disabled extension should not be registered for new agent"
473+ assert not opencode_git_feature .exists (), "Old disabled extension command should be removed on switch"
474+
475+ registry = json .loads (
476+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
477+ )
478+ git_meta = registry ["extensions" ]["git" ]
479+ assert git_meta ["enabled" ] is False
480+ assert "claude" not in git_meta ["registered_commands" ]
481+ assert "opencode" not in git_meta ["registered_commands" ]
482+
337483 def test_switch_preserves_shared_infra (self , tmp_path ):
338484 """Switching preserves shared scripts, templates, and memory."""
339485 project = _init_project (tmp_path , "claude" )
0 commit comments