@@ -658,6 +658,47 @@ def test_dispatch_with_mock_cli(self, tmp_path, monkeypatch):
658658 # Claude is a SkillsIntegration so uses /speckit-specify
659659 assert "/speckit-specify login" in call_args [0 ][0 ][2 ]
660660
661+ def test_dispatch_uses_executable_override_for_fallback_preflight (self , tmp_path , monkeypatch ):
662+ """Command preflight falls back to build_exec_args() argv[0]."""
663+ from unittest .mock import MagicMock , patch
664+ from specify_cli .workflows .steps .command import CommandStep
665+ from specify_cli .workflows .base import StepContext , StepStatus
666+
667+ monkeypatch .setenv ("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE" , "/opt/claude" )
668+ seen_which : list [str ] = []
669+
670+ def fake_which (name : str ) -> str | None :
671+ seen_which .append (name )
672+ return name if name == "/opt/claude" else None
673+
674+ step = CommandStep ()
675+ ctx = StepContext (
676+ inputs = {"name" : "login" },
677+ default_integration = "claude" ,
678+ project_root = str (tmp_path ),
679+ )
680+ config = {
681+ "id" : "test" ,
682+ "command" : "speckit.specify" ,
683+ "input" : {"args" : "{{ inputs.name }}" },
684+ }
685+
686+ mock_result = MagicMock ()
687+ mock_result .returncode = 0
688+ mock_result .stdout = '{"result": "done"}'
689+ mock_result .stderr = ""
690+
691+ with patch ("specify_cli.workflows.steps.command.shutil.which" , side_effect = fake_which ), \
692+ patch ("subprocess.run" , return_value = mock_result ) as mock_run :
693+ result = step .execute (config , ctx )
694+
695+ assert result .status == StepStatus .COMPLETED
696+ assert result .output ["dispatched" ] is True
697+ assert seen_which [:2 ] == ["claude" , "/opt/claude" ]
698+ call_args = mock_run .call_args
699+ assert call_args [0 ][0 ][0 ] == "/opt/claude"
700+ assert "/speckit-specify login" in call_args [0 ][0 ][2 ]
701+
661702 def test_dispatch_failure_returns_failed_status (self , tmp_path ):
662703 """When the CLI exits non-zero, the step should fail."""
663704 from unittest .mock import patch , MagicMock
@@ -810,6 +851,46 @@ def test_dispatch_with_mock_cli(self, tmp_path):
810851 assert result .output ["dispatched" ] is True
811852 assert result .output ["exit_code" ] == 0
812853
854+ def test_dispatch_uses_executable_override_for_fallback_preflight (self , tmp_path , monkeypatch ):
855+ """Prompt preflight falls back to build_exec_args() argv[0]."""
856+ from unittest .mock import MagicMock , patch
857+ from specify_cli .workflows .steps .prompt import PromptStep
858+ from specify_cli .workflows .base import StepContext , StepStatus
859+
860+ monkeypatch .setenv ("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE" , "/opt/claude" )
861+ seen_which : list [str ] = []
862+
863+ def fake_which (name : str ) -> str | None :
864+ seen_which .append (name )
865+ return name if name == "/opt/claude" else None
866+
867+ step = PromptStep ()
868+ ctx = StepContext (
869+ default_integration = "claude" ,
870+ project_root = str (tmp_path ),
871+ )
872+ config = {
873+ "id" : "ask" ,
874+ "type" : "prompt" ,
875+ "prompt" : "Explain this code" ,
876+ }
877+
878+ mock_result = MagicMock ()
879+ mock_result .returncode = 0
880+ mock_result .stdout = "Here is the explanation"
881+ mock_result .stderr = ""
882+
883+ with patch ("specify_cli.workflows.steps.prompt.shutil.which" , side_effect = fake_which ), \
884+ patch ("subprocess.run" , return_value = mock_result ) as mock_run :
885+ result = step .execute (config , ctx )
886+
887+ assert result .status == StepStatus .COMPLETED
888+ assert result .output ["dispatched" ] is True
889+ assert seen_which [:2 ] == ["claude" , "/opt/claude" ]
890+ call_args = mock_run .call_args
891+ assert call_args [0 ][0 ][0 ] == "/opt/claude"
892+ assert call_args [0 ][0 ][2 ] == "Explain this code"
893+
813894 def test_validate_missing_prompt (self ):
814895 from specify_cli .workflows .steps .prompt import PromptStep
815896
0 commit comments