Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/specify_cli/integrations/copilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@


def _copilot_executable() -> str:
"""Return the executable name for Copilot CLI on this platform.
"""Return the available Copilot CLI executable for this platform."""
if os.name != "nt":
return "copilot"

On Windows, subprocess invocation is reliable with `copilot.cmd`.
"""
if os.name == "nt":
return "copilot.cmd"
return "copilot"
for candidate in ("copilot.exe", "copilot.cmd", "copilot"):
if shutil.which(candidate):
return candidate

return "copilot.cmd"


def _allow_all() -> bool:
Expand Down Expand Up @@ -292,10 +294,8 @@ def options(cls) -> list[IntegrationOption]:
def _resolve_executable(self) -> str:
"""Return the Copilot CLI executable, respecting the env-var override.

Checks ``SPECKIT_INTEGRATION_COPILOT_EXECUTABLE`` first. Falls
back to the platform-specific default from ``_copilot_executable()``
(``copilot.cmd`` on Windows, ``copilot`` elsewhere) so that
existing behaviour is preserved when the env var is unset.
Checks ``SPECKIT_INTEGRATION_COPILOT_EXECUTABLE`` first, then
discovers the platform-specific default from ``_copilot_executable``.
"""
env_name = "SPECKIT_INTEGRATION_COPILOT_EXECUTABLE"
override = os.environ.get(env_name, "").strip()
Expand Down
70 changes: 70 additions & 0 deletions tests/integrations/test_extra_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,76 @@ def test_executable_env_var_copilot_unset_uses_platform_default(monkeypatch):
assert args[0] == _copilot_executable()


def test_copilot_windows_discovers_exe_on_path(monkeypatch):
"""A native Windows executable is selected when it is the only match."""
import specify_cli.integrations.copilot as copilot

monkeypatch.setattr(copilot.os, "name", "nt")
monkeypatch.setattr(
copilot.shutil,
"which",
lambda candidate: r"C:\Tools\copilot.exe" if candidate == "copilot.exe" else None,
)

assert copilot._copilot_executable() == "copilot.exe"


def test_copilot_windows_prefers_exe_over_cmd(monkeypatch):
"""Windows executable discovery uses the documented candidate order."""
import specify_cli.integrations.copilot as copilot

monkeypatch.setattr(copilot.os, "name", "nt")
monkeypatch.setattr(
copilot.shutil,
"which",
lambda candidate: candidate if candidate in {"copilot.exe", "copilot.cmd"} else None,
)

assert copilot._copilot_executable() == "copilot.exe"


def test_copilot_windows_keeps_historical_fallback(monkeypatch):
"""Missing Windows candidates retain the historical executable name."""
import specify_cli.integrations.copilot as copilot

monkeypatch.setattr(copilot.os, "name", "nt")
monkeypatch.setattr(copilot.shutil, "which", lambda candidate: None)

assert copilot._copilot_executable() == "copilot.cmd"


def test_copilot_executable_env_var_precedes_path_discovery(monkeypatch):
"""The explicit executable override remains higher priority than PATH."""
import specify_cli.integrations.copilot as copilot

monkeypatch.setattr(copilot.os, "name", "nt")
monkeypatch.setattr(copilot.shutil, "which", lambda candidate: candidate)
monkeypatch.setenv("SPECKIT_INTEGRATION_COPILOT_EXECUTABLE", "/opt/copilot")

assert copilot.CopilotIntegration()._resolve_executable() == "/opt/copilot"


def test_copilot_dispatch_command_uses_path_discovery(monkeypatch):
"""Command dispatch uses the executable discovered on Windows PATH."""
import specify_cli.integrations.copilot as copilot
import subprocess

capture = _RunCapture()
monkeypatch.setattr(copilot.os, "name", "nt")
monkeypatch.setattr(
copilot.shutil,
"which",
lambda candidate: r"C:\Tools\copilot.exe" if candidate == "copilot.exe" else None,
)
monkeypatch.setattr(subprocess, "run", capture)
monkeypatch.setenv("SPECKIT_COPILOT_ALLOW_ALL_TOOLS", "0")

copilot.CopilotIntegration().dispatch_command("speckit.plan", args="body", stream=False)

assert capture.captured_args is not None
assert capture.captured_args[0] == "copilot.exe"


def test_executable_env_var_copilot_dispatch_command(monkeypatch):
"""CopilotIntegration.dispatch_command honours the executable env var."""
import subprocess
Expand Down
Loading