mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(workflows): dispatch prompt steps via the resolved executable (#3793)
PromptStep._try_dispatch runs `subprocess.run(exec_args, ...)` with an
UNRESOLVED argv[0] -- a bare name like `claude`. On Windows subprocess.run calls
CreateProcess, which does not consult PATHEXT, so an agent CLI installed as a
`.cmd`/`.bat` shim (the usual npm layout) raises FileNotFoundError [WinError 2].
That OSError is swallowed by the method's `except OSError: return None`, and
execute() then reports "CLI not found or not installed" -- even though the
step's own preflight `shutil.which(...)` two lines earlier just found it.
The sibling path does not have this bug: IntegrationBase.dispatch_command (used
by the `command` step) resolves argv[0] through shutil.which first, added in
8e5643d for exactly this reason. Same machine, same integration, CLI present as
a .cmd shim:
type: prompt -> failed "integration 'claude' CLI not found or not installed."
type: command -> completed
Primitive confirmation: bare `subprocess.run(["fakeagent"])` raises
[WinError 2] while `subprocess.run([shutil.which("fakeagent")])` runs fine.
Reuse the path the preflight already resolved (`fallback_cli_path`) instead of
calling which() again, so the shim is executed. On POSIX it is the same
executable, so behaviour is unchanged there.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,17 @@ class PromptStep(StepBase):
|
||||
if not exec_args:
|
||||
return None
|
||||
|
||||
# Windows: ``subprocess.run`` calls ``CreateProcess``, which does not
|
||||
# consult ``PATHEXT``, so a bare command name like ``claude`` installed
|
||||
# as ``claude.cmd`` (the usual npm shim layout) fails with
|
||||
# ``WinError 2``. That OSError is swallowed below and reported as "CLI
|
||||
# not found or not installed" -- even though the preflight above just
|
||||
# found it. Reuse the already-resolved path so the shim is executed,
|
||||
# mirroring ``IntegrationBase.dispatch_command``, which the ``command``
|
||||
# step already goes through. On POSIX this is the same executable.
|
||||
if fallback_cli_path:
|
||||
exec_args = [fallback_cli_path, *exec_args[1:]]
|
||||
|
||||
import subprocess
|
||||
|
||||
project_root = (
|
||||
|
||||
@@ -1486,6 +1486,41 @@ class TestPromptStep:
|
||||
assert result.output["dispatched"] is True
|
||||
assert result.output["exit_code"] == 0
|
||||
|
||||
def test_try_dispatch_executes_the_resolved_executable(self, tmp_path):
|
||||
"""argv[0] must be the shutil.which-resolved path, not the bare name.
|
||||
|
||||
On Windows subprocess.run calls CreateProcess, which ignores PATHEXT, so
|
||||
a bare `claude` installed as `claude.cmd` (the usual npm shim) raises
|
||||
WinError 2. That OSError is swallowed and reported as "CLI not found or
|
||||
not installed" even though the preflight which() just found it, while
|
||||
the `command` step -- which goes through
|
||||
IntegrationBase.dispatch_command -- resolves argv[0] and works.
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
from specify_cli.workflows.steps.prompt import PromptStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = PromptStep()
|
||||
ctx = StepContext(default_integration="claude", project_root=str(tmp_path))
|
||||
config = {"id": "test", "type": "prompt", "prompt": "hello"}
|
||||
|
||||
resolved = r"C:\tools\claude.CMD"
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = ""
|
||||
|
||||
with patch(
|
||||
"specify_cli.workflows.steps.prompt.shutil.which",
|
||||
lambda name: resolved,
|
||||
), patch("subprocess.run", return_value=mock_result) as run:
|
||||
result = step.execute(config, ctx)
|
||||
|
||||
assert result.status == StepStatus.COMPLETED
|
||||
assert result.output["dispatched"] is True
|
||||
argv = run.call_args.args[0]
|
||||
assert argv[0] == resolved, argv
|
||||
|
||||
def test_dispatch_with_mock_cli(self, tmp_path):
|
||||
from unittest.mock import patch, MagicMock
|
||||
from specify_cli.workflows.steps.prompt import PromptStep
|
||||
|
||||
Reference in New Issue
Block a user