fix(browser): discover browser-use from user-level tool directories
Desktop/TUI workers can spawn with a minimal PATH that omits ~/.local/bin, the default location where uv tool install links the browser-use binary. _find_cli() then failed to resolve an installed CLI and Browser Use mode silently fell back to the built-in tools. Probe the user-level tool dir (~/.local/bin on POSIX, APPDATA/uv/bin on Windows) between PATH and the managed HERMES_HOME/bin, for both the browser-use binary and the uvx fallback. Salvaged from PR #83788 by @kimyxx onto current main; tests adapted and extended with precedence and uvx coverage.
This commit is contained in:
@@ -706,7 +706,15 @@ class TestBrowserExec:
|
||||
|
||||
|
||||
class TestFindCliManagedBin:
|
||||
"""_find_cli probes $HERMES_HOME/bin after PATH (managed uv/uvx/browser-use)."""
|
||||
"""_find_cli probes ~/.local/bin and $HERMES_HOME/bin after PATH."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _hermetic_home(self, tmp_path, monkeypatch):
|
||||
"""Pin HOME so the ~/.local/bin probe can't leak the host's real
|
||||
user-level installs into these real-PATH-probing tests."""
|
||||
monkeypatch.setenv("HOME", str(tmp_path / "userhome"))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
|
||||
|
||||
def test_managed_bin_browser_use_found(self, tmp_path, monkeypatch):
|
||||
bin_dir = tmp_path / "home" / "bin"
|
||||
@@ -714,8 +722,6 @@ class TestFindCliManagedBin:
|
||||
bu = bin_dir / "browser-use"
|
||||
bu.write_text("#!/bin/sh\n")
|
||||
bu.chmod(bu.stat().st_mode | stat.S_IXUSR)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
|
||||
assert bu_cli._find_cli_unpatched() == [str(bu)]
|
||||
|
||||
def test_managed_bin_uvx_fallback(self, tmp_path, monkeypatch):
|
||||
@@ -724,15 +730,45 @@ class TestFindCliManagedBin:
|
||||
uvx = bin_dir / "uvx"
|
||||
uvx.write_text("#!/bin/sh\n")
|
||||
uvx.chmod(uvx.stat().st_mode | stat.S_IXUSR)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
|
||||
assert bu_cli._find_cli_unpatched() == [str(uvx), "browser-use"]
|
||||
|
||||
def test_nothing_found(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
||||
monkeypatch.setenv("PATH", str(tmp_path / "empty"))
|
||||
assert bu_cli._find_cli_unpatched() is None
|
||||
|
||||
def test_user_local_bin_browser_use_found(self, tmp_path, monkeypatch):
|
||||
"""#83788: Desktop/TUI workers spawn with a minimal PATH that omits
|
||||
~/.local/bin, where `uv tool install browser-use` links the binary
|
||||
by default — _find_cli must probe it explicitly."""
|
||||
cli_dir = tmp_path / "userhome" / ".local" / "bin"
|
||||
cli_dir.mkdir(parents=True)
|
||||
cli = cli_dir / "browser-use"
|
||||
cli.write_text("#!/bin/sh\n")
|
||||
cli.chmod(cli.stat().st_mode | stat.S_IXUSR)
|
||||
assert bu_cli._find_cli_unpatched() == [str(cli)]
|
||||
|
||||
def test_user_local_bin_precedes_managed_bin(self, tmp_path, monkeypatch):
|
||||
"""A user-level install wins over Hermes' managed copy — mirrors the
|
||||
PATH-first preference (user intent beats bootstrap)."""
|
||||
user_dir = tmp_path / "userhome" / ".local" / "bin"
|
||||
user_dir.mkdir(parents=True)
|
||||
user_cli = user_dir / "browser-use"
|
||||
user_cli.write_text("#!/bin/sh\n")
|
||||
user_cli.chmod(user_cli.stat().st_mode | stat.S_IXUSR)
|
||||
managed_dir = tmp_path / "home" / "bin"
|
||||
managed_dir.mkdir(parents=True)
|
||||
managed_cli = managed_dir / "browser-use"
|
||||
managed_cli.write_text("#!/bin/sh\n")
|
||||
managed_cli.chmod(managed_cli.stat().st_mode | stat.S_IXUSR)
|
||||
assert bu_cli._find_cli_unpatched() == [str(user_cli)]
|
||||
|
||||
def test_user_local_bin_uvx_fallback(self, tmp_path, monkeypatch):
|
||||
cli_dir = tmp_path / "userhome" / ".local" / "bin"
|
||||
cli_dir.mkdir(parents=True)
|
||||
uvx = cli_dir / "uvx"
|
||||
uvx.write_text("#!/bin/sh\n")
|
||||
uvx.chmod(uvx.stat().st_mode | stat.S_IXUSR)
|
||||
assert bu_cli._find_cli_unpatched() == [str(uvx), "browser-use"]
|
||||
|
||||
|
||||
class TestInstallCli:
|
||||
def test_already_installed_on_path(self, tmp_path, monkeypatch):
|
||||
|
||||
@@ -203,21 +203,40 @@ def _managed_bin_dir() -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _user_local_bin_dir() -> Optional[str]:
|
||||
"""The standard user-level tool dir (~/.local/bin on POSIX; uv's default
|
||||
tool bin dir on Windows). Desktop/TUI workers may start with a minimal
|
||||
PATH that omits it even when `uv tool install browser-use` put the
|
||||
binary there."""
|
||||
try:
|
||||
if os.name == "nt":
|
||||
base = os.environ.get("APPDATA")
|
||||
if base:
|
||||
return str(Path(base) / "uv" / "bin")
|
||||
return None
|
||||
return str(Path(os.path.expanduser("~")) / ".local" / "bin")
|
||||
except Exception as e: # pragma: no cover — defensive
|
||||
logger.debug("Could not resolve user-local bin dir: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _find_cli() -> Optional[List[str]]:
|
||||
"""Locate the browser-use CLI, or None when it can't be run.
|
||||
|
||||
Prefers an installed browser-use binary (PATH, then Hermes' managed
|
||||
$HERMES_HOME/bin); falls back to running it through uvx (PATH, then
|
||||
managed). The managed probes matter because Hermes bootstraps its own
|
||||
uv into $HERMES_HOME/bin, which is not on the user's PATH.
|
||||
Prefers an installed browser-use binary (PATH, then the user-level tool
|
||||
dir, then Hermes' managed $HERMES_HOME/bin); falls back to running it
|
||||
through uvx across the same probe set. The extra probes matter because
|
||||
Hermes bootstraps its own uv into $HERMES_HOME/bin (not on the user's
|
||||
PATH), and Desktop/TUI workers can spawn with a minimal PATH that omits
|
||||
~/.local/bin where `uv tool install` links binaries by default.
|
||||
"""
|
||||
bin_dir = _managed_bin_dir()
|
||||
for probe_path in (None, bin_dir):
|
||||
probe_paths = (None, _user_local_bin_dir(), _managed_bin_dir())
|
||||
for probe_path in probe_paths:
|
||||
if probe_path is None or probe_path:
|
||||
direct = shutil.which("browser-use", path=probe_path)
|
||||
if direct:
|
||||
return [direct]
|
||||
for probe_path in (None, bin_dir):
|
||||
for probe_path in probe_paths:
|
||||
if probe_path is None or probe_path:
|
||||
uvx = shutil.which("uvx", path=probe_path)
|
||||
if uvx:
|
||||
|
||||
Reference in New Issue
Block a user