From c68e306ea41f0cfafc929ccbbb5e5ea6b565236c Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:40:05 +0800 Subject: [PATCH] fix(tools): include user local bin in POSIX PATH --- tests/tools/test_local_env_blocklist.py | 22 ++++++++++++++++++++++ tools/environments/local.py | 20 +++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/tests/tools/test_local_env_blocklist.py b/tests/tools/test_local_env_blocklist.py index c0787cb955..48727f4a1e 100644 --- a/tests/tools/test_local_env_blocklist.py +++ b/tests/tools/test_local_env_blocklist.py @@ -1594,6 +1594,28 @@ class TestSanePathIncludesHomebrew: for entry in _SANE_PATH.split(os.pathsep): assert entry in path_entries + def test_make_run_env_appends_existing_user_local_bin_on_posix(self, monkeypatch, tmp_path): + """A thin remote SSH PATH can still resolve user-installed CLIs (#111778).""" + from tools.environments import local as local_mod + from tools.environments.local import _make_run_env + + local_bin = tmp_path / ".local" / "bin" + local_bin.mkdir(parents=True) + executable = local_bin / "himalaya" + executable.touch() + executable.chmod(0o755) + monkeypatch.setattr(local_mod, "_git_bash_bin_dirs", lambda: []) + monkeypatch.setattr(local_mod.Path, "home", classmethod(lambda cls: tmp_path)) + with patch.dict(os.environ, {"PATH": "/usr/bin:/bin"}, clear=True): + result = _make_run_env({}) + + if sys.platform == "win32": + assert str(local_bin) not in result["PATH"].split(os.pathsep) + else: + entries = result["PATH"].split(os.pathsep) + assert entries[:2] == ["/usr/bin", "/bin"] + assert str(local_bin) in entries + @pytest.mark.macos_only def test_make_run_env_real_launchd_path_gains_homebrew(self): diff --git a/tools/environments/local.py b/tools/environments/local.py index fd67f7086e..c30a2fe8af 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -552,16 +552,30 @@ def _managed_runtime_path_entries() -> list[str]: return [] +def _existing_user_local_bin_dir() -> str | None: + """Return the POSIX user's ``~/.local/bin`` only when it exists.""" + if _IS_WINDOWS: + return None + candidate = Path.home() / ".local" / "bin" + return str(candidate) if candidate.is_dir() else None + + def _append_missing_sane_path_entries(existing_path: str) -> str: """Normalised POSIX PATH with missing sane entries appended: empty entries dropped (shells read them as cwd), duplicates collapsed (first wins), then - missing ``_SANE_PATH`` / managed-runtime dirs appended so user entries keep - precedence. Windows is a no-op passthrough (native ``;`` PATH untouched).""" + missing ``_SANE_PATH``, managed-runtime dirs, and an existing user-local bin + dir appended so inherited/login-shell entries keep precedence. Windows is a + no-op passthrough (native ``;`` PATH untouched).""" if _IS_WINDOWS: return existing_path # dict preserves first-occurrence order; empty entries dropped. ordered = dict.fromkeys(entry for entry in existing_path.split(":") if entry) - ordered.update(dict.fromkeys([*_SANE_PATH.split(":"), *_managed_runtime_path_entries()])) + user_local_bin = _existing_user_local_bin_dir() + ordered.update(dict.fromkeys([ + *_SANE_PATH.split(":"), + *_managed_runtime_path_entries(), + *([user_local_bin] if user_local_bin else []), + ])) return ":".join(ordered)