From 1cec6afdfcf830bbbc69f5484e893b08f869f918 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jun 2026 02:02:51 +0800 Subject: [PATCH] fix(doctor): detect agent-browser in the Hermes-managed node bin so setup-browser installs aren't reported as missing (#53192) `hermes acp --setup-browser` installs agent-browser into the Hermes-managed node prefix (~/.hermes/node/bin/agent-browser), which isn't necessarily on PATH. doctor only checked PROJECT_ROOT/node_modules and PATH (shutil.which), so it false-negatived with "agent-browser not installed" even though the binary was present and runnable. Mirror dep_ensure._has_hermes_agent_browser() by also checking HERMES_HOME/node/bin and the legacy HERMES_HOME/node_modules/.bin path, each gated by agent_browser_runnable(). Tested with tests/hermes_cli/test_doctor.py (added positive + not-runnable cases) and pytest tests/hermes_cli/test_doctor.py -q (66 passed). --- hermes_cli/doctor.py | 12 ++++++ tests/hermes_cli/test_doctor.py | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 8a361c5cf1da..6dfaa1071764 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -1813,12 +1813,24 @@ def run_doctor(args): agent_browser_path = PROJECT_ROOT / "node_modules" / "agent-browser" agent_browser_ok = False _which_ab = shutil.which("agent-browser") + # `hermes acp --setup-browser` installs agent-browser into the + # Hermes-managed node prefix, which isn't necessarily on PATH. Mirror + # dep_ensure._has_hermes_agent_browser() so doctor and dep_ensure agree + # on what "installed" means; otherwise doctor false-negatives (#53192). + _managed_ab = HERMES_HOME / "node" / "bin" / "agent-browser" + _legacy_ab = HERMES_HOME / "node_modules" / ".bin" / "agent-browser" if agent_browser_path.exists(): check_ok("agent-browser (Node.js)", "(browser automation)") agent_browser_ok = True elif _which_ab and agent_browser_runnable(_which_ab): check_ok("agent-browser", "(browser automation)") agent_browser_ok = True + elif _managed_ab.is_file() and agent_browser_runnable(str(_managed_ab)): + check_ok("agent-browser", "(browser automation)") + agent_browser_ok = True + elif _legacy_ab.is_file() and agent_browser_runnable(str(_legacy_ab)): + check_ok("agent-browser", "(browser automation)") + agent_browser_ok = True elif _which_ab: # Found on PATH but won't run — almost always a dangling global # symlink left behind by agent-browser's npm postinstall after a diff --git a/tests/hermes_cli/test_doctor.py b/tests/hermes_cli/test_doctor.py index 8ded2affd119..b2fe505f3bb6 100644 --- a/tests/hermes_cli/test_doctor.py +++ b/tests/hermes_cli/test_doctor.py @@ -742,6 +742,73 @@ def test_run_doctor_termux_does_not_mark_browser_available_without_agent_browser assert "npm install -g agent-browser && agent-browser install" in out +def _run_doctor_with_managed_agent_browser(monkeypatch, tmp_path, runnable): + """Set up run_doctor with node present, agent-browser only in the + Hermes-managed node bin (~/.hermes/node/bin), not on PATH or in + PROJECT_ROOT/node_modules. Returns the captured stdout.""" + home = tmp_path / ".hermes" + (home / "node" / "bin").mkdir(parents=True, exist_ok=True) + (home / "config.yaml").write_text("memory: {}\n", encoding="utf-8") + managed_ab = home / "node" / "bin" / "agent-browser" + managed_ab.write_text("#!/bin/sh\n", encoding="utf-8") + project = tmp_path / "project" + project.mkdir(exist_ok=True) # no node_modules/agent-browser here + + monkeypatch.delenv("TERMUX_VERSION", raising=False) + monkeypatch.delenv("PREFIX", raising=False) + monkeypatch.setattr(doctor_mod, "HERMES_HOME", home) + monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project) + monkeypatch.setattr(doctor_mod, "_DHH", str(home)) + # node on PATH, agent-browser is NOT on PATH (only in the managed bin) + monkeypatch.setattr( + doctor_mod.shutil, + "which", + lambda cmd: "/usr/bin/node" if cmd in {"node", "npm"} else None, + ) + # agent_browser_runnable is imported into doctor's namespace + monkeypatch.setattr( + doctor_mod, + "agent_browser_runnable", + lambda path: runnable and str(path) == str(managed_ab), + ) + + fake_model_tools = types.SimpleNamespace( + check_tool_availability=lambda *a, **kw: ([], []), + TOOLSET_REQUIREMENTS={}, + ) + monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools) + + try: + from hermes_cli import auth as _auth_mod + monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {}) + monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {}) + monkeypatch.setattr(_auth_mod, "get_xai_oauth_auth_status", lambda: {}) + except Exception: + pass + + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + doctor_mod.run_doctor(Namespace(fix=False)) + return buf.getvalue() + + +def test_run_doctor_detects_agent_browser_in_managed_node_bin(monkeypatch, tmp_path): + # Regression for #53192: `hermes acp --setup-browser` installs into + # ~/.hermes/node/bin/agent-browser, which isn't on PATH; doctor must still + # report it installed instead of "agent-browser not installed". + out = _run_doctor_with_managed_agent_browser(monkeypatch, tmp_path, runnable=True) + assert "agent-browser not installed" not in out + assert "agent-browser found but not runnable" not in out + assert "✓ agent-browser" in out + + +def test_run_doctor_managed_agent_browser_not_runnable_still_warns(monkeypatch, tmp_path): + # A present-but-unrunnable managed binary must fall through to the existing + # warning, not be reported as OK. + out = _run_doctor_with_managed_agent_browser(monkeypatch, tmp_path, runnable=False) + assert "agent-browser not installed" in out + + def test_run_doctor_kimi_cn_env_is_detected_and_probe_is_null_safe(monkeypatch, tmp_path): home = tmp_path / ".hermes" home.mkdir(parents=True, exist_ok=True)