mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
The Zed ACP Registry path (uvx --from 'hermes-agent[acp]==X' hermes-acp)
gets a Python-only install. Browser tools depend on the agent-browser npm
package + Chromium, neither of which are in the wheel. Without an
explicit bootstrap, registry users have no path to working browser tools.
Ship a bundled, idempotent bootstrap script (Linux/macOS bash + Windows
PowerShell) inside acp_adapter/bootstrap/ as wheel package-data. New
entry points:
hermes acp --setup-browser # interactive; prompts before Chromium download
hermes acp --setup-browser --yes # non-interactive
hermes-acp --setup-browser
The terminal-auth flow (hermes acp --setup) also offers the browser
bootstrap as a follow-up after model selection, so first-run registry
users get the option without knowing the flag exists.
Key design choices:
- npm install -g --prefix $NODE_PREFIX so we never need sudo. System Node
on PATH is respected; only the install target is redirected to the
user-writable Hermes-managed Node prefix.
- tools/browser_tool.py::_browser_candidate_path_dirs() already walks
$HERMES_HOME/node/bin, so installed binaries are discovered with no
agent-side code change.
- System Chrome/Chromium detection short-circuits the ~400 MB Playwright
download when a suitable browser already exists.
- Bash + PowerShell live as ONE copy each under acp_adapter/bootstrap/.
Not duplicated under scripts/. install.sh and install.ps1 keep their
inline browser blocks for the source-checkout path.
E2E validated end-to-end:
bash bootstrap_browser_tools.sh --skip-chromium
→ installs agent-browser into ~/.hermes/node/bin/
tools.browser_tool._find_agent_browser()
→ returns the installed path
check_browser_requirements()
→ returns True (browser tools register)
Tests:
- tests/acp/test_entry.py: 11 tests covering --setup-browser dispatch
(linux + windows + --yes forwarding + failure propagation), the
terminal-auth follow-up prompt path, and a package-data wheel-shipping
assertion that catches any future pyproject.toml regression.
Docs: website/docs/user-guide/features/acp.md gains a 'Browser tools
(optional)' subsection with the two-line install + what-it-does.
196 lines
5.5 KiB
Python
196 lines
5.5 KiB
Python
"""Tests for acp_adapter.entry startup wiring."""
|
|
|
|
import sys
|
|
|
|
import acp
|
|
import pytest
|
|
|
|
from acp_adapter import entry
|
|
|
|
|
|
def test_main_enables_unstable_protocol(monkeypatch):
|
|
calls = {}
|
|
|
|
async def fake_run_agent(agent, **kwargs):
|
|
calls["kwargs"] = kwargs
|
|
|
|
monkeypatch.setattr(entry, "_setup_logging", lambda: None)
|
|
monkeypatch.setattr(entry, "_load_env", lambda: None)
|
|
monkeypatch.setattr(acp, "run_agent", fake_run_agent)
|
|
|
|
entry.main([])
|
|
|
|
assert calls["kwargs"]["use_unstable_protocol"] is True
|
|
|
|
|
|
def test_main_version_prints_without_starting_server(monkeypatch, capsys):
|
|
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
|
|
|
|
entry.main(["--version"])
|
|
|
|
output = capsys.readouterr().out.strip()
|
|
assert output
|
|
assert "Starting hermes-agent ACP adapter" not in output
|
|
|
|
|
|
def test_main_check_prints_ok_without_starting_server(monkeypatch, capsys):
|
|
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
|
|
|
|
entry.main(["--check"])
|
|
|
|
assert capsys.readouterr().out.strip() == "Hermes ACP check OK"
|
|
|
|
|
|
def test_main_setup_runs_model_configuration(monkeypatch):
|
|
calls = {}
|
|
|
|
def fake_hermes_main():
|
|
calls["argv"] = sys.argv[:]
|
|
|
|
monkeypatch.setattr("hermes_cli.main.main", fake_hermes_main)
|
|
# Pretend stdin is not a TTY so the follow-up browser prompt is skipped.
|
|
# That keeps this test focused on the model-setup wiring; the
|
|
# browser-prompt path has its own test below.
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
|
|
entry.main(["--setup"])
|
|
|
|
assert calls["argv"][1:] == ["model"]
|
|
|
|
|
|
def test_main_setup_offers_browser_install_when_tty(monkeypatch):
|
|
"""When stdin is a TTY and the user answers yes, model setup is followed
|
|
by a browser-tools bootstrap call."""
|
|
monkeypatch.setattr("hermes_cli.main.main", lambda: None)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
monkeypatch.setattr("builtins.input", lambda *_args, **_kwargs: "y")
|
|
|
|
bootstrap_calls = []
|
|
monkeypatch.setattr(
|
|
entry,
|
|
"_run_setup_browser",
|
|
lambda assume_yes=False: bootstrap_calls.append(assume_yes) or 0,
|
|
)
|
|
|
|
entry.main(["--setup"])
|
|
|
|
assert bootstrap_calls == [False]
|
|
|
|
|
|
def test_main_setup_skips_browser_prompt_on_no(monkeypatch):
|
|
monkeypatch.setattr("hermes_cli.main.main", lambda: None)
|
|
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
monkeypatch.setattr("builtins.input", lambda *_args, **_kwargs: "")
|
|
|
|
called = []
|
|
monkeypatch.setattr(
|
|
entry,
|
|
"_run_setup_browser",
|
|
lambda assume_yes=False: called.append(assume_yes) or 0,
|
|
)
|
|
|
|
entry.main(["--setup"])
|
|
|
|
assert called == []
|
|
|
|
|
|
def test_main_setup_browser_invokes_bundled_script(monkeypatch):
|
|
"""`hermes-acp --setup-browser` must shell out to the bundled bootstrap
|
|
script — never reimplement the install logic inline."""
|
|
monkeypatch.setattr("platform.system", lambda: "Linux")
|
|
|
|
captured = {}
|
|
|
|
def fake_run(cmd, check=False):
|
|
captured["cmd"] = cmd
|
|
|
|
class _R:
|
|
returncode = 0
|
|
|
|
return _R()
|
|
|
|
monkeypatch.setattr("subprocess.run", fake_run)
|
|
|
|
entry.main(["--setup-browser"])
|
|
|
|
assert captured["cmd"][0] == "bash"
|
|
assert captured["cmd"][1].endswith("bootstrap_browser_tools.sh")
|
|
# --yes is NOT passed when the flag is absent.
|
|
assert "--yes" not in captured["cmd"]
|
|
|
|
|
|
def test_main_setup_browser_forwards_yes_flag(monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Linux")
|
|
|
|
captured = {}
|
|
|
|
def fake_run(cmd, check=False):
|
|
captured["cmd"] = cmd
|
|
|
|
class _R:
|
|
returncode = 0
|
|
|
|
return _R()
|
|
|
|
monkeypatch.setattr("subprocess.run", fake_run)
|
|
|
|
entry.main(["--setup-browser", "--yes"])
|
|
|
|
assert "--yes" in captured["cmd"]
|
|
|
|
|
|
def test_main_setup_browser_uses_powershell_on_windows(monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
|
|
|
captured = {}
|
|
|
|
def fake_run(cmd, check=False):
|
|
captured["cmd"] = cmd
|
|
|
|
class _R:
|
|
returncode = 0
|
|
|
|
return _R()
|
|
|
|
monkeypatch.setattr("subprocess.run", fake_run)
|
|
|
|
entry.main(["--setup-browser", "--yes"])
|
|
|
|
assert captured["cmd"][0] == "powershell.exe"
|
|
assert any(part.endswith("bootstrap_browser_tools.ps1") for part in captured["cmd"])
|
|
assert "-Yes" in captured["cmd"]
|
|
|
|
|
|
def test_main_setup_browser_propagates_failure(monkeypatch):
|
|
monkeypatch.setattr("platform.system", lambda: "Linux")
|
|
|
|
class _R:
|
|
returncode = 7
|
|
|
|
monkeypatch.setattr("subprocess.run", lambda cmd, check=False: _R())
|
|
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
entry.main(["--setup-browser"])
|
|
assert excinfo.value.code == 7
|
|
|
|
|
|
def test_bootstrap_scripts_ship_with_package():
|
|
"""The package-data wiring (pyproject.toml) must include the bootstrap
|
|
scripts — otherwise `--setup-browser` 404s at runtime."""
|
|
from pathlib import Path
|
|
|
|
bootstrap_dir = Path(entry.__file__).resolve().parent / "bootstrap"
|
|
sh = bootstrap_dir / "bootstrap_browser_tools.sh"
|
|
ps1 = bootstrap_dir / "bootstrap_browser_tools.ps1"
|
|
|
|
assert sh.is_file(), f"missing bundled script: {sh}"
|
|
assert ps1.is_file(), f"missing bundled script: {ps1}"
|
|
|
|
sh_text = sh.read_text(encoding="utf-8")
|
|
ps1_text = ps1.read_text(encoding="utf-8")
|
|
|
|
# Sanity: scripts know how to find the Hermes-managed Node prefix.
|
|
assert "HERMES_HOME" in sh_text
|
|
assert "agent-browser" in sh_text
|
|
assert "HermesHome" in ps1_text
|
|
assert "agent-browser" in ps1_text
|