mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 23:12:22 +02:00
The Windows branch of `_create_bash_subprocess` spawned Git Bash with neither pipes nor the env it was handed. `proc.stdout` and `proc.stderr` came back `None`, so `_run_subprocess_streaming`'s reader returned immediately and the Bash tool reported `"(no output)"` alongside the real exit code — while the child inherited the server's own stdout/stderr and wrote agent command output into the console and the launchd/Docker logs. The `env` parameter was accepted and never used, so `PATH`, `VIRTUAL_ENV`, `HOME`, `TMPDIR` and the configured import paths carried in `ctx["subproc_env"]` never reached the child on Windows, even though every POSIX path applies them. Spawn it the way the POSIX path at `:688` already does: `stdin=DEVNULL`, `stdout=PIPE`, `stderr=PIPE`, `env=env`. `website/configuration-reference.md` is generated from source line numbers, so the four added lines shift one entry; regenerated with `scripts/generate_env_reference.py`.
208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
"""Windows execution contract for the agent Bash tool."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
from types import SimpleNamespace
|
|
|
|
from src.agent_tools import subprocess_tools
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_uses_git_bash_with_structural_cwd(monkeypatch):
|
|
captured = {}
|
|
bash = r"C:\Program Files\Git\bin\bash.exe"
|
|
workspace = r"D:\Workspaces\Project with spaces"
|
|
process = object()
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(subprocess_tools, "find_bash", lambda: bash)
|
|
|
|
async def fake_exec(*argv, **kwargs):
|
|
captured["argv"] = argv
|
|
captured["kwargs"] = kwargs
|
|
return process
|
|
|
|
async def fail_shell(*_args, **_kwargs):
|
|
pytest.fail("native Windows Bash must not execute through cmd.exe")
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_shell)
|
|
|
|
result = await subprocess_tools._create_bash_subprocess(
|
|
"pwd; cat package.json",
|
|
cwd=workspace,
|
|
env={"HOME": r"C:\Odysseus\data"},
|
|
)
|
|
|
|
assert result is process
|
|
assert captured["argv"] == (bash, "-c", "pwd; cat package.json")
|
|
assert captured["kwargs"]["cwd"] == workspace
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_captures_output_instead_of_inheriting_server_handles(monkeypatch):
|
|
captured = {}
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
|
|
)
|
|
|
|
async def fake_exec(*_argv, **kwargs):
|
|
captured.update(kwargs)
|
|
return object()
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
|
|
|
|
await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work")
|
|
|
|
assert captured["stdout"] == asyncio.subprocess.PIPE
|
|
assert captured["stderr"] == asyncio.subprocess.PIPE
|
|
assert captured["stdin"] == asyncio.subprocess.DEVNULL
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_applies_the_subprocess_env(monkeypatch):
|
|
captured = {}
|
|
env = {"PATH": r"C:\Odysseus\venv\Scripts", "HOME": r"C:\Odysseus\data"}
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
|
|
)
|
|
|
|
async def fake_exec(*_argv, **kwargs):
|
|
captured.update(kwargs)
|
|
return object()
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
|
|
|
|
await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work", env=env)
|
|
|
|
assert captured["env"] == env
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_tool_passes_ctx_env_through_to_the_child(monkeypatch):
|
|
captured = {}
|
|
env = {"PATH": r"C:\Odysseus\venv\Scripts", "VIRTUAL_ENV": r"C:\Odysseus\venv"}
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
subprocess_tools, "find_bash", lambda: r"C:\Program Files\Git\bin\bash.exe"
|
|
)
|
|
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: r"D:\Workspaces\Project")
|
|
|
|
async def fake_exec(*argv, **kwargs):
|
|
captured["argv"] = argv
|
|
captured["kwargs"] = kwargs
|
|
return SimpleNamespace(pid=4242)
|
|
|
|
async def fake_stream(_process, **_kwargs):
|
|
return "ok", "", 0, False
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec)
|
|
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
|
|
|
|
result = await subprocess_tools.BashTool().execute(
|
|
"pwd",
|
|
{"subproc_env": env, "session_id": "chat-1"},
|
|
)
|
|
|
|
assert result == {"output": "ok", "exit_code": 0}
|
|
assert captured["kwargs"]["env"] == env
|
|
assert captured["kwargs"]["stdout"] == asyncio.subprocess.PIPE
|
|
assert captured["kwargs"]["stderr"] == asyncio.subprocess.PIPE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_without_git_bash_fails_clearly(monkeypatch):
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(subprocess_tools, "find_bash", lambda: None)
|
|
|
|
async def fail_spawn(*_args, **_kwargs):
|
|
pytest.fail("no subprocess should start without Git Bash")
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fail_spawn)
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_spawn)
|
|
|
|
with pytest.raises(RuntimeError, match="Git Bash is required"):
|
|
await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypatch):
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(subprocess_tools, "find_bash", lambda: None)
|
|
|
|
result = await subprocess_tools.BashTool().execute(
|
|
"pwd",
|
|
{"subproc_env": {}, "session_id": None},
|
|
)
|
|
|
|
assert result["exit_code"] == 1
|
|
assert "install Git for Windows" in result["error"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch):
|
|
captured = {}
|
|
workspace = r"D:\Workspaces\Project with spaces"
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True)
|
|
monkeypatch.setattr(
|
|
subprocess_tools.shutil,
|
|
"which",
|
|
lambda name: r"C:\msys64\usr\bin\tmux.exe",
|
|
)
|
|
monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: workspace)
|
|
|
|
async def fail_tmux(*_args, **_kwargs):
|
|
pytest.fail("native Windows must not enter the POSIX tmux path")
|
|
|
|
async def fake_create(command, **kwargs):
|
|
captured["command"] = command
|
|
captured["kwargs"] = kwargs
|
|
return SimpleNamespace(pid=12345)
|
|
|
|
async def fake_stream(_process, **_kwargs):
|
|
return "ok", "", 0, False
|
|
|
|
monkeypatch.setattr(subprocess_tools, "_run_tmux_bash", fail_tmux)
|
|
monkeypatch.setattr(subprocess_tools, "_create_bash_subprocess", fake_create)
|
|
monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream)
|
|
|
|
result = await subprocess_tools.BashTool().execute(
|
|
"pwd",
|
|
{"subproc_env": {}, "session_id": "chat-1"},
|
|
)
|
|
|
|
assert result == {"output": "ok", "exit_code": 0}
|
|
assert captured["command"] == "pwd"
|
|
assert captured["kwargs"]["cwd"] == workspace
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_posix_bash_keeps_existing_shell_path(monkeypatch):
|
|
captured = {}
|
|
process = object()
|
|
|
|
monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", False)
|
|
|
|
async def fake_shell(command, **kwargs):
|
|
captured["command"] = command
|
|
captured["kwargs"] = kwargs
|
|
return process
|
|
|
|
async def fail_exec(*_args, **_kwargs):
|
|
pytest.fail("POSIX behavior must continue through create_subprocess_shell")
|
|
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fake_shell)
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fail_exec)
|
|
|
|
result = await subprocess_tools._create_bash_subprocess("pwd", cwd="/tmp/work")
|
|
|
|
assert result is process
|
|
assert captured == {"command": "pwd", "kwargs": {"cwd": "/tmp/work"}}
|