Skip to content
Merged
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
10 changes: 9 additions & 1 deletion swe_af/runtime/codex_harness_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,16 @@ async def _run_codex_cli_with_stdin(
env: dict[str, str] | None,
cwd: str | None,
) -> tuple[str, str, int]:
# Windows: CreateProcess does no PATHEXT resolution, and npm installs the
# codex CLI only as a .cmd shim — spawning the bare name raises
# FileNotFoundError ([WinError 2]) on every call even though the shell
# finds it. resolve_cli_command resolves the shim's real path via
# shutil.which (a no-op on POSIX and for names that already carry a path).
from agentfield.harness._cli import resolve_cli_command

proc = await asyncio.create_subprocess_exec(
*cmd,
resolve_cli_command(cmd[0]),
*cmd[1:],
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
Expand Down
43 changes: 43 additions & 0 deletions tests/test_codex_harness_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,46 @@ def test_non_codex_prompt_suffix_keeps_agentfield_write_tool_default(tmp_path) -

assert "Write tool" in suffix
assert "Codex CLI" not in suffix


def test_run_codex_cli_spawns_via_resolved_command(monkeypatch) -> None:
"""The spawn must route cmd[0] through the SDK's resolve_cli_command.

On Windows, npm installs the codex CLI only as a .cmd shim, and
CreateProcess does no PATHEXT resolution — spawning the bare name fails
with FileNotFoundError ([WinError 2]) on every codex harness call.
resolve_cli_command resolves the shim's real path (no-op on POSIX).
"""
import asyncio

import agentfield.harness._cli as sdk_cli

from swe_af.runtime.codex_harness_patch import _run_codex_cli_with_stdin

spawned: dict = {}

class FakeProc:
returncode = 0

async def communicate(self, data: bytes) -> tuple[bytes, bytes]:
spawned["stdin"] = data
return b"", b""

async def fake_exec(program: str, *args: str, **kwargs: object) -> FakeProc:
spawned["program"] = program
spawned["args"] = args
return FakeProc()

monkeypatch.setattr(sdk_cli, "resolve_cli_command", lambda name: f"resolved::{name}")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_exec)

stdout, stderr, returncode = asyncio.run(
_run_codex_cli_with_stdin(
["codex", "exec", "--json"], "prompt", env=None, cwd=None
)
)

assert spawned["program"] == "resolved::codex"
assert spawned["args"][:2] == ("exec", "--json")
assert spawned["stdin"] == b"prompt"
assert (stdout, stderr, returncode) == ("", "", 0)
Loading