Files
hermes-agent/tests/hermes_cli/test_linux_desktop_entry.py
Teknium 6fe933e709 fix(cli): launch-context-independent Linux desktop-entry Exec (salvaged from #94874)
Rewrites resolve_exec_command so the generated .desktop Exec no longer
depends on how the installer happened to be launched: fixes the bare
repo-script form whose shebang escapes the venv, and the symlinked-venv
form that .resolve() dereferenced into the base interpreter store.

Salvaged squashed from PR #94874 (24 commits) after the original branch
was found to carry stray __pycache__/.gitignore payload.

Co-authored-by: Gökhan <gkhn.yldrmlr@gmail.com>
2026-08-31 10:07:51 -07:00

989 lines
36 KiB
Python

"""Tests for the Linux XDG desktop entry installed by ``hermes desktop``."""
from __future__ import annotations
import os
import stat
import sys
from pathlib import Path
import pytest
from hermes_cli import linux_desktop_entry as lde
@pytest.fixture
def xdg_home(tmp_path, monkeypatch) -> Path:
data_home = tmp_path / "xdg-data"
monkeypatch.setenv("XDG_DATA_HOME", str(data_home))
# Isolate the known-wrapper probe too: tests must never see the real
# ~/.local/bin/hermes on the dev machine.
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setattr(lde.sys, "platform", "linux")
return data_home
def _make_project(tmp_path: Path) -> Path:
root = tmp_path / "hermes-agent"
icon = root / "apps" / "desktop" / "assets" / "icon.png"
icon.parent.mkdir(parents=True)
icon.write_bytes(b"\x89PNG fake")
return root
def _parse(entry_text: str) -> dict:
values = {}
for line in entry_text.splitlines():
if "=" in line and not line.startswith("["):
key, val = line.split("=", 1)
values[key] = val
return values
def test_install_writes_entry_with_absolute_exec_and_icon(
tmp_path, xdg_home, monkeypatch
):
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text("", encoding="utf-8")
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
# Keep the icon install out of the way: this test pins the
# absolute-path FALLBACK (copy impossible / not attempted here).
monkeypatch.setattr(lde, "_install_icon_to_hicolor", lambda _icon: False)
entry = lde.install_desktop_entry(root)
assert entry == xdg_home / "applications" / "hermes.desktop"
values = _parse(entry.read_text(encoding="utf-8"))
# Exec must be the absolute path of the resolved binary. The launcher
# runs with a minimal PATH, so a bare `hermes` would not resolve.
assert values["Exec"] == f"{hermes_bin} desktop"
assert Path(values["Exec"].split(" ")[0]).is_absolute()
# Icon must be an absolute path to the real icon in the checkout.
icon_path = Path(values["Icon"])
assert icon_path.is_absolute()
assert icon_path == lde.icon_path(root)
def test_install_prefers_themed_icon_from_hicolor(tmp_path, xdg_home, monkeypatch):
"""When the icon installs into hicolor, the entry uses the themed name.
The themed name survives a moved/archived checkout; an absolute
Icon= path does not (the same durability class the Exec line was
fixed for).
"""
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text("", encoding="utf-8")
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
values = _parse(entry.read_text(encoding="utf-8"))
assert values["Icon"] == "hermes"
# And the icon really landed in the hicolor tree: the fixture icon is
# a fake PNG (no valid IHDR), so the size is unknown and the icon
# lands under scalable/.
dest = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
assert dest.is_file()
assert dest.read_bytes() == lde.icon_path(root).read_bytes()
def test_install_icon_copy_failure_falls_back_to_absolute(
tmp_path, xdg_home, monkeypatch
):
"""An impossible icon copy keeps the absolute path (never breaks)."""
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text("", encoding="utf-8")
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
def _boom(src, dst):
raise OSError("read-only tree")
monkeypatch.setattr(lde.shutil, "copyfile", _boom)
entry = lde.install_desktop_entry(root)
values = _parse(entry.read_text(encoding="utf-8"))
# The real helper catches the copy OSError and returns False, so the
# caller falls back to the absolute path without raising.
assert values["Icon"] == str(lde.icon_path(root))
assert values["Type"] == "Application"
assert values["Name"] == "Hermes"
assert values["Terminal"] == "false"
def test_installed_entry_is_executable(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
assert entry.stat().st_mode & stat.S_IXUSR
def test_exec_falls_back_to_interpreter_module(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert exec_line.endswith("-m hermes_cli.main desktop")
assert Path(exec_line.split(" ")[0]).is_absolute()
# #90292: the shell installer's bash wrapper makes argv[0] the repo `hermes`
# python script whose `#!/usr/bin/env python3` shebang resolves to the SYSTEM
# interpreter when the DE spawns the .desktop entry → ModuleNotFoundError,
# silent (Terminal=false). The Exec line must prefix sys.executable for any
# resolved bin that is a python script escaping the running venv.
def test_exec_prefixes_interpreter_for_env_shebang_python_script(
tmp_path, xdg_home, monkeypatch
):
import sys
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
hermes_bin.chmod(0o755)
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
interpreter = os.path.abspath(sys.executable)
assert exec_line.split(" ")[0].strip('"') == interpreter
assert str(hermes_bin) in exec_line
assert exec_line.endswith("desktop")
def test_exec_leaves_shell_wrapper_launchers_alone(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text(
'#!/bin/bash\nexec /opt/hermes/venv/bin/python "$@"\n', encoding="utf-8"
)
hermes_bin.chmod(0o755)
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# A bash wrapper execs the venv python itself — no interpreter prefix.
assert exec_line == f"{hermes_bin} desktop"
def test_exec_leaves_venv_shebang_scripts_alone(tmp_path, xdg_home, monkeypatch):
import sys
root = _make_project(tmp_path)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
interpreter = os.path.abspath(sys.executable)
hermes_bin.write_text(f"#!{interpreter}\nimport hermes_cli\n", encoding="utf-8")
hermes_bin.chmod(0o755)
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# Console-script with the venv's own interpreter in the shebang: correct
# as-is, prefixing would only add noise.
assert exec_line == f"{hermes_bin} desktop"
# The persisted entry must be launch-context independent: whatever process
# writes it, the next launch reads and rewrites the same bytes. argv[0]
# differs per launch path (wrapper / repo script / python -m), so a
# checkout-internal argv[0] must not be persisted — the resolver falls
# through to PATH, where the installer's durable wrapper lives.
def _argv0_context(monkeypatch, argv0: str) -> None:
import sys
monkeypatch.setattr(sys, "argv", [argv0, "desktop"])
def test_exec_converges_from_repo_script_argv0_to_installed_wrapper(
tmp_path, xdg_home, monkeypatch
):
"""A broken interpreter-form entry must self-heal to the wrapper form.
Launching with argv[0] = <checkout>/hermes (what the broken entry
itself spawns) previously re-persisted the same broken form forever —
the bootstrap loop that kept #90492 from repairing existing installs.
"""
import sys
root = _make_project(tmp_path)
repo_script = root / "hermes" # checkout-internal launcher candidate
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
wrapper = tmp_path / "installed" / "bin" / "hermes"
wrapper.parent.mkdir(parents=True)
wrapper.write_text(f'#!/bin/bash\nexec {sys.executable} "$@"\n', encoding="utf-8")
wrapper.chmod(0o755)
# argv[0] = repo script; PATH lookup finds the installed wrapper.
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr(
"shutil.which", lambda name: str(wrapper) if name == "hermes" else None
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# Converged on the durable wrapper — NOT the repo script, and NOT an
# interpreter-prefixed form pinning sys.executable.
assert exec_line == f"{wrapper} desktop"
def test_exec_never_persists_a_bare_interpreter_command(
tmp_path, xdg_home, monkeypatch
):
"""The `python -m hermes_cli.main` relaunch context must not write
`Exec=<python> desktop` — a command line no DE can run."""
import sys
root = _make_project(tmp_path)
wrapper = tmp_path / "installed" / "bin" / "hermes"
wrapper.parent.mkdir(parents=True)
wrapper.write_text("#!/bin/bash\nexit 0\n", encoding="utf-8")
wrapper.chmod(0o755)
interpreter = tmp_path / "uv" / "cpython-3.11.15" / "bin" / "python3.11"
interpreter.parent.mkdir(parents=True)
interpreter.write_bytes(b"\x7fELF fake")
interpreter.chmod(0o755)
# argv[0] IS the interpreter (python -m context); PATH has the wrapper.
_argv0_context(monkeypatch, str(interpreter))
monkeypatch.setattr(
"shutil.which", lambda name: str(wrapper) if name == "hermes" else None
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
first_token = exec_line.split(" ")[0].strip('"')
assert Path(first_token) != interpreter
assert not (
Path(first_token).name.startswith("python")
and "desktop" in exec_line.split(" ", 1)[1]
), f"persisted an unrunnable bare-interpreter Exec: {exec_line}"
assert exec_line == f"{wrapper} desktop"
def test_exec_keeps_resolver_fallback_when_no_wrapper_on_path(
tmp_path, xdg_home, monkeypatch
):
"""No wrapper anywhere → #90492's runnable fallback, never a dead Exec.
With argv[0] checkout-internal and PATH + known locations both empty,
the resolver returns None and resolve_exec_command emits the runnable
`sys.executable -m hermes_cli.main desktop` fallback. Persisting the
interpreter itself (`<python> desktop`) would be unrunnable by any DE;
persisting the repo script alone dies on its env shebang.
"""
import sys
root = _make_project(tmp_path)
repo_script = root / "hermes"
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr("shutil.which", lambda name: None)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
def fake_resolve():
# Mirror resolve_hermes_bin's chain: argv[0] → relative → PATH → None.
return sys.argv[0] if sys.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# The runnable module fallback — NOT the bare repo script (its env
# shebang would escape the venv under a DE) and NOT `<python> desktop`.
assert exec_line.endswith("-m hermes_cli.main desktop")
assert Path(exec_line.split(" ")[0].strip('"')).is_absolute()
assert str(repo_script) not in exec_line
def test_exec_uses_known_wrapper_when_path_lookup_misses(
tmp_path, xdg_home, monkeypatch
):
"""Stripped-PATH session + wrapper at the known installer location.
systemd user sessions and autostart relaunches often run without
~/.local/bin on PATH. When shutil.which finds nothing, the resolver
must probe the known durable locations directly instead of silently
persisting a checkout-internal Exec line.
"""
import sys
root = _make_project(tmp_path)
repo_script = root / "hermes"
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
# The wrapper exists at the known location but is NOT on PATH.
# Realistic installer shim: execs this checkout's venv python on the
# checkout's hermes script (the aidiyet check requires it to target
# the writing checkout).
known_wrapper = tmp_path / "known-home" / ".local" / "bin" / "hermes"
known_wrapper.parent.mkdir(parents=True)
known_wrapper.write_text(
f'#!/bin/bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
encoding="utf-8",
)
known_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(tmp_path / "known-home"))
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr("shutil.which", lambda name: None)
def fake_resolve():
return sys.argv[0] if sys.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# The probe found the wrapper despite the PATH miss.
assert exec_line == f"{known_wrapper} desktop"
def test_exec_rejects_known_wrapper_from_another_checkout(
tmp_path, xdg_home, monkeypatch
):
"""A known-location wrapper that targets a DIFFERENT checkout is skipped.
On machines with multiple installs over time, ~/.local/bin/hermes may
belong to another checkout. Persisting it would make the entry stable
but silently point at that other installation — the failure class the
aidiyet check exists to prevent. The runnable module fallback must win
instead.
"""
import sys
root = _make_project(tmp_path)
repo_script = root / "hermes"
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
# A shim belonging to a DIFFERENT checkout.
other_root = tmp_path / "other-install"
other_root.mkdir()
foreign_wrapper = tmp_path / "known-home" / ".local" / "bin" / "hermes"
foreign_wrapper.parent.mkdir(parents=True)
foreign_wrapper.write_text(
f"#!/bin/bash\nexec {other_root / 'venv' / 'bin' / 'python'} "
f'{other_root / "hermes"} "$@"\n',
encoding="utf-8",
)
foreign_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(tmp_path / "known-home"))
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr("shutil.which", lambda name: None)
def fake_resolve():
return sys.argv[0] if sys.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# The foreign wrapper was rejected; the runnable module fallback won.
assert str(foreign_wrapper) not in exec_line
assert exec_line.endswith("-m hermes_cli.main desktop")
@pytest.mark.parametrize(
("layout", "env_overrides", "expected"),
[
pytest.param(
"user",
{},
"HOME-SET-BY-TEST/.local/bin/hermes",
id="user-layout",
),
pytest.param(
"termux",
{"PREFIX": "PREFIX-SET-BY-TEST"},
"PREFIX-SET-BY-TEST/bin/hermes",
id="termux-prefix-first",
),
pytest.param(
"root-fhs",
{"__EUID0__": "1"},
"/usr/local/bin/hermes",
id="root-fhs",
),
pytest.param(
"non-root-no-fhs",
{"__EUID0__": "0"},
"HOME-SET-BY-TEST/.local/bin/hermes",
id="non-root-excludes-fhs",
),
],
)
def test_known_wrapper_candidates_cover_installer_layouts(
layout, env_overrides, expected, monkeypatch
):
"""_known_wrapper_candidates mirrors get_command_link_dir() layouts.
Termux ($PREFIX/bin) outranks everything; root FHS (/usr/local/bin)
applies only to euid 0; the user layout (~/.local/bin) is always a
candidate. Locking these in protects against silent regressions in
the stripped-PATH probe path.
"""
import os
sentinel_home = "/home/__sentinel_home__"
monkeypatch.setenv("HOME", sentinel_home)
for key, value in env_overrides.items():
if key == "__EUID0__":
monkeypatch.setattr(lde.os, "geteuid", lambda: 0 if value == "1" else 1000)
else:
monkeypatch.setenv(key, value)
candidates = [str(c) for c in lde._known_wrapper_candidates()]
expected_resolved = expected.replace("HOME-SET-BY-TEST", sentinel_home)
assert expected_resolved in candidates
if layout == "termux":
# PREFIX outranks the user layout.
assert candidates[0] == expected_resolved
if layout == "root-fhs":
# Root FHS outranks the user layout.
assert candidates.index("/usr/local/bin/hermes") < candidates.index(
f"{sentinel_home}/.local/bin/hermes"
)
if layout == "non-root-no-fhs":
# Non-root euid: /usr/local/bin must be excluded outright.
assert "/usr/local/bin/hermes" not in candidates
def test_install_is_idempotent_and_skips_cache_refresh(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
)
calls: list[Path] = []
monkeypatch.setattr(
lde, "refresh_desktop_databases", lambda d: calls.append(d) or []
)
lde.install_desktop_entry(root)
assert len(calls) == 1
# Unchanged content → no rewrite, no menu-cache churn on every launch.
lde.install_desktop_entry(root)
assert len(calls) == 1
def test_install_without_source_icon_uses_themed_name(tmp_path, xdg_home, monkeypatch):
root = tmp_path / "hermes-agent"
root.mkdir()
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
# A broken absolute path renders as no icon. The themed name resolves
# when Hermes is installed some other way.
assert _parse(entry.read_text(encoding="utf-8"))["Icon"] == "hermes"
@pytest.mark.macos_only
def test_install_is_a_noop_on_macos(tmp_path):
"""Faking darwin only renamed the host — the real macOS runner is the
only place the `sys.platform` guard is exercised against a real host."""
assert lde.install_desktop_entry(_make_project(tmp_path)) is None
@pytest.mark.windows_only
def test_install_is_a_noop_on_windows(tmp_path):
"""As above for Windows: a fake left POSIX paths and a POSIX XDG layout
in place, so the no-op was never proven against a real one."""
assert lde.install_desktop_entry(_make_project(tmp_path)) is None
# ---------------------------------------------------------------------------
# Cache refresh tool gating
# ---------------------------------------------------------------------------
def _stub_tools(monkeypatch, available: "set[str]") -> "list[list[str]]":
ran: list[list[str]] = []
monkeypatch.setattr(
lde.shutil,
"which",
lambda name: f"/usr/bin/{name}" if name in available else None,
)
monkeypatch.setattr(lde, "_run_quiet", lambda cmd: ran.append(cmd) or True)
return ran
def test_refresh_runs_kbuildsycoca6_when_present(monkeypatch, tmp_path):
ran = _stub_tools(monkeypatch, {"update-desktop-database", "kbuildsycoca6"})
tools = lde.refresh_desktop_databases(tmp_path)
assert tools == ["update-desktop-database", "kbuildsycoca6"]
assert ran == [
["/usr/bin/update-desktop-database", str(tmp_path)],
["/usr/bin/kbuildsycoca6", "--noincremental"],
]
def test_refresh_falls_back_to_kbuildsycoca5(monkeypatch, tmp_path):
ran = _stub_tools(monkeypatch, {"kbuildsycoca5"})
tools = lde.refresh_desktop_databases(tmp_path)
assert tools == ["kbuildsycoca5"]
assert ran == [["/usr/bin/kbuildsycoca5", "--noincremental"]]
def test_refresh_prefers_kbuildsycoca6_over_5(monkeypatch, tmp_path):
ran = _stub_tools(monkeypatch, {"kbuildsycoca6", "kbuildsycoca5"})
lde.refresh_desktop_databases(tmp_path)
assert [cmd[0] for cmd in ran] == ["/usr/bin/kbuildsycoca6"]
def test_refresh_skips_missing_tools(monkeypatch, tmp_path):
ran = _stub_tools(monkeypatch, set())
assert lde.refresh_desktop_databases(tmp_path) == []
assert ran == []
def test_refresh_reports_only_tools_that_succeeded(monkeypatch, tmp_path):
monkeypatch.setattr(lde.shutil, "which", lambda name: f"/usr/bin/{name}")
# update-desktop-database fails (exit != 0). kbuildsycoca6 succeeds.
monkeypatch.setattr(lde, "_run_quiet", lambda cmd: "kbuildsycoca" in cmd[0])
assert lde.refresh_desktop_databases(tmp_path) == ["kbuildsycoca6"]
def test_run_quiet_swallows_missing_binary(tmp_path):
assert lde._run_quiet([str(tmp_path / "definitely-not-a-binary")]) is False
def test_exec_arg_quoting_handles_spaces(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
spaced = tmp_path / "my apps" / "hermes"
spaced.parent.mkdir()
spaced.write_text("", encoding="utf-8")
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: str(spaced))
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert exec_line == f'"{spaced}" desktop'
@pytest.mark.skipif(
sys.platform == "win32", reason="Symlinks require elevated privileges on Windows"
)
def test_running_interpreter_keeps_venv_semantic_path(tmp_path, monkeypatch):
"""Lexical preserved only when pyvenv.cfg marks the path as a venv."""
# venv layout: bin/python symlink -> base, pyvenv.cfg at venv root
base = tmp_path / "base" / "python3.11"
base.parent.mkdir(parents=True)
base.write_text("", encoding="utf-8")
venv_root = tmp_path / "venv"
venv_bin = venv_root / "bin"
venv_bin.mkdir(parents=True)
(venv_root / "pyvenv.cfg").write_text("home = /base\n", encoding="utf-8")
venv_python = venv_bin / "python"
venv_python.symlink_to(base)
monkeypatch.setattr(lde.sys, "executable", str(venv_python))
assert lde._running_interpreter() == str(venv_python)
# non-venv symlink: resolve instead (durability over lexical)
plain_root = tmp_path / "plain" / "bin"
plain_root.mkdir(parents=True)
plain_link = plain_root / "python3"
plain_link.symlink_to(base)
monkeypatch.setattr(lde.sys, "executable", str(plain_link))
assert lde._running_interpreter() == str(base)
def test_running_interpreter_resolves_plain_interpreter(monkeypatch):
"""A non-symlinked, non-venv executable resolves to itself."""
monkeypatch.setattr(lde.sys, "executable", "/usr/bin/python3")
out = lde._running_interpreter()
assert Path(out).is_absolute()
def test_can_import_probe_runs_and_caches(tmp_path):
"""The probe executes the real interpreter and memoizes the answer.
Asserts the two things that hold on ANY host: the probe returns a
definite boolean for a real interpreter (not None, not an exception
path), and the per-path cache is populated so the second call pays
no subprocess. Host-dependent capability itself (True vs False) is
deliberately NOT asserted - a CI host with hermes pip-installed
system-wide would legitimately answer True.
"""
import time
real = Path("/usr/bin/python3")
if not real.exists():
pytest.skip("no system python to probe")
lde._probe_cache.pop(str(real), None)
try:
first = lde._can_import_hermes_cli(real)
assert isinstance(first, bool)
assert str(real) in lde._probe_cache
t0 = time.monotonic()
second = lde._can_import_hermes_cli(real)
assert second is first
assert time.monotonic() - t0 < 0.05 # cache hit: no subprocess
finally:
lde._probe_cache.pop(str(real), None)
def test_exec_falls_back_to_running_interpreter_when_probe_fails(
tmp_path, xdg_home, monkeypatch
):
"""A candidate interpreter that fails the import probe is not persisted."""
import sys as _s
root = _make_project(tmp_path)
interpreter = tmp_path / "uv" / "bin" / "python3.11"
interpreter.parent.mkdir(parents=True)
interpreter.write_bytes(b"\x7fELF fake")
interpreter.chmod(0o755)
_argv0_context(monkeypatch, str(interpreter))
monkeypatch.setattr("shutil.which", lambda name: None)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
def fake_resolve():
return _s.argv[0] if _s.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
# Force the probe to fail for whatever interpreter gets chosen first.
monkeypatch.setattr(lde, "_can_import_hermes_cli", lambda p: False)
# And the fallback interpreter must itself pass (it always should).
monkeypatch.setattr(
lde,
"_running_interpreter_fallback",
lambda: os.path.abspath(sys.executable),
)
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
# Runnable module form under the RUNNING interpreter - never the
# unprobeable ELF fake, never a bare "<python> desktop".
assert exec_line.endswith("-m hermes_cli.main desktop")
first = exec_line.split(" ")[0].strip('"')
assert first == os.path.abspath(sys.executable)
assert str(interpreter) not in exec_line
@pytest.mark.parametrize(
"suffix",
["-old", ".bak", "-copy"],
)
def test_wrapper_ownership_rejects_sibling_extensions(suffix, tmp_path):
"""A shim execing `<checkout><suffix>/...` must NOT pass ownership.
Bare substring matching accepted these; the boundary-aware matcher
must reject them (stable-but-wrong entry pointing at the renamed
old install).
"""
checkout = tmp_path / "hermes-agent"
checkout.mkdir()
evil = tmp_path / "evil-shim"
evil.write_text(
f"#!/bin/bash\n"
f"exec {checkout}{suffix}/venv/bin/python "
f'{checkout}{suffix}/hermes "$@"\n',
encoding="utf-8",
)
assert lde._wrapper_targets_checkout(evil, checkout) is False
@pytest.mark.skipif(
sys.platform == "win32", reason="Symlinks require elevated privileges on Windows"
)
def test_wrapper_ownership_accepts_shim_via_symlinked_home(tmp_path, monkeypatch):
"""Installer writes $INSTALL_DIR lexically; the root stays lexical too.
With /home -> /real-home, a shim that references the lexical
checkout path must match the lexical checkout root (the resolved
root alone would never match the shim's text).
"""
home_link = tmp_path / "home-link"
home_real = tmp_path / "home-real"
home_real.mkdir()
home_link.symlink_to(home_real)
lexical_checkout = home_link / "hermes-agent"
(home_real / "hermes-agent").mkdir()
shim = home_link / ".local" / "bin" / "hermes"
shim.parent.mkdir(parents=True)
shim.write_text(
f"#!/bin/bash\n"
f"exec {lexical_checkout}/venv/bin/python "
f'{lexical_checkout}/hermes "$@"\n',
encoding="utf-8",
)
assert lde._wrapper_targets_checkout(shim, lexical_checkout) is True
# And the end-to-end probe finds it via the lexical root: make the
# shim executable, point HOME at the symlinked home, and give the
# resolver a checkout-internal primary (the repo script) so the
# probe leg actually engages.
shim.chmod(0o755)
monkeypatch.setenv("HOME", str(home_link))
monkeypatch.setattr("shutil.which", lambda name: None)
repo_script = lexical_checkout / "hermes"
repo_script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
monkeypatch.setattr(sys, "argv", [str(repo_script), "desktop"])
assert lde._resolve_hermes_bin_for_desktop_entry(
resolve_fn=lambda: sys.argv[0] if sys.argv[0] else None,
checkout_root=lexical_checkout,
) == str(shim)
def test_needs_interpreter_case_insensitive_match(tmp_path, monkeypatch):
"""Interpreter paths with uppercase must not flag own venv scripts.
The shebang is lowercased for comparison; the interpreter dir must
be too (conda env names, usernames, uv's ephemeral .tmpXXX dirs all
carry uppercase - an asymmetric compare would prefix the venv's own
console script spuriously).
"""
venv_bin = tmp_path / "MyEnv" / "bin"
venv_bin.mkdir(parents=True)
interpreter = venv_bin / "python"
interpreter.write_text("", encoding="utf-8")
console_script = venv_bin / "hermes"
console_script.write_text(f"#!{interpreter}\nimport hermes_cli\n", encoding="utf-8")
monkeypatch.setattr(lde.sys, "executable", str(interpreter))
assert lde._needs_interpreter(console_script) is False
def test_needs_interpreter_rejects_sibling_directory(tmp_path, monkeypatch):
"""``<venv>/bin-extra/python`` is NOT inside ``<venv>/bin``.
Substring matching accepted it (the parent dir appears verbatim inside
the sibling path), skipping the interpreter prefix for a script whose
shebang actually points OUTSIDE the venv. Path-component comparison
rejects it.
"""
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
interp = venv_bin / "python"
interp.write_text("", encoding="utf-8")
monkeypatch.setattr(lde.sys, "executable", str(interp))
sibling_script = tmp_path / "sibling"
sibling_script.write_text(
f"#!{tmp_path}/venv/bin-extra/python\nimport hermes_cli\n",
encoding="utf-8",
)
assert lde._needs_interpreter(sibling_script) is True
def test_needs_interpreter_strips_flags_before_comparing(tmp_path, monkeypatch):
"""A flagged own-venv shebang is not misclassified by the flag token."""
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
interp = venv_bin / "python"
interp.write_text("", encoding="utf-8")
monkeypatch.setattr(lde.sys, "executable", str(interp))
flagged = tmp_path / "flagged"
flagged.write_text(f"#!{interp} -S\nimport hermes_cli\n", encoding="utf-8")
assert lde._needs_interpreter(flagged) is False
def test_needs_interpreter_env_shebang_always_escapes(tmp_path, monkeypatch):
"""``env`` resolves through the DE's PATH - not the installer's."""
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
interp = venv_bin / "python"
interp.write_text("", encoding="utf-8")
monkeypatch.setattr(lde.sys, "executable", str(interp))
# Even when env itself sits in the venv bin (parent equality would
# pass), the PATH resolution semantics mean the shebang escapes.
env_script = tmp_path / "envscript"
env_script.write_text(
f"#!{venv_bin}/env python3\nimport hermes_cli\n", encoding="utf-8"
)
assert lde._needs_interpreter(env_script) is True
# ...unless env carries an absolute venv interpreter after -S.
env_abs = tmp_path / "envabs"
env_abs.write_text(
f"#!/usr/bin/env -S {interp}\nimport hermes_cli\n", encoding="utf-8"
)
assert lde._needs_interpreter(env_abs) is False
def test_probe_skips_wrapper_with_escaping_python_shebang(
tmp_path, xdg_home, monkeypatch
):
"""A checkout-referencing wrapper with an env shebang is skipped.
Ownership alone would accept it (the body references this checkout),
but its `#!/usr/bin/env python3` shebang dies in the DE context.
The shebang-safety gate skips it; the module fallback wins. Idea
credited to autumn8's #92122 rung-2 check.
"""
import sys as _s
root = _make_project(tmp_path)
repo_script = root / "hermes"
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
# A wrapper that targets this checkout but cannot run itself.
broken_wrapper = xdg_home / ".local" / "bin" / "hermes"
broken_wrapper.parent.mkdir(parents=True)
broken_wrapper.write_text(
f"#!/usr/bin/env python3\n# launcher for {root}\nimport hermes_cli\n",
encoding="utf-8",
)
broken_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(xdg_home))
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr("shutil.which", lambda name: None)
def fake_resolve():
return sys.argv[0] if sys.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert str(broken_wrapper) not in exec_line
assert exec_line.endswith("-m hermes_cli.main desktop")
def test_probe_accepts_shell_launcher_wrapper(tmp_path, xdg_home, monkeypatch):
"""A bash launcher is safe by construction and still wins the probe."""
root = _make_project(tmp_path)
repo_script = root / "hermes"
repo_script.write_text(
"#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
)
repo_script.chmod(0o755)
good_wrapper = xdg_home / ".local" / "bin" / "hermes"
good_wrapper.parent.mkdir(parents=True)
good_wrapper.write_text(
f"#!/bin/bash\nexec {root / 'venv' / 'bin' / 'python'} "
f'{root / "hermes"} "$@"\n',
encoding="utf-8",
)
good_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(xdg_home))
_argv0_context(monkeypatch, str(repo_script))
monkeypatch.setattr("shutil.which", lambda name: None)
def fake_resolve():
return sys.argv[0] if sys.argv[0] else None
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert exec_line == f"{good_wrapper} desktop"
def test_install_icon_handles_truncated_png_header(tmp_path, xdg_home, monkeypatch):
"""A truncated PNG (valid signature + IHDR tag, <24 bytes) must not
raise struct.error out of the fail-safe: it lands in scalable/ like
any other unknown-size image."""
root = _make_project(tmp_path)
icon = lde.icon_path(root)
icon.write_bytes(
b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00" # 22 bytes
)
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir()
hermes_bin.write_text("", encoding="utf-8")
monkeypatch.setattr(
"hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
values = _parse(entry.read_text(encoding="utf-8"))
assert values["Icon"] == "hermes"
dest = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
assert dest.is_file()