@@ -394,3 +394,125 @@ def test_install_modify_uninstall_preserves_modified(self, tmp_path):
394394 assert plan_file .read_text (encoding = "utf-8" ) == "# user customization\n "
395395 finally :
396396 os .chdir (old_cwd )
397+
398+
399+ # ── Edge-case fixes ─────────────────────────────────────────────────
400+
401+
402+ class TestScriptTypeValidation :
403+ def test_invalid_script_type_rejected (self , tmp_path ):
404+ """--script with an invalid value should fail with a clear error."""
405+ project = tmp_path / "proj"
406+ project .mkdir ()
407+ (project / ".specify" ).mkdir ()
408+ old_cwd = os .getcwd ()
409+ try :
410+ os .chdir (project )
411+ result = runner .invoke (app , [
412+ "integration" , "install" , "claude" ,
413+ "--script" , "bash" ,
414+ ])
415+ finally :
416+ os .chdir (old_cwd )
417+ assert result .exit_code != 0
418+ assert "Invalid script type" in result .output
419+
420+ def test_valid_script_types_accepted (self , tmp_path ):
421+ """Both 'sh' and 'ps' should be accepted."""
422+ project = tmp_path / "proj"
423+ project .mkdir ()
424+ (project / ".specify" ).mkdir ()
425+ old_cwd = os .getcwd ()
426+ try :
427+ os .chdir (project )
428+ result = runner .invoke (app , [
429+ "integration" , "install" , "claude" ,
430+ "--script" , "sh" ,
431+ ], catch_exceptions = False )
432+ finally :
433+ os .chdir (old_cwd )
434+ assert result .exit_code == 0
435+
436+
437+ class TestParseIntegrationOptionsEqualsForm :
438+ def test_equals_form_parsed (self ):
439+ """--commands-dir=./x should be parsed the same as --commands-dir ./x."""
440+ from specify_cli import _parse_integration_options
441+ from specify_cli .integrations import get_integration
442+
443+ integration = get_integration ("generic" )
444+ assert integration is not None
445+
446+ result_space = _parse_integration_options (integration , "--commands-dir ./mydir" )
447+ result_equals = _parse_integration_options (integration , "--commands-dir=./mydir" )
448+ assert result_space is not None
449+ assert result_equals is not None
450+ assert result_space ["commands_dir" ] == "./mydir"
451+ assert result_equals ["commands_dir" ] == "./mydir"
452+
453+
454+ class TestUninstallNoManifestClearsInitOptions :
455+ def test_init_options_cleared_on_no_manifest_uninstall (self , tmp_path ):
456+ """When no manifest exists, uninstall should still clear init-options.json."""
457+ project = tmp_path / "proj"
458+ project .mkdir ()
459+ (project / ".specify" ).mkdir ()
460+
461+ # Write integration.json and init-options.json without a manifest
462+ int_json = project / ".specify" / "integration.json"
463+ int_json .write_text (json .dumps ({"integration" : "claude" }), encoding = "utf-8" )
464+
465+ opts_json = project / ".specify" / "init-options.json"
466+ opts_json .write_text (json .dumps ({
467+ "integration" : "claude" ,
468+ "ai" : "claude" ,
469+ "ai_skills" : True ,
470+ "script" : "sh" ,
471+ }), encoding = "utf-8" )
472+
473+ old_cwd = os .getcwd ()
474+ try :
475+ os .chdir (project )
476+ result = runner .invoke (app , ["integration" , "uninstall" , "claude" ])
477+ finally :
478+ os .chdir (old_cwd )
479+ assert result .exit_code == 0
480+
481+ # init-options.json should have integration keys cleared
482+ opts = json .loads (opts_json .read_text (encoding = "utf-8" ))
483+ assert "integration" not in opts
484+ assert "ai" not in opts
485+ assert "ai_skills" not in opts
486+ # Non-integration keys preserved
487+ assert opts .get ("script" ) == "sh"
488+
489+
490+ class TestSwitchClearsMetadataAfterTeardown :
491+ def test_metadata_cleared_between_phases (self , tmp_path ):
492+ """If install phase fails during switch, metadata should not reference the removed integration."""
493+ project = _init_project (tmp_path , "claude" )
494+
495+ # Verify initial state
496+ int_json = project / ".specify" / "integration.json"
497+ assert json .loads (int_json .read_text (encoding = "utf-8" ))["integration" ] == "claude"
498+
499+ old_cwd = os .getcwd ()
500+ try :
501+ os .chdir (project )
502+ # Switch to copilot — should succeed and update metadata
503+ result = runner .invoke (app , [
504+ "integration" , "switch" , "copilot" ,
505+ "--script" , "sh" ,
506+ ], catch_exceptions = False )
507+ finally :
508+ os .chdir (old_cwd )
509+ assert result .exit_code == 0
510+
511+ # integration.json should reference copilot, not claude
512+ data = json .loads (int_json .read_text (encoding = "utf-8" ))
513+ assert data ["integration" ] == "copilot"
514+
515+ # init-options.json should reference copilot
516+ opts_json = project / ".specify" / "init-options.json"
517+ opts = json .loads (opts_json .read_text (encoding = "utf-8" ))
518+ assert opts .get ("ai" ) == "copilot"
0 commit comments