fix(tools): resolve MSYS paths in file tools on Windows

Git Bash hands file tools paths like /c/Users/... which Path() on native
Windows treats as relative \\c\\Users\\... under the process cwd. Reuse
local._msys_to_windows_path (extended for /cygdrive and /mnt drive forms)
in _resolve_path_for_task / _resolve_base_dir so read/write/search land on
the real drive. Container/WSL Linux paths are left untouched.

Salvages #50488 (drops unrelated desktop artifact commit); tests adapted
from #46995.

Co-authored-by: Jeff Watts <186512915+lEWFkRAD@users.noreply.github.com>
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-10 01:54:51 -05:00
parent 9cb2a8abb0
commit 3f8b220049
4 changed files with 112 additions and 3 deletions

View file

@ -427,6 +427,69 @@ class TestSearchHandler:
assert "error" in result
# ---------------------------------------------------------------------------
# Windows MSYS path resolution (salvage of #50488 / #46995)
# ---------------------------------------------------------------------------
class TestWindowsMsysPathResolution:
"""File tools must translate Git Bash drive paths before Path resolution."""
def test_absolute_msys_path_normalized_before_windows_resolve(self, monkeypatch):
import tools.environments.local as local_mod
import tools.file_tools as file_tools
monkeypatch.setattr(file_tools.sys, "platform", "win32")
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False)
resolved = file_tools._resolve_path_for_task("/c/Users/Mark/project/app.py")
assert str(resolved) == r"C:\Users\Mark\project\app.py"
def test_cygdrive_path_normalized(self, monkeypatch):
import tools.environments.local as local_mod
import tools.file_tools as file_tools
monkeypatch.setattr(file_tools.sys, "platform", "win32")
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False)
resolved = file_tools._resolve_path_for_task("/cygdrive/d/code/main.py")
assert str(resolved) == r"D:\code\main.py"
def test_relative_path_uses_normalized_msys_cwd(self, monkeypatch):
import tools.environments.local as local_mod
import tools.file_tools as file_tools
monkeypatch.setattr(file_tools.sys, "platform", "win32")
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False)
monkeypatch.setattr(
file_tools,
"_authoritative_workspace_root",
lambda task_id="default": "/c/Users/Mark/project",
)
resolved = file_tools._resolve_path_for_task("src/app.py", task_id="msys")
assert str(resolved) == r"C:\Users\Mark\project\src\app.py"
def test_container_paths_skip_msys_translation(self, monkeypatch):
"""WSL/docker Linux paths must not be rewritten as Windows drives."""
import tools.environments.local as local_mod
import tools.file_tools as file_tools
monkeypatch.setattr(file_tools.sys, "platform", "win32")
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": True)
monkeypatch.setattr(
file_tools,
"_authoritative_workspace_root",
lambda task_id="default": "/home/don/project",
)
resolved = file_tools._resolve_path_for_task("/home/don/.env")
assert str(resolved) == "/home/don/.env"
# ---------------------------------------------------------------------------
# Tool result hint tests (#722)
# ---------------------------------------------------------------------------

View file

@ -68,6 +68,15 @@ class TestMsysToWindowsPath:
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
assert _msys_to_windows_path("/tmp/foo") == "/tmp/foo"
assert _msys_to_windows_path("/home/x") == "/home/x"
# /mnt/<name>/... only translates when <name> is a single drive letter.
assert _msys_to_windows_path("/mnt/home/x") == "/mnt/home/x"
def test_translates_cygdrive_and_wsl_mnt_forms(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
assert _msys_to_windows_path("/cygdrive/c/Users/NVIDIA") == r"C:\Users\NVIDIA"
assert _msys_to_windows_path("/mnt/d/Projects/foo") == r"D:\Projects\foo"
assert _msys_to_windows_path("/cygdrive/c") == "C:\\"
assert _msys_to_windows_path("/mnt/c/") == "C:\\"
def test_empty_string(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)

View file

@ -25,16 +25,24 @@ def _msys_to_windows_path(cwd: str) -> str:
native Windows form (``C:\\Users\\x``) so ``os.path.isdir`` and
``subprocess.Popen(..., cwd=...)`` can find it.
Also accepts the Cygwin (``/cygdrive/c/...``) and WSL-mount
(``/mnt/c/...``) spellings of a drive root. Multi-segment POSIX paths
like ``/home/x`` or ``/tmp/foo`` are left untouched.
No-ops on non-Windows hosts or for paths that aren't in MSYS form.
Returns the input unchanged when no translation applies. This is
idempotent calling it on an already-Windows path returns it as-is.
"""
if not _IS_WINDOWS or not cwd:
return cwd
# Match leading "/<single letter>/" or exactly "/<letter>" (bare drive root).
m = re.match(r'^/([a-zA-Z])(/.*)?$', cwd)
# Match leading "/<single letter>/" or exactly "/<letter>" (bare drive root),
# plus /cygdrive/<letter>/... and /mnt/<letter>/... variants.
m = re.match(r'^/(?:(?:cygdrive|mnt)/)?([a-zA-Z])(/.*)?$', cwd)
if not m:
return cwd
# Reject /cygdrive or /mnt with no drive letter — the optional group above
# already requires the letter. Multi-char first segments (/home, /tmp)
# fail the single-letter capture and fall through as no-ops.
drive = m.group(1).upper()
tail = (m.group(2) or "").replace('/', '\\')
return f"{drive}:{tail or chr(92)}" # chr(92) = backslash, avoid raw-string escape

View file

@ -6,6 +6,7 @@ import json
import logging
import os
import posixpath
import sys
import threading
from pathlib import Path, PurePosixPath
@ -406,6 +407,17 @@ def _resolve_base_dir(
if not posixpath.isabs(base_text):
base_text = posixpath.join(os.getcwd(), base_text)
return _normalize_without_host_deref(base_text)
# Git Bash ``pwd -P`` reports ``/c/Users/...``; translate before Path so
# relative file-tool paths don't anchor under a nonexistent ``\\c\\Users``.
from tools.environments.local import _msys_to_windows_path
base_text = _msys_to_windows_path(base_text)
if sys.platform == "win32":
import ntpath
if not ntpath.isabs(base_text):
base_text = ntpath.join(os.getcwd(), base_text)
return Path(ntpath.normpath(base_text))
base = Path(base_text)
if not base.is_absolute():
# Last-resort anchoring: a live cwd should already be absolute, but if a
@ -420,14 +432,31 @@ def _resolve_path_for_task(filepath: str, task_id: str = "default") -> Path | Pu
See :func:`_resolve_base_dir` for how the base is chosen. Absolute input
paths are returned resolved-but-unanchored.
On native Windows, Git Bash / MSYS drive paths (``/c/Users/...``) are
translated to ``C:\\Users\\...`` before resolution so file tools don't
treat them as relative ``\\c\\Users\\...`` under the process cwd.
"""
container_paths = _uses_container_paths(task_id)
expanded = _expand_tilde(filepath)
if container_paths:
expanded = _expand_tilde(filepath)
if posixpath.isabs(expanded):
return _normalize_without_host_deref(expanded)
resolved = _resolve_base_dir(task_id, container_paths=True) / expanded
return _normalize_without_host_deref(resolved)
# Host paths only — never rewrite Linux paths inside a container/WSL env.
from tools.environments.local import _msys_to_windows_path
expanded = _expand_tilde(_msys_to_windows_path(filepath))
if sys.platform == "win32":
import ntpath
if ntpath.isabs(expanded):
return Path(ntpath.normpath(expanded))
joined = ntpath.join(str(_resolve_base_dir(task_id, container_paths=False)), expanded)
return Path(ntpath.normpath(joined))
p = Path(expanded)
if p.is_absolute():
return p.resolve()