Computer use and browser use are meant to work out of the box. The PM rewrite (3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install (baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed False, so the tool never reached the model and its lazy ensure could not fire, and browser_exec quietly fell back to the built-in tools. - cua-driver is a default PM package, so the installers, a bare `hermes pm install` and `hermes update` carry it on every target it builds for. Adds the missing Android gap (the lock has no bionic artifact). - The shared default-tool step, which the installers (via source completion) and `hermes update` both run, provisions the Browser Use CLI for the default and explicit Browser Use backends. `--skip-browser` declines it along with agent-browser, and `off`/Camofox never use it. - Installers regain --skip-computer-use / -SkipComputerUse (recorded as `--without cua-driver`). - The update message stops calling every default "browser tools".
160 lines
6.8 KiB
Python
160 lines
6.8 KiB
Python
"""The default `pm install` closure must stage the interpreter boot requires.
|
|
|
|
A fresh source install emits boot launchers (the source completion's
|
|
publish_launchers, hermes_cli/_launchers.py) that exec the pm STORE
|
|
interpreter. The `python`
|
|
package is marked optional (dev installs use their own venv; sealed bundles
|
|
adopt a shipped one), so the old default closure — every non-optional
|
|
lockfile package — skipped it and left `hermes` unbootable (audit C05).
|
|
|
|
Behavioral: drive cmd_install with the real lockfile/registry and stub
|
|
installers, asserting on the requested package names, not on script text.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
import pm.cli
|
|
|
|
|
|
@pytest.fixture()
|
|
def install_spy(monkeypatch):
|
|
calls = {"names": None, "verified": None, "sync_extras": None, "activated": [], "fail": set()}
|
|
|
|
def fake_install_names(names, target=None, *, verify=True):
|
|
# Accumulate: optional defaults install in their own call after the
|
|
# required closure.
|
|
calls["names"] = [*(calls["names"] or []), *names]
|
|
calls["verified"] = verify
|
|
return len(calls["fail"] & set(names))
|
|
|
|
def fake_sync_venv(extras=None, **kwargs):
|
|
calls["sync_extras"] = list(extras or [])
|
|
return None
|
|
|
|
def fake_activate(**kwargs):
|
|
calls["activated"].append(kwargs)
|
|
return []
|
|
|
|
monkeypatch.setattr(pm.cli, "_install_names", fake_install_names)
|
|
monkeypatch.setattr(importlib.import_module("pm.install"), "sync_venv", fake_sync_venv)
|
|
monkeypatch.setattr(importlib.import_module("pm.install"), "activate", fake_activate)
|
|
return calls
|
|
|
|
|
|
def test_default_closure_includes_the_boot_interpreter(install_spy):
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=None, tools_only=False)) == 0
|
|
assert "python" in install_spy["names"]
|
|
assert "venv" not in install_spy["names"]
|
|
assert all(not pm.cli.get_package(name).internal for name in install_spy["names"])
|
|
assert install_spy["sync_extras"] == ["all"]
|
|
assert install_spy["activated"] == [{"allow_incomplete": True}]
|
|
|
|
|
|
@pytest.mark.parametrize("names", [["npm", "ripgrep"], ["dmgbuild"]])
|
|
def test_explicit_names_pass_through_untouched(install_spy, names) -> None:
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=names, tools_only=False)) == 0
|
|
assert install_spy["names"] == names
|
|
assert install_spy["sync_extras"] is None
|
|
assert install_spy["activated"] == []
|
|
|
|
|
|
def test_tools_only_publishes_tools_and_stops_before_the_venv(install_spy):
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=None, extra=[], target=None, tools_only=True)) == 0
|
|
assert "python" in install_spy["names"]
|
|
assert "venv" not in install_spy["names"]
|
|
assert install_spy["sync_extras"] is None
|
|
assert install_spy["activated"] == [{"allow_incomplete": True}]
|
|
|
|
|
|
def test_trust_recorded_skips_the_byte_check_and_still_syncs(install_spy):
|
|
assert pm.cli.cmd_install(argparse.Namespace(
|
|
names=None, extra=[], target=None, tools_only=False, trust_recorded=True)) == 0
|
|
assert "python" in install_spy["names"]
|
|
assert install_spy["verified"] is False
|
|
assert install_spy["sync_extras"] == ["all"]
|
|
|
|
|
|
@pytest.mark.parametrize("kwargs, message", [
|
|
({"names": ["ripgrep"], "extra": [], "target": None, "tools_only": False, "trust_recorded": True},
|
|
"--trust-recorded"),
|
|
({"names": None, "extra": ["anthropic"], "target": None, "tools_only": False, "trust_recorded": True},
|
|
"--trust-recorded"),
|
|
({"names": None, "extra": [], "target": "linux-x64", "tools_only": False, "trust_recorded": True},
|
|
"--target"),
|
|
])
|
|
def test_trust_recorded_refuses_a_narrowed_install(install_spy, capsys, kwargs, message):
|
|
assert pm.cli.cmd_install(argparse.Namespace(**kwargs)) == 1
|
|
assert message in capsys.readouterr().out
|
|
assert install_spy["names"] is None
|
|
assert install_spy["sync_extras"] is None
|
|
|
|
|
|
def test_a_missing_tool_blocks_the_venv_sync(install_spy, monkeypatch, capsys):
|
|
monkeypatch.setattr(
|
|
importlib.import_module("pm.install"), "activate",
|
|
lambda **kwargs: ["git: not installed or outdated"],
|
|
)
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=None, tools_only=False)) == 1
|
|
assert install_spy["sync_extras"] is None
|
|
assert "git: not installed or outdated" in capsys.readouterr().out
|
|
|
|
|
|
def _bare_install(install_spy, **kwargs) -> list[str]:
|
|
install_spy["names"] = None
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=None, tools_only=False, **kwargs)) == 0
|
|
return install_spy["names"]
|
|
|
|
|
|
def test_browser_tools_are_a_default_the_user_can_decline_and_restore(install_spy):
|
|
assert "agent-browser" in _bare_install(install_spy)
|
|
assert "agent-browser" not in _bare_install(install_spy, without=["agent-browser"])
|
|
# The opt-out is recorded: a later bare install (the updater's shape) keeps it.
|
|
assert "agent-browser" not in _bare_install(install_spy)
|
|
install_spy["names"] = None
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=["agent-browser"], tools_only=False)) == 0
|
|
assert "agent-browser" in _bare_install(install_spy)
|
|
|
|
|
|
def test_a_failed_default_download_does_not_fail_the_install(install_spy, capsys):
|
|
install_spy["fail"] = {"agent-browser"}
|
|
assert "agent-browser" in _bare_install(install_spy)
|
|
assert install_spy["sync_extras"] == ["all"]
|
|
assert "hermes pm install agent-browser" in capsys.readouterr().out
|
|
|
|
|
|
def test_termux_has_no_pm_browser_default():
|
|
from pm.defaults import default_packages
|
|
|
|
names = pm.cli._lockfile().names()
|
|
assert "agent-browser" in default_packages(names, target="linux-x64", declined_names=frozenset())
|
|
assert default_packages(names, target="linux-arm64-bionic", declined_names=frozenset()) == []
|
|
|
|
|
|
def test_without_refuses_a_required_package(install_spy, capsys):
|
|
assert pm.cli.cmd_install(argparse.Namespace(names=None, tools_only=False, without=["git"])) == 1
|
|
assert "--without accepts only" in capsys.readouterr().out
|
|
assert install_spy["names"] is None
|
|
|
|
|
|
def test_default_install_carries_the_computer_use_driver_wherever_it_builds():
|
|
"""Computer use's only OS path ships in the box; a default claimed for a target
|
|
with no pinned artifact would fail every install there (the bionic KeyError)."""
|
|
from pm.defaults import default_packages
|
|
from pm.lock import Lockfile
|
|
from pm.paths import lockfile_path
|
|
from pm.store import ALL_TARGETS
|
|
|
|
lock = Lockfile(lockfile_path())
|
|
for target in ALL_TARGETS:
|
|
defaults = default_packages(lock.names(), target=target, declined_names=frozenset())
|
|
assert all(lock.artifacts(name, target) for name in defaults), target
|
|
driver = pm.cli.get_package("cua-driver")
|
|
assert ("cua-driver" in defaults) == (driver.missing_reason(target) is None), target
|
|
assert "cua-driver" not in default_packages(
|
|
lock.names(), target="linux-x64", declined_names=frozenset({"cua-driver"}))
|