fix: normalize local terminal relative cwd

This commit is contained in:
threadhoard-vps 2026-05-16 06:30:10 +00:00 committed by Teknium
parent 0f239f49c6
commit 5458c76566
2 changed files with 88 additions and 3 deletions

View file

@ -0,0 +1,52 @@
"""Regression tests for local terminal initial cwd normalization."""
from pathlib import Path
from tools.environments.local import LocalEnvironment, _resolve_local_initial_cwd
def test_relative_initial_cwd_resolves_from_parent(tmp_path, monkeypatch):
project = tmp_path / "hermes-agent"
project.mkdir()
monkeypatch.chdir(tmp_path)
assert _resolve_local_initial_cwd("hermes-agent") == str(project)
def test_relative_initial_cwd_matching_current_dir_uses_current_dir(tmp_path, monkeypatch):
project = tmp_path / "hermes-agent"
project.mkdir()
monkeypatch.chdir(project)
assert _resolve_local_initial_cwd("hermes-agent") == str(project)
def test_local_environment_does_not_cd_into_nested_matching_relative_cwd(tmp_path, monkeypatch):
project = tmp_path / "hermes-agent"
project.mkdir()
monkeypatch.chdir(project)
env = LocalEnvironment(cwd="hermes-agent", timeout=5)
try:
result = env.execute("pwd", timeout=5)
finally:
env.cleanup()
assert result["returncode"] == 0
assert result["output"].strip() == str(project)
assert "cd: hermes-agent" not in result["output"]
def test_local_environment_keeps_existing_relative_child_cwd(tmp_path, monkeypatch):
project = tmp_path / "hermes-agent"
project.mkdir()
monkeypatch.chdir(tmp_path)
env = LocalEnvironment(cwd="hermes-agent", timeout=5)
try:
result = env.execute("pwd", timeout=5)
finally:
env.cleanup()
assert result["returncode"] == 0
assert result["output"].strip() == str(project)

View file

@ -48,6 +48,40 @@ def _msys_to_windows_path(cwd: str) -> str:
return f"{drive}:{tail or chr(92)}" # chr(92) = backslash, avoid raw-string escape
def _resolve_local_initial_cwd(cwd: str) -> str:
"""Resolve the local backend's initial cwd to an absolute host path.
``TERMINAL_CWD`` can be populated from config.yaml before the terminal
backend is created. If that value is relative and happens to match the
directory Hermes was already launched from (for example ``hermes-agent``
while the process cwd is ``~/.hermes/hermes-agent``), passing it through
unchanged makes the wrapper run ``cd hermes-agent`` *inside* the project
and fail with a confusing nested-path error. Anchor relative local cwd
values once, up front, so both ``subprocess.Popen(cwd=...)`` and the
in-shell ``cd`` use the same absolute directory.
"""
expanded = os.path.expanduser(cwd) if cwd else os.getcwd()
if _IS_WINDOWS:
expanded = _msys_to_windows_path(expanded)
if os.path.isabs(expanded):
return expanded
candidate = os.path.abspath(expanded)
current = os.getcwd()
# Common recovery for config values like ``hermes-agent`` when Hermes was
# launched from that directory already. ``os.path.abspath`` would point at
# a nonexistent nested ``./hermes-agent``; use the current directory instead.
if not os.path.isdir(candidate):
wanted_parts = Path(expanded).parts
current_parts = Path(current).parts
if wanted_parts and len(wanted_parts) <= len(current_parts):
if current_parts[-len(wanted_parts):] == wanted_parts:
return current
return candidate
def _windows_to_msys_path(cwd: str) -> str:
"""Translate a native Windows path (``C:\\Users\\x``) to Git Bash /
MSYS form (``/c/Users/x``) so ``builtin cd`` resolves it reliably.
@ -1102,9 +1136,8 @@ class LocalEnvironment(BaseEnvironment):
"""
def __init__(self, cwd: str = "", timeout: int = 60, env: dict = None):
if cwd:
cwd = os.path.expanduser(cwd)
super().__init__(cwd=cwd or os.getcwd(), timeout=timeout, env=env)
cwd = _resolve_local_initial_cwd(cwd)
super().__init__(cwd=cwd, timeout=timeout, env=env)
self.init_session()
def get_temp_dir(self) -> str: