diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index 1a8285d7c4..9aa549c777 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -49,11 +49,22 @@ def _copilot_executable() -> str: """Return the executable name for Copilot CLI on this platform. - On Windows, subprocess invocation is reliable with `copilot.cmd`. + On Windows, the Copilot CLI may be installed as `copilot.exe` (e.g. a + standalone installer, winget, scoop) or as a `copilot.cmd` npm shim. + Probe `PATH` for whichever is actually present instead of assuming the + npm-style shim. """ - if os.name == "nt": - return "copilot.cmd" - return "copilot" + if os.name != "nt": + return "copilot" + + for candidate in ("copilot.exe", "copilot.cmd", "copilot"): + if shutil.which(candidate): + return candidate + + # Nothing found on PATH — keep the historical default so the + # resulting "command not found" error still references the + # previously expected name. + return "copilot.cmd" def _allow_all() -> bool: diff --git a/tests/integrations/test_extra_args.py b/tests/integrations/test_extra_args.py index 0ab68cb43a..44c488263c 100644 --- a/tests/integrations/test_extra_args.py +++ b/tests/integrations/test_extra_args.py @@ -639,6 +639,45 @@ def test_executable_env_var_copilot_unset_uses_platform_default(monkeypatch): assert args[0] == _copilot_executable() +def test_copilot_executable_windows_prefers_exe_on_path(monkeypatch): + """On Windows, `_copilot_executable()` must detect a `copilot.exe` + install rather than assuming the npm `copilot.cmd` shim (#4755).""" + import shutil + + from specify_cli.integrations.copilot import _copilot_executable + + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr( + shutil, "which", lambda name: r"C:\tools\copilot.exe" if name == "copilot.exe" else None + ) + assert _copilot_executable() == "copilot.exe" + + +def test_copilot_executable_windows_falls_back_to_cmd_shim(monkeypatch): + """A Windows install exposing only `copilot.cmd` (npm shim) still works.""" + import shutil + + from specify_cli.integrations.copilot import _copilot_executable + + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr( + shutil, "which", lambda name: r"C:\tools\copilot.cmd" if name == "copilot.cmd" else None + ) + assert _copilot_executable() == "copilot.cmd" + + +def test_copilot_executable_windows_nothing_on_path_keeps_historical_default(monkeypatch): + """Nothing found on PATH keeps the historical `copilot.cmd` default so + the resulting error still names the previously expected executable.""" + import shutil + + from specify_cli.integrations.copilot import _copilot_executable + + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr(shutil, "which", lambda name: None) + assert _copilot_executable() == "copilot.cmd" + + def test_executable_env_var_copilot_dispatch_command(monkeypatch): """CopilotIntegration.dispatch_command honours the executable env var.""" import subprocess