hermes-agent/tests/hermes_cli/test_ensure_acp_launcher.py
Teknium 38cd253abd feat(update): self-heal the hermes-acp launcher + document Buzz Desktop as an ACP host
Existing installs predate the install.sh hermes-acp launcher, and hermes
update never re-runs setup_path, so ACP hosts (Zed, JetBrains, Buzz)
still resolve Hermes as unavailable until a reinstall. _ensure_acp_launcher()
writes the launcher next to an existing hermes command in ~/.local/bin or
/usr/local/bin during hermes update — delegating to the sibling launcher so
it is correct for every install layout. Never follows symlinks (#21454),
skips unwritable dirs, no-op on Windows (venv Scripts is already on PATH).

Docs: add a Buzz Desktop section to the ACP page (en + zh-Hans).
2026-07-28 11:53:12 -07:00

121 lines
3.9 KiB
Python

"""`hermes update` must self-heal the ``hermes-acp`` launcher.
ACP hosts (Zed, JetBrains, Buzz Desktop) resolve the agent by the
``hermes-acp`` command name on the login-shell PATH. Fresh installs get the
launcher from ``scripts/install.sh``; existing installs get it from
``_ensure_acp_launcher()`` during ``hermes update``.
"""
import os
import stat
from pathlib import Path
from unittest.mock import patch
import pytest
from hermes_cli.main import _ensure_acp_launcher
@pytest.fixture
def fake_home(tmp_path, monkeypatch):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
bin_dir = tmp_path / ".local" / "bin"
bin_dir.mkdir(parents=True)
return bin_dir
def test_writes_launcher_next_to_hermes(fake_home):
hermes = fake_home / "hermes"
hermes.write_text("#!/usr/bin/env bash\nexec true\n", encoding="utf-8")
hermes.chmod(0o755)
_ensure_acp_launcher()
acp = fake_home / "hermes-acp"
assert acp.is_file()
assert acp.stat().st_mode & stat.S_IXUSR
text = acp.read_text(encoding="utf-8")
# Delegates to the sibling `hermes` launcher with the acp subcommand.
assert f'exec "{hermes}" acp "$@"' in text
def test_noop_without_hermes_launcher(fake_home):
_ensure_acp_launcher()
assert not (fake_home / "hermes-acp").exists()
def test_does_not_overwrite_existing_command(fake_home):
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
existing = fake_home / "hermes-acp"
marker = "#!/usr/bin/env python\n# real console script\n"
existing.write_text(marker, encoding="utf-8")
_ensure_acp_launcher()
assert existing.read_text(encoding="utf-8") == marker
def test_does_not_follow_symlink_into_venv(fake_home, tmp_path):
"""#21454 failure mode: never write through a symlinked hermes-acp."""
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
console_script = tmp_path / "venv" / "bin" / "hermes-acp"
console_script.parent.mkdir(parents=True)
marker = "#!/usr/bin/env python\n# real console script\n"
console_script.write_text(marker, encoding="utf-8")
(fake_home / "hermes-acp").symlink_to(console_script)
_ensure_acp_launcher()
assert console_script.read_text(encoding="utf-8") == marker
assert (fake_home / "hermes-acp").is_symlink()
def test_skips_broken_symlink(fake_home, tmp_path):
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
dangling = fake_home / "hermes-acp"
dangling.symlink_to(tmp_path / "gone")
_ensure_acp_launcher()
assert dangling.is_symlink()
assert not dangling.exists()
def test_symlinked_hermes_counts_as_present(fake_home, tmp_path):
"""FHS-style installs symlink ~/.local/bin/hermes — still eligible."""
real = tmp_path / "real-hermes"
real.write_text("#!/bin/sh\n", encoding="utf-8")
(fake_home / "hermes").symlink_to(real)
_ensure_acp_launcher()
assert (fake_home / "hermes-acp").is_file()
def test_idempotent(fake_home):
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
_ensure_acp_launcher()
first = (fake_home / "hermes-acp").read_text(encoding="utf-8")
_ensure_acp_launcher()
assert (fake_home / "hermes-acp").read_text(encoding="utf-8") == first
def test_noop_on_windows(fake_home):
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
with patch("hermes_cli.main.sys") as fake_sys:
fake_sys.platform = "win32"
_ensure_acp_launcher()
assert not (fake_home / "hermes-acp").exists()
def test_unwritable_bin_dir_is_skipped(fake_home):
(fake_home / "hermes").write_text("#!/bin/sh\n", encoding="utf-8")
if os.geteuid() == 0:
pytest.skip("root ignores directory write permissions")
fake_home.chmod(0o555)
try:
_ensure_acp_launcher() # must not raise
assert not (fake_home / "hermes-acp").exists()
finally:
fake_home.chmod(0o755)