"""Browser consumers use real PM facts, not ambient npm caches.""" import json import os import sys from pathlib import Path import pytest import pm from pm import paths from tools import browser_tool as bt from tools import browser_tool_install as install from tools import browser_tool_session as session @pytest.fixture def browser_store(tmp_path, monkeypatch): home = tmp_path / "home with spaces" home.mkdir() store = home / "tools" monkeypatch.setenv("HOME", str(home)) monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store)) monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1") monkeypatch.setenv("PATH", "") monkeypatch.setattr(bt, "_SANE_PATH_DIRS", ()) monkeypatch.setattr(install, "_discover_homebrew_node_dirs", lambda: ()) monkeypatch.delenv("AGENT_BROWSER_EXECUTABLE_PATH", raising=False) monkeypatch.delenv("PLAYWRIGHT_BROWSERS_PATH", raising=False) monkeypatch.delenv("TERMUX_VERSION", raising=False) monkeypatch.setattr(bt, "_chromium_autoinstall_attempted", False) lock = pm.Lockfile(paths.lockfile_path()) target = pm.current_target() def publish(name, content="browser-fixture"): package = pm.get_package(name) version = lock.version(name) assert version is not None entry = store / package.store_entry(version, target) if name == "chromium": binary = entry / "browser" / ("chrome.exe" if os.name == "nt" else "chrome") else: binary = package.binary(entry, target) assert binary is not None binary.parent.mkdir(parents=True, exist_ok=True) binary.write_text(content) binary.chmod(0o755) pm.Facts(store / "facts.json").record( name, version, entry.name, package.env(entry, target), store, target=target, artifacts=[a["sha256"] for a in lock.artifacts(name, target)], ) installed = pm.installed_package(name) assert installed is not None and installed.binary == binary return binary return home, store, publish def test_unrecorded_playwright_cache_is_not_a_pm_browser(browser_store): home, _, _ = browser_store (home / ".cache" / "ms-playwright" / "chromium-1234").mkdir(parents=True) assert install._chromium_installed() is False def test_doctor_fix_publishes_and_reads_the_pm_browser(browser_store, monkeypatch): from hermes_cli import doctor_tools import pm.client _, _, publish = browser_store monkeypatch.setenv("PATH", "") requests = [] def request(operation, payload, **kwargs): # Only the worker/download boundary is replaced; selection, admission, # facts and environment composition are the actual PM implementation. requests.append((operation, payload)) publish("chromium") publish("agent-browser") monkeypatch.setattr(pm.client, "_request", request) assert doctor_tools._check_agent_browser(False) is False assert requests == [] assert doctor_tools._check_agent_browser(True) is True assert requests == [("ensure", {"name": "agent-browser", "explicit": True})] def test_missing_browser_refuses_lazy_install_even_with_npx(browser_store, monkeypatch, tmp_path): _, store, _ = browser_store bin_dir = tmp_path / "bin" bin_dir.mkdir() npx = bin_dir / ("npx.exe" if os.name == "nt" else "npx") npx.write_bytes(b"not an agent-browser installation") npx.chmod(0o755) monkeypatch.setenv("PATH", str(bin_dir)) with pytest.raises(FileNotFoundError, match="agent-browser CLI not found"): install._find_agent_browser(validate=False) with pytest.raises(FileNotFoundError, match="[Ll]azy|[Aa]utomatic|disabled"): install._find_agent_browser() assert not store.exists() def test_late_pm_install_is_visible_without_cache_reset(browser_store, monkeypatch): _, _, publish = browser_store monkeypatch.setenv("PATH", "") with pytest.raises(FileNotFoundError): install._find_agent_browser() binary = publish("agent-browser") assert install._find_agent_browser() == str(binary) def test_execution_acquires_missing_browser_through_pm(browser_store, monkeypatch): import pm.client _, _, publish = browser_store monkeypatch.setenv("PATH", "") monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "0") assert pm.lazy_installs_allowed() requests = [] def request(operation, payload, **kwargs): requests.append((operation, payload)) publish("chromium") publish("agent-browser") monkeypatch.setattr(pm.client, "_request", request) result = install._find_agent_browser() installed = pm.installed_package("agent-browser") assert installed is not None and result == str(installed.binary) assert requests == [("ensure", {"name": "agent-browser", "explicit": False})] assert install._chromium_installed() def test_termux_ownership_policy_never_provisions(browser_store, monkeypatch): # Exercise the real environment policy, not an emulated Android binary. from hermes_cli import doctor_tools monkeypatch.setenv("TERMUX_VERSION", "test") monkeypatch.setenv("PATH", "") monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "0") monkeypatch.setattr(install, "_running_in_docker", lambda: False) monkeypatch.setattr(bt, "_chromium_autoinstall_attempted", False) def forbidden(*args, **kwargs): pytest.fail("Termux browser installation is externally owned") monkeypatch.setattr(pm, "ensure", forbidden) with pytest.raises(FileNotFoundError, match="npm install -g agent-browser"): install._find_agent_browser() assert install._maybe_autoinstall_chromium() is False assert doctor_tools._check_agent_browser(True) is False def test_cdp_override_does_not_require_pm_browser(browser_store, monkeypatch): monkeypatch.setenv("BROWSER_CDP_URL", "ws://127.0.0.1:9222/devtools/browser/test") monkeypatch.setattr(bt, "_is_browser_use_cli_mode", lambda: False) monkeypatch.setattr(bt, "_is_camofox_mode", lambda: False) assert install.check_browser_requirements() is True @pytest.mark.platforms("posix") def test_external_browser_on_path_remains_supported_without_pm(browser_store, monkeypatch, tmp_path): _, store, _ = browser_store bin_dir = tmp_path / "external browser" bin_dir.mkdir() binary = bin_dir / "agent-browser" binary.write_text(f"#!{sys.executable}\nprint('external-agent-browser')\n") binary.chmod(0o755) monkeypatch.setenv("PATH", str(bin_dir)) assert install._find_agent_browser(validate=False) == str(binary) assert install._find_agent_browser() == str(binary) assert not store.exists() def test_historical_warmer_is_inert(monkeypatch): def forbidden(*args, **kwargs): pytest.fail("historical browser warmer must do no work") monkeypatch.setattr("subprocess.Popen", forbidden) monkeypatch.setattr(pm, "ensure", forbidden) from tools import browser_tool assert browser_tool.warm_agent_browser_npx_cache(timeout=0.1) is False def test_external_chromium_override_survives_pm_composition(browser_store, monkeypatch, tmp_path): _, _, publish = browser_store publish("chromium") override = tmp_path / "user browser" override.write_bytes(b"external") monkeypatch.setenv("AGENT_BROWSER_EXECUTABLE_PATH", str(override)) assert install._chromium_installed() assert session._agent_browser_command_env(str(tmp_path))["AGENT_BROWSER_EXECUTABLE_PATH"] == str(override) def test_restricted_path_discovers_external_browser_without_execution(browser_store, monkeypatch, tmp_path): _, store, _ = browser_store external_bin = tmp_path / "external-homebrew" / "bin" external_bin.mkdir(parents=True) binary = external_bin / ("agent-browser.exe" if os.name == "nt" else "agent-browser") binary.write_bytes(b"external browser; presence check must not execute") binary.chmod(0o755) monkeypatch.setenv("PATH", str(tmp_path / "empty-path")) monkeypatch.setattr(bt, "_SANE_PATH_DIRS", (str(external_bin),)) def forbidden(*args, **kwargs): pytest.fail("passive external browser discovery must not execute or install") monkeypatch.setattr("subprocess.Popen", forbidden) monkeypatch.setattr(pm, "ensure", forbidden) assert install._find_agent_browser(validate=False) == str(binary) assert not store.exists() def test_pm_browser_wins_over_legacy_and_ambient_installs(browser_store, monkeypatch): home, store, publish = browser_store binary = publish("agent-browser") publish("chromium") legacy = home / "node_modules" / ".bin" legacy.mkdir(parents=True) external = legacy / ("agent-browser.exe" if os.name == "nt" else "agent-browser") external.write_bytes(b"legacy browser") external.chmod(0o755) monkeypatch.setenv("PATH", str(legacy)) before = (store / "facts.json").read_bytes() assert install._find_agent_browser(validate=False) == str(binary) assert (store / "facts.json").read_bytes() == before @pytest.mark.platforms("posix") def test_exact_pm_child_receives_composed_scrubbed_environment(browser_store, monkeypatch, tmp_path): _, store, publish = browser_store binary = publish("agent-browser", f"#!{sys.executable}\n" + '''import json, os, sys print(json.dumps({"argv": sys.argv, "env": dict(os.environ)})) ''') chromium = publish("chromium") monkeypatch.setenv("PATH", str(tmp_path / "hostile-path")) system_bin = tmp_path / "external-system-bin" system_bin.mkdir() monkeypatch.setattr(bt, "_SANE_PATH_DIRS", (str(system_bin),)) monkeypatch.setenv("OPENAI_API_KEY", "must-not-reach-browser") monkeypatch.setenv("BROWSERBASE_API_KEY", "browser-provider-key") before = dict(os.environ) socket_dir = tmp_path / "session" socket_dir.mkdir() command = install._find_agent_browser(validate=False) env = session._agent_browser_command_env(str(socket_dir)) proc = session._popen_agent_browser([command, "--json", "get", "url"], env, str(socket_dir), "probe") assert proc.wait(timeout=10) == 0 output, error = session._read_command_output_files(str(socket_dir / "_stdout_probe"), str(socket_dir / "_stderr_probe")) assert error == "" child = json.loads(output) assert Path(child["argv"][0]) == binary assert child["argv"][1:] == ["--json", "get", "url"] assert child["env"]["AGENT_BROWSER_EXECUTABLE_PATH"] == str(chromium) assert child["env"]["PLAYWRIGHT_BROWSERS_PATH"] == str(store) assert child["env"]["PATH"].split(os.pathsep)[0] == str(binary.parent) assert str(system_bin) in child["env"]["PATH"].split(os.pathsep)[1:] assert "OPENAI_API_KEY" not in child["env"] assert child["env"]["BROWSERBASE_API_KEY"] == "browser-provider-key" assert child["env"]["AGENT_BROWSER_SOCKET_DIR"] == str(socket_dir) assert dict(os.environ) == before @pytest.mark.platforms("posix") def test_runtime_and_chrome_fallback_launch_the_same_pm_binary(browser_store, monkeypatch, tmp_path): from tools import browser_tool_cloud as cloud from tools import browser_tool_lightpanda_fallback as fallback _, _, publish = browser_store binary = publish("agent-browser", f"#!{sys.executable}\n" + '''import json, os, sys print(json.dumps({"success": True, "data": { "url": "https://example.com/", "argv": sys.argv, "browser": os.environ.get("AGENT_BROWSER_EXECUTABLE_PATH") }})) ''') chromium = publish("chromium") monkeypatch.setenv("PATH", "") monkeypatch.setattr(bt, "_socket_safe_tmpdir", lambda: str(tmp_path)) monkeypatch.setattr(cloud, "_is_local_mode", lambda: True) monkeypatch.setattr(cloud, "_get_browser_engine", lambda: "auto") monkeypatch.setattr(cloud, "_is_headed_mode", lambda: False) monkeypatch.setattr(session, "_get_session_info", lambda task_id: {"session_name": "fixture", "cdp_url": None}) monkeypatch.setattr("tools.interrupt.is_interrupted", lambda: False) direct = session._run_browser_command("fixture", "get", ["url"]) chrome = fallback._run_chrome_fallback_command("fixture", "get", ["url"], timeout=10) for result in (direct, chrome): assert result["success"] is True assert result["data"]["argv"][0] == str(binary) assert result["data"]["browser"] == str(chromium) assert direct["data"]["argv"][1:3] == ["--session", "fixture"] assert chrome["data"]["argv"][1:3] == ["--engine", "chrome"] @pytest.mark.parametrize("fails", [False, True]) def test_chromium_autoinstall_is_one_shot(browser_store, monkeypatch, fails): import pm.client _, _, publish = browser_store monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "0") monkeypatch.setattr(install, "_running_in_docker", lambda: False) calls = [] def request(operation, payload, **kwargs): calls.append((operation, payload)) if fails: raise pm.InstallError("chromium", "download failed") publish("chromium") monkeypatch.setattr(pm.client, "_request", request) assert install._maybe_autoinstall_chromium() is (not fails) assert install._maybe_autoinstall_chromium() is (not fails) assert calls == [("ensure", {"name": "chromium", "explicit": False})] @pytest.mark.parametrize("policy", ["docker", "disabled", "termux", "override"]) def test_chromium_acquisition_policy(browser_store, monkeypatch, policy, tmp_path): import pm.client monkeypatch.setattr(install, "_running_in_docker", lambda: policy == "docker") monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1" if policy == "disabled" else "0") if policy == "termux": monkeypatch.setenv("TERMUX_VERSION", "test") if policy == "override": override = tmp_path / "external-chrome" override.touch() monkeypatch.setenv("AGENT_BROWSER_EXECUTABLE_PATH", str(override)) assert install._chromium_installed() override.unlink() assert not install._chromium_installed() monkeypatch.setattr(pm.client, "_request", lambda *a, **kw: pytest.fail("forbidden acquisition")) assert not install._maybe_autoinstall_chromium() @pytest.mark.parametrize("backend", ["local", "camofox", "cdp"]) def test_browser_readiness_ignores_ambient_chromium(browser_store, monkeypatch, backend): from tools import browser_tool_cloud, browser_tool_lightpanda_fallback home, _, publish = browser_store publish("agent-browser") (home / "chromium_headless_shell-1234").mkdir() monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(home)) ambient = home / ("chromium.exe" if os.name == "nt" else "chromium") ambient.touch() ambient.chmod(0o755) monkeypatch.setenv("PATH", str(home)) monkeypatch.setattr(bt, "_is_browser_use_cli_mode", lambda: False) monkeypatch.setattr(bt, "_is_camofox_mode", lambda: backend == "camofox") monkeypatch.setattr(browser_tool_cloud, "_get_cloud_provider", lambda: None) monkeypatch.setattr(browser_tool_lightpanda_fallback, "_using_lightpanda_engine", lambda: False) monkeypatch.setenv("BROWSER_CDP_URL", "ws://example.test/cdp" if backend == "cdp" else "") assert install.check_browser_requirements() is (backend != "local") publish("chromium") assert install.check_browser_requirements()