fix: harden Windows exec shell default

Maintainer edit: prefer pwsh when available so single-line commands support modern PowerShell operators, add -NonInteractive, and parse explicit cmd.exe paths with Windows path semantics for cross-platform tests.
This commit is contained in:
chengyongru
2026-07-06 12:11:25 +08:00
committed by Xubin Ren
parent 33b1c6f601
commit 220de320fb
2 changed files with 49 additions and 8 deletions
+45 -4
View File
@@ -6,6 +6,7 @@ platform-specific binaries (all subprocess calls are mocked).
"""
import asyncio
import shutil
import sys
from unittest.mock import AsyncMock, patch
@@ -127,8 +128,9 @@ class TestSpawnWindows:
await ExecTool._spawn("dir", r"C:\work", env)
args = mock_exec.call_args[0]
assert "powershell" in args[0].lower()
assert any(shell in args[0].lower() for shell in ("pwsh", "powershell"))
assert "-NoProfile" in args
assert "-NonInteractive" in args
assert "-Command" in args
assert "dir" in args[-1]
@@ -161,8 +163,9 @@ class TestSpawnWindows:
await ExecTool._spawn('python -c "print(1)\nprint(2)"', r"C:\work", env)
args = mock_exec.call_args[0]
assert "powershell" in args[0].lower()
assert any(shell in args[0].lower() for shell in ("pwsh", "powershell"))
assert "-NoProfile" in args
assert "-NonInteractive" in args
assert "-Command" in args
assert "print(1)" in args[-1]
assert "print(2)" in args[-1]
@@ -189,6 +192,28 @@ class TestSpawnWindows:
kwargs = mock_shell.call_args[1]
assert kwargs["cwd"] == r"C:\work"
@pytest.mark.asyncio
async def test_prefers_pwsh_when_available(self):
env = {"PATH": ""}
def fake_which(command):
if command == "pwsh":
return r"C:\Program Files\PowerShell\7\pwsh.exe"
if command == "powershell":
return r"C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe"
return None
with (
patch("nanobot.agent.tools.shell._IS_WINDOWS", True),
patch("nanobot.agent.tools.shell.shutil.which", side_effect=fake_which),
patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec,
):
mock_exec.return_value = AsyncMock()
await ExecTool._spawn("dir", r"C:\work", env)
args = mock_exec.call_args[0]
assert "pwsh" in args[0].lower()
# ---------------------------------------------------------------------------
# path_append
@@ -532,7 +557,7 @@ class TestWindowsMultilineExec:
assert "2" in result
assert "Exit code: 0" in result
args = mock_exec.call_args[0]
assert "powershell" in args[0].lower()
assert any(shell in args[0].lower() for shell in ("pwsh", "powershell"))
@pytest.mark.asyncio
async def test_multiline_node_uses_powershell(self):
@@ -551,7 +576,7 @@ class TestWindowsMultilineExec:
assert "1" in result
args = mock_exec.call_args[0]
assert "powershell" in args[0].lower()
assert any(shell in args[0].lower() for shell in ("pwsh", "powershell"))
@pytest.mark.asyncio
async def test_single_line_uses_powershell(self):
@@ -615,6 +640,7 @@ class TestResolveShellWindows:
assert "hello" in result
args = mock_exec.call_args[0]
assert "powershell" in args[0].lower()
assert "-NonInteractive" in args
@pytest.mark.asyncio
async def test_shell_cmd_accepted(self):
@@ -644,3 +670,18 @@ class TestResolveShellWindows:
assert "Error: unsupported shell" in result
assert "Allowed: powershell, pwsh, cmd" in result
@pytest.mark.skipif(
sys.platform != "win32" or shutil.which("pwsh") is None,
reason="requires Windows with PowerShell 7",
)
class TestWindowsRealExec:
@pytest.mark.asyncio
async def test_single_line_and_separator_uses_pwsh(self):
result = await ExecTool(timeout=10).execute(command="echo before && echo after")
assert "before" in result
assert "after" in result
assert "Exit code: 0" in result