From 03bd9e14bf514aa26c9ca859c4f9ebff86814edc Mon Sep 17 00:00:00 2001 From: ozakidai <159523628+ozakidai@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:26:57 +0900 Subject: [PATCH] Fix Windows command resolution for agent launch --- src/ucode/launcher.py | 5 ++++- tests/test_launcher.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ucode/launcher.py b/src/ucode/launcher.py index 7bd0b3b53..000c486e7 100644 --- a/src/ucode/launcher.py +++ b/src/ucode/launcher.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import shutil import signal import subprocess import sys @@ -25,7 +26,9 @@ def exec_or_spawn(argv: list[str]) -> None: os.execvp(argv[0], argv) return # unreachable on POSIX; keeps type-checkers happy - proc = subprocess.Popen(argv) + # Resolve npm .cmd shims because CreateProcess does not honor PATHEXT. + executable = shutil.which(argv[0]) or argv[0] + proc = subprocess.Popen([executable, *argv[1:]]) try: returncode = proc.wait() except KeyboardInterrupt: diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 07e66a85c..9cae8a6c8 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -29,6 +29,7 @@ def test_windows_spawns_and_waits(self): with ( patch.object(launcher.os, "name", "nt"), patch.object(launcher.os, "execvp") as execvp, + patch.object(launcher.shutil, "which", return_value=None), patch.object(launcher.subprocess, "Popen", return_value=proc) as popen, ): with pytest.raises(SystemExit) as exc: @@ -38,11 +39,27 @@ def test_windows_spawns_and_waits(self): proc.wait.assert_called_once() assert exc.value.code == 0 + def test_windows_resolves_npm_command_shim(self): + proc = MagicMock() + proc.wait.return_value = 0 + resolved = r"C:\npm\claude.CMD" + with ( + patch.object(launcher.os, "name", "nt"), + patch.object(launcher.shutil, "which", return_value=resolved) as which, + patch.object(launcher.subprocess, "Popen", return_value=proc) as popen, + ): + with pytest.raises(SystemExit) as exc: + launcher.exec_or_spawn(["claude", "--settings", "x"]) + which.assert_called_once_with("claude") + popen.assert_called_once_with([resolved, "--settings", "x"]) + assert exc.value.code == 0 + def test_windows_propagates_child_exit_code(self): proc = MagicMock() proc.wait.return_value = 42 with ( patch.object(launcher.os, "name", "nt"), + patch.object(launcher.shutil, "which", return_value=None), patch.object(launcher.subprocess, "Popen", return_value=proc), ): with pytest.raises(SystemExit) as exc: @@ -55,6 +72,7 @@ def test_windows_keyboard_interrupt_forwards_sigint(self): proc.wait.side_effect = [KeyboardInterrupt(), 130] with ( patch.object(launcher.os, "name", "nt"), + patch.object(launcher.shutil, "which", return_value=None), patch.object(launcher.subprocess, "Popen", return_value=proc), ): with pytest.raises(SystemExit) as exc: