- 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.
355 lines
15 KiB
Python
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() |