Files
hermes-agent/tests/tools/test_browser_pm.py
ethernet 1d602a04e3 fix: round-6 CI backlog (32 python, docker arm64, win32-arm64, windows lane, desktop lint)
- pm.environment: the `--no-install-package <name>` root read parses [project].name without
  tomllib on the pre-3.11 bootstrap python (Docker arm64 stage_runtime). Both parsers proven
  to agree on the real pyproject.
- windows-build-deps.ps1 / run_tests.sh: with DISTUTILS_USE_SDK, setuptools takes link.exe
  from PATH; under a bash-hosted step Git for Windows' coreutils `link` shadowed MSVC's.
  The MSVC linker directory now leads PATH (cl.exe was already found — this was the next
  failure in the ruamel-yaml-clib build).
- tests/install/e2e-assets/source-build-env.ps1: clear the identity variables through the
  env: drive — [Environment]::SetEnvironmentVariable(..., 'Process') on .NET/Unix does not
  reach spawned children, so the stamp child still saw GITHUB_SHA. Red→green under nix pwsh.
- old-updater surface: warm_agent_browser_npx_cache is a permanent def in both facade and
  sibling (the frozen surface names both); its compat pointer is retired, and the shims test's
  __module__ check holds.
- tests re-seamed / de-faked: import guard tests use the probe_root fixture (CI has no
  editable finder), the takeover child tree gets a hermes_constants stub, posix.sh hand-off
  test pins HERMES_HOME (our script honours the ambient one), the two Windows-layout
  PYTHONPATH tests are platforms("windows") (they fake `Lib/site-packages` on Linux; pm's
  site_packages() is host-correct), setup-pin test picks Git bash over System32's WSL stub,
  expose_cli's Windows test asserts the installer-convention convergence (the branch retired
  "windows-installer-owned"), plugin-manifest satisfied-dep fixture uses a core dep (pyyaml is
  gone), housekeeping test yaml imports go through hermes_yaml.
- apps/desktop main.ts: three imports restored in round 4 whose users main removed.
2026-09-21 03:44:00 -04:00

355 lines
15 KiB
Python

"""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()