mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
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:
parent
9cb2a8abb0
commit
3f8b220049
4 changed files with 112 additions and 3 deletions
|
|
@ -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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue