@@ -614,6 +614,47 @@ def test_dispatch_with_mock_cli(self, tmp_path, monkeypatch):
614614 # Claude is a SkillsIntegration so uses /speckit-specify
615615 assert "/speckit-specify login" in call_args [0 ][0 ][2 ]
616616
617+ def test_dispatch_uses_executable_override_for_preflight (self , tmp_path , monkeypatch ):
618+ """Command dispatch availability must follow build_exec_args() argv[0]."""
619+ from unittest .mock import MagicMock , patch
620+ from specify_cli .workflows .steps .command import CommandStep
621+ from specify_cli .workflows .base import StepContext , StepStatus
622+
623+ monkeypatch .setenv ("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE" , "/opt/claude" )
624+ seen_which : list [str ] = []
625+
626+ def fake_which (name : str ) -> str | None :
627+ seen_which .append (name )
628+ return name if name == "/opt/claude" else None
629+
630+ step = CommandStep ()
631+ ctx = StepContext (
632+ inputs = {"name" : "login" },
633+ default_integration = "claude" ,
634+ project_root = str (tmp_path ),
635+ )
636+ config = {
637+ "id" : "test" ,
638+ "command" : "speckit.specify" ,
639+ "input" : {"args" : "{{ inputs.name }}" },
640+ }
641+
642+ mock_result = MagicMock ()
643+ mock_result .returncode = 0
644+ mock_result .stdout = '{"result": "done"}'
645+ mock_result .stderr = ""
646+
647+ with patch ("specify_cli.workflows.steps.command.shutil.which" , side_effect = fake_which ), \
648+ patch ("subprocess.run" , return_value = mock_result ) as mock_run :
649+ result = step .execute (config , ctx )
650+
651+ assert result .status == StepStatus .COMPLETED
652+ assert result .output ["dispatched" ] is True
653+ assert seen_which == ["/opt/claude" ]
654+ call_args = mock_run .call_args
655+ assert call_args [0 ][0 ][0 ] == "/opt/claude"
656+ assert "/speckit-specify login" in call_args [0 ][0 ][2 ]
657+
617658 def test_dispatch_failure_returns_failed_status (self , tmp_path ):
618659 """When the CLI exits non-zero, the step should fail."""
619660 from unittest .mock import patch , MagicMock
@@ -734,6 +775,46 @@ def test_dispatch_with_mock_cli(self, tmp_path):
734775 assert result .output ["dispatched" ] is True
735776 assert result .output ["exit_code" ] == 0
736777
778+ def test_dispatch_uses_executable_override_for_preflight (self , tmp_path , monkeypatch ):
779+ """Prompt dispatch availability must follow build_exec_args() argv[0]."""
780+ from unittest .mock import MagicMock , patch
781+ from specify_cli .workflows .steps .prompt import PromptStep
782+ from specify_cli .workflows .base import StepContext , StepStatus
783+
784+ monkeypatch .setenv ("SPECKIT_INTEGRATION_CLAUDE_EXECUTABLE" , "/opt/claude" )
785+ seen_which : list [str ] = []
786+
787+ def fake_which (name : str ) -> str | None :
788+ seen_which .append (name )
789+ return name if name == "/opt/claude" else None
790+
791+ step = PromptStep ()
792+ ctx = StepContext (
793+ default_integration = "claude" ,
794+ project_root = str (tmp_path ),
795+ )
796+ config = {
797+ "id" : "ask" ,
798+ "type" : "prompt" ,
799+ "prompt" : "Explain this code" ,
800+ }
801+
802+ mock_result = MagicMock ()
803+ mock_result .returncode = 0
804+ mock_result .stdout = "Here is the explanation"
805+ mock_result .stderr = ""
806+
807+ with patch ("specify_cli.workflows.steps.prompt.shutil.which" , side_effect = fake_which ), \
808+ patch ("subprocess.run" , return_value = mock_result ) as mock_run :
809+ result = step .execute (config , ctx )
810+
811+ assert result .status == StepStatus .COMPLETED
812+ assert result .output ["dispatched" ] is True
813+ assert seen_which == ["/opt/claude" ]
814+ call_args = mock_run .call_args
815+ assert call_args [0 ][0 ][0 ] == "/opt/claude"
816+ assert call_args [0 ][0 ][2 ] == "Explain this code"
817+
737818 def test_validate_missing_prompt (self ):
738819 from specify_cli .workflows .steps .prompt import PromptStep
739820
0 commit comments