fix(windows): bash-safe snapshot paths after #63113

#63113 rewrote native drive paths in ShellFileOperations, but init_session
/_wrap_command still embedded C:/... hermes-snap paths from get_temp_dir.
MSYS arg-converts those during bash -l and surfaces Directory \drivers\etc
— including for relative write_file targets, since the wrapper is the fault.

Add _bash_safe_path, override BaseEnvironment._quote_shell_path on
LocalEnvironment (no base→local import), and normalize mixed /c/Users\...
paths in file ops.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-13 01:59:58 -04:00
parent acb3bde097
commit f2fcf89c1f
5 changed files with 181 additions and 28 deletions

View file

@ -477,6 +477,23 @@ class TestShellFileOpsHelpers:
# Non-drive paths are untouched.
assert file_ops._escape_shell_arg("/tmp/foo") == "'/tmp/foo'"
def test_escape_shell_arg_normalizes_mixed_msys_paths(self, monkeypatch, file_ops):
import tools.environments.local as local_mod
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
mixed = r"/c/Users/Alexander\Documents\NewTEST\readme.txt"
assert file_ops._escape_shell_arg(mixed) == (
"'/c/Users/Alexander/Documents/NewTEST/readme.txt'"
)
def test_escape_shell_arg_rewrites_forward_slash_native_paths(self, monkeypatch, file_ops):
import tools.environments.local as local_mod
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
assert file_ops._escape_shell_arg(
"C:/Users/alice/notes.txt"
) == "'/c/Users/alice/notes.txt'"
def test_read_file_uses_bash_safe_windows_paths(self, mock_env, monkeypatch):
import tools.environments.local as local_mod

View file

@ -20,12 +20,14 @@ on the real OS.
from unittest.mock import patch
from tools.environments.base import BaseEnvironment
from tools.environments import local as local_mod
from tools.environments.local import (
LocalEnvironment,
_bash_safe_path,
_make_run_env,
_msys_to_windows_path,
_quote_bash_path,
_resolve_safe_cwd,
_sanitize_subprocess_env,
_windows_to_msys_path,
@ -112,6 +114,42 @@ class TestWindowsToMsysPath:
assert _windows_to_msys_path(r"\\server\share") == r"\\server\share"
# ---------------------------------------------------------------------------
# _bash_safe_path / _quote_bash_path — shell-script interpolation
# ---------------------------------------------------------------------------
class TestBashSafePath:
def test_native_windows_path_becomes_msys(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
assert _bash_safe_path(r"C:\Users\alice\notes.txt") == "/c/Users/alice/notes.txt"
def test_forward_slash_native_path_becomes_msys(self, monkeypatch):
"""Production get_temp_dir emits C:/... — still needs /c/... rewrite."""
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
assert (
_bash_safe_path("C:/Users/Alexander/.hermes/cache/terminal/hermes-snap-x.sh")
== "/c/Users/Alexander/.hermes/cache/terminal/hermes-snap-x.sh"
)
def test_mixed_msys_path_normalizes_backslashes(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
mixed = r"/c/Users/Alexander\Documents\NewTEST\readme.txt"
assert _bash_safe_path(mixed) == "/c/Users/Alexander/Documents/NewTEST/readme.txt"
def test_noop_off_windows(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", False)
path = r"/c/Users\Alexander\Documents"
assert _bash_safe_path(path) == path
def test_quote_bash_path_quotes_mixed_windows_path(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
quoted = _quote_bash_path(
r"C:\Users\Alexander\AppData\Local\Temp\hermes-snap-abc.sh"
)
assert "/c/Users/Alexander/AppData/Local/Temp/hermes-snap-abc.sh" in quoted
assert "\\" not in quoted
# ---------------------------------------------------------------------------
# _resolve_safe_cwd — Windows fast path
# ---------------------------------------------------------------------------
@ -326,3 +364,61 @@ class TestWrapCommandWindowsNativeCwd:
assert "builtin cd -- /c/Users/liush 2>/dev/null || true" in captured["script"]
assert r"C:\Users\liush" not in captured["script"]
def test_init_session_bootstrap_quotes_snapshot_paths_in_msys_form(self, monkeypatch):
"""Snapshot paths must reach bash as /c/... — C:/... still trips MSYS
arg conversion during bash -l and surfaces as \\drivers\\etc."""
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
captured = {}
def fake_run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):
captured["script"] = cmd_string
raise RuntimeError("stop after capturing bootstrap")
monkeypatch.setattr(LocalEnvironment, "_run_bash", fake_run_bash)
# Production shape: get_temp_dir forces forward slashes but keeps C:.
snap = "C:/Users/Alexander/.hermes/cache/terminal/hermes-snap-deadbeef.sh"
with patch.object(LocalEnvironment, "__init__", lambda self, **kw: None):
env = LocalEnvironment.__new__(LocalEnvironment)
BaseEnvironment.__init__(
env,
cwd=r"C:\Users\Alexander\Documents",
timeout=10,
)
env._snapshot_path = snap
env._cwd_file = snap + ".cwd"
env.init_session()
script = captured["script"]
assert "/c/Users/Alexander/.hermes/cache/terminal/hermes-snap-deadbeef.sh" in script
assert "C:/Users/Alexander" not in script
assert r"C:\Users\Alexander" not in script
def test_init_session_bootstrap_rewrites_backslash_snapshot_paths(self, monkeypatch):
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
captured = {}
def fake_run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):
captured["script"] = cmd_string
raise RuntimeError("stop after capturing bootstrap")
monkeypatch.setattr(LocalEnvironment, "_run_bash", fake_run_bash)
snap = r"C:\Users\Alexander\AppData\Local\Temp\hermes-snap-deadbeef.sh"
with patch.object(LocalEnvironment, "__init__", lambda self, **kw: None):
env = LocalEnvironment.__new__(LocalEnvironment)
BaseEnvironment.__init__(
env,
cwd=r"C:\Users\Alexander\Documents",
timeout=10,
)
env._snapshot_path = snap
env._cwd_file = snap + ".cwd"
env.init_session()
script = captured["script"]
assert "/c/Users/Alexander/AppData/Local/Temp/hermes-snap-deadbeef.sh" in script
assert r"C:\Users\Alexander\AppData" not in script