@@ -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,150 @@ 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+ old_cwd = os .getcwd ()
353+ try :
354+ os .chdir (project )
355+ result = runner .invoke (app , [
356+ "extension" , "add" , "git" ,
357+ ], catch_exceptions = False )
358+ finally :
359+ os .chdir (old_cwd )
360+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
361+
362+ # Verify git extension skills exist for kimi
363+ kimi_git_feature = project / ".kimi" / "skills" / "speckit-git-feature" / "SKILL.md"
364+ assert kimi_git_feature .exists (), "Git extension skill should exist for kimi"
365+
366+ try :
367+ os .chdir (project )
368+ result = runner .invoke (app , [
369+ "integration" , "switch" , "opencode" ,
370+ "--script" , "sh" ,
371+ ], catch_exceptions = False )
372+ finally :
373+ os .chdir (old_cwd )
374+ assert result .exit_code == 0 , result .output
375+
376+ # Git extension commands should exist for opencode
377+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
378+ assert opencode_git_feature .exists (), "Git extension command should exist for opencode"
379+
380+ # Old kimi extension skills should be removed
381+ assert not kimi_git_feature .exists (), "Old kimi extension skill should be removed"
382+
383+ # Extension registry should be updated
384+ registry = json .loads (
385+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
386+ )
387+ registered_commands = registry ["extensions" ]["git" ]["registered_commands" ]
388+ assert "opencode" in registered_commands
389+ assert "kimi" not in registered_commands
390+
391+ # Switch to claude
392+ try :
393+ os .chdir (project )
394+ result = runner .invoke (app , [
395+ "integration" , "switch" , "claude" ,
396+ "--script" , "sh" ,
397+ ], catch_exceptions = False )
398+ finally :
399+ os .chdir (old_cwd )
400+ assert result .exit_code == 0 , result .output
401+
402+ # Git extension skills should exist for claude
403+ claude_git_feature = project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md"
404+ assert claude_git_feature .exists (), "Git extension skill should exist for claude"
405+
406+ # Old opencode extension commands should be removed
407+ assert not opencode_git_feature .exists (), "Old opencode extension command should be removed"
408+
409+ # Extension registry should be updated
410+ registry = json .loads (
411+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
412+ )
413+ registered_commands = registry ["extensions" ]["git" ]["registered_commands" ]
414+ assert "claude" in registered_commands
415+ assert "opencode" not in registered_commands
416+
417+ def test_switch_migrates_copilot_skills_extension_commands (self , tmp_path ):
418+ """Copilot --skills should receive extension skills, not .agent.md files."""
419+ project = _init_project (tmp_path , "opencode" )
420+
421+ result = _run_in_project (project , ["extension" , "add" , "git" ])
422+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
423+
424+ result = _run_in_project (project , [
425+ "integration" , "switch" , "copilot" ,
426+ "--script" , "sh" ,
427+ "--integration-options" , "--skills" ,
428+ ])
429+ assert result .exit_code == 0 , result .output
430+
431+ copilot_git_feature = project / ".github" / "skills" / "speckit-git-feature" / "SKILL.md"
432+ copilot_agent_file = project / ".github" / "agents" / "speckit.git.feature.agent.md"
433+ assert copilot_git_feature .exists (), "Git extension skill should exist for Copilot skills mode"
434+ assert not copilot_agent_file .exists (), "Copilot skills mode should not create extension .agent.md files"
435+
436+ registry = json .loads (
437+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
438+ )
439+ git_meta = registry ["extensions" ]["git" ]
440+ assert "speckit-git-feature" in git_meta ["registered_skills" ]
441+ assert "copilot" not in git_meta ["registered_commands" ]
442+
443+ result = _run_in_project (project , [
444+ "integration" , "switch" , "opencode" ,
445+ "--script" , "sh" ,
446+ ])
447+ assert result .exit_code == 0 , result .output
448+
449+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
450+ assert opencode_git_feature .exists (), "Git extension command should exist for opencode"
451+ assert not copilot_git_feature .exists (), "Old Copilot extension skill should be removed"
452+
453+ registry = json .loads (
454+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
455+ )
456+ git_meta = registry ["extensions" ]["git" ]
457+ assert git_meta ["registered_skills" ] == []
458+ assert "opencode" in git_meta ["registered_commands" ]
459+ assert "copilot" not in git_meta ["registered_commands" ]
460+
461+ def test_switch_does_not_register_disabled_extensions (self , tmp_path ):
462+ """Disabled extensions should stay disabled and should not migrate commands."""
463+ project = _init_project (tmp_path , "opencode" )
464+
465+ result = _run_in_project (project , ["extension" , "add" , "git" ])
466+ assert result .exit_code == 0 , f"extension add failed: { result .output } "
467+ result = _run_in_project (project , ["extension" , "disable" , "git" ])
468+ assert result .exit_code == 0 , result .output
469+
470+ opencode_git_feature = project / ".opencode" / "command" / "speckit.git.feature.md"
471+ assert opencode_git_feature .exists (), "Disabled extension command remains until integration switch"
472+
473+ result = _run_in_project (project , [
474+ "integration" , "switch" , "claude" ,
475+ "--script" , "sh" ,
476+ ])
477+ assert result .exit_code == 0 , result .output
478+
479+ claude_git_feature = project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md"
480+ assert not claude_git_feature .exists (), "Disabled extension should not be registered for new agent"
481+ assert not opencode_git_feature .exists (), "Old disabled extension command should be removed on switch"
482+
483+ registry = json .loads (
484+ (project / ".specify" / "extensions" / ".registry" ).read_text (encoding = "utf-8" )
485+ )
486+ git_meta = registry ["extensions" ]["git" ]
487+ assert git_meta ["enabled" ] is False
488+ assert "claude" not in git_meta ["registered_commands" ]
489+ assert "opencode" not in git_meta ["registered_commands" ]
490+
337491 def test_switch_preserves_shared_infra (self , tmp_path ):
338492 """Switching preserves shared scripts, templates, and memory."""
339493 project = _init_project (tmp_path , "claude" )
0 commit comments