Files
hermes-agent/tests/hermes_cli/test_linux_desktop_entry.py
Octopustank a6686cc396 fix(desktop): carry the window app id in the Linux launcher entry
Packaged windows claim `com.nousresearch.hermes` as their Wayland app id
(electron-builder bakes product-identity.cjs's `appId` into
extraMetadata.desktopName; Electron hands that string to the compositor
verbatim), while the entry was written as `hermes.desktop` with
`StartupWMClass=Hermes` — so GNOME matched neither StartupWMClass nor a
`<app_id>.desktop` file name and every launch fell back to the placeholder
icon, with the raw app id in the tooltip.

- write `<app_id>.desktop` with `StartupWMClass=<app_id>` (Name= stays "Hermes")
- retire a leftover `hermes.desktop` once the new entry is on disk, and only
  when the file still names this app and launcher management is enabled;
  foreign files at that path are left alone
- nix/desktop.nix derives the entry file name from the module instead of
  hardcoding it
- tests: the installed entry carries the app id, legacy retirement, foreign-file
  preservation, opt-out preservation, plus a node probe asserting
  APP_ID == product-identity.cjs appId

Known consequence: an existing taskbar pin points at the old entry id and has to
be re-added once.
2026-09-27 07:46:31 -05:00

1266 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for the Linux XDG desktop entry installed by ``hermes desktop``."""
from __future__ import annotations
import io
import os
import stat
import struct
import sys
import time
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 _png_ihdr(width: int, height: int) -> bytes:
"""Minimal PNG prefix whose IHDR the installer can parse (no pixels)."""
return (
b"\x89PNG\r\n\x1a\n"
+ b"\x00\x00\x00\r"
+ b"IHDR"
+ struct.pack(">II", width, height)
)
def _stub_install(tmp_path, monkeypatch) -> None:
hermes_bin = tmp_path / "bin" / "hermes"
hermes_bin.parent.mkdir(exist_ok=True)
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 _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" / lde.DESKTOP_ENTRY_NAME
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 256x256/ (indexed; never scalable, which is SVG-only).
dest = xdg_home / "icons" / "hicolor" / "256x256" / "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(
'#!/usr/bin/env 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'#!/usr/bin/env 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."""
root = _make_project(tmp_path)
wrapper = tmp_path / "installed" / "bin" / "hermes"
wrapper.parent.mkdir(parents=True)
wrapper.write_text("#!/usr/bin/env 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'#!/usr/bin/env 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_never_persists_a_checkout_internal_path_hit(tmp_path, xdg_home, monkeypatch):
"""A PATH hit inside THIS checkout is a launch-context artifact, like argv[0].
The desktop-update hand-off hands the updater <checkout>/venv/bin at the
FRONT of PATH (apps/desktop/electron/main.ts), so argv[0] is the venv
console script — checkout-internal, correctly skipped as a durable
answer — and the reroute that hides argv[0] re-resolves over PATH and
hits THE SAME SCRIPT. The rerouted branch returned that hit outright,
persisting the venv form; the next DE launch re-resolves to the durable
wrapper and flips the bytes back. Alternating writers alternate the
file content (captured: wrapper -> venv -> wrapper inside one update
cycle), and every flip rewrites the entry. A rewrite landing
inside a grid launch's STARTING window is the arm for the gnome-shell
50.x crash this module already guards against. A PATH hit inside the
checkout must fall through to the durable probe.
"""
root = _make_project(tmp_path)
venv_script = root / "venv" / "bin" / "hermes"
venv_script.parent.mkdir(parents=True)
venv_script.write_text("#!/usr/bin/env bash\nexec true\n", encoding="utf-8")
venv_script.chmod(0o755)
known_wrapper = tmp_path / "path-home" / ".local" / "bin" / "hermes"
known_wrapper.parent.mkdir(parents=True)
known_wrapper.write_text(
f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
encoding="utf-8",
)
known_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(tmp_path / "path-home"))
# The hand-off's resolver chain: argv[0] = venv console script; with
# argv[0] hidden, the PATH rerun yields the SAME script.
def fake_resolve():
return sys.argv[0] or str(venv_script)
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
_argv0_context(monkeypatch, str(venv_script))
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
assert entry is not None
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert exec_line == f"{known_wrapper} desktop"
assert str(venv_script) not in exec_line
# …and the same context a second time re-renders byte-identical content:
# the no-op guard then skips the rewrite entirely (no write, no rescan).
lde._probe_cache.clear()
entry2 = lde.install_desktop_entry(root)
assert entry2 is not None
assert entry2.read_text(encoding="utf-8") == entry.read_text(encoding="utf-8")
def test_exec_finds_known_wrapper_when_resolver_has_no_candidate(
tmp_path, xdg_home, monkeypatch
):
"""`None` from the resolver must still probe known wrapper locations.
A cold relaunch (argv[0] is not an executable file, e.g. `-c` under
`python -m`, and PATH has no `hermes`) makes resolve_hermes_bin return
None outright. The early `return primary` that used to fire here skipped
the durable-wrapper probe, so the persisted Exec flipped to the bare
`<python> -m hermes_cli.main desktop` module form. Each flip between the
wrapper and module forms rewrites the entry on the next launch; any
rewrite that lands while gnome-shell's ShellApp for the entry is still
STARTING crashes the shell (shell_app_dispose `state == STOPPED`
assertion, gnome-shell 50.4). The entry must converge on the durable
wrapper wherever it exists.
"""
root = _make_project(tmp_path)
known_wrapper = tmp_path / "cold-home" / ".local" / "bin" / "hermes"
known_wrapper.parent.mkdir(parents=True)
known_wrapper.write_text(
f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
encoding="utf-8",
)
known_wrapper.chmod(0o755)
monkeypatch.setenv("HOME", str(tmp_path / "cold-home"))
# argv[0] is not an executable path at all — the resolver's own chain
# yields None with or without argv[0].
_argv0_context(monkeypatch, "-c")
monkeypatch.setattr("shutil.which", lambda name: None)
monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
entry = lde.install_desktop_entry(root)
assert entry is not None
exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
assert exec_line == f"{known_wrapper} desktop"
# …and the SAME context a second time re-renders byte-identical content:
# the no-op guard in install_desktop_entry then skips the rewrite.
lde._probe_cache.clear()
entry2 = lde.install_desktop_entry(root)
assert entry2 is not None
assert entry2.read_text(encoding="utf-8") == entry.read_text(encoding="utf-8")
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"#!/usr/bin/env 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.
"""
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_installed_entry_carries_the_window_app_id(tmp_path, xdg_home, monkeypatch):
"""The window's app_id is what GNOME matches the entry against — not the old "Hermes"."""
_stub_install(tmp_path, monkeypatch)
root = _make_project(tmp_path)
entry = lde.install_desktop_entry(root)
assert entry is not None
assert entry.name == f"{lde.APP_ID}.desktop"
values = _parse(entry.read_text(encoding="utf-8"))
assert values["StartupWMClass"] == lde.APP_ID
assert values["Name"] == "Hermes" # the menu label is not part of the identity
def test_install_retires_the_legacy_entry_name(tmp_path, xdg_home, monkeypatch):
"""A leftover hermes.desktop would surface as a second Hermes in the app grid."""
_stub_install(tmp_path, monkeypatch)
root = _make_project(tmp_path)
legacy = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
legacy.parent.mkdir(parents=True)
legacy.write_text(
"[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n",
encoding="utf-8",
)
entry = lde.install_desktop_entry(root)
assert entry is not None and entry.is_file()
assert not legacy.exists()
def test_install_keeps_foreign_files_at_the_legacy_path(tmp_path, xdg_home, monkeypatch):
"""Only our own entry retires; another app's file at that name is not ours to delete."""
_stub_install(tmp_path, monkeypatch)
root = _make_project(tmp_path)
foreign = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
foreign.parent.mkdir(parents=True)
foreign.write_text(
"[Desktop Entry]\nType=Application\nName=Someone else\nExec=other-app\n",
encoding="utf-8",
)
lde.install_desktop_entry(root)
assert foreign.is_file()
assert "Name=Someone else" in foreign.read_text(encoding="utf-8")
def test_install_opt_out_preserves_the_legacy_entry(tmp_path, xdg_home, monkeypatch):
"""The opt-out protects user edits, so it also stops the legacy retirement.
The missing-entry path still creates the app-id entry; the deletion is
management too and must not run when the user asked to be left alone.
"""
hermes_home = tmp_path / "hermes-home"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
(hermes_home / "config.yaml").write_text(
"desktop:\n manage_launcher_entry: false\n", encoding="utf-8"
)
_stub_install(tmp_path, monkeypatch)
root = _make_project(tmp_path)
legacy = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
legacy.parent.mkdir(parents=True)
legacy.write_text(
"[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n",
encoding="utf-8",
)
entry = lde.install_desktop_entry(root)
assert entry == xdg_home / "applications" / lde.DESKTOP_ENTRY_NAME
assert legacy.is_file(), "the opt-out must keep the legacy entry in place"
def test_app_id_matches_the_desktop_build_identity():
"""APP_ID mirrors apps/desktop/product-identity.cjs; the two must not drift apart."""
import shutil
import subprocess
node = shutil.which("node")
if node is None:
pytest.skip("node is not available")
repo = Path(__file__).resolve().parents[2]
probe = "console.log(require('./apps/desktop/product-identity.cjs').appId)"
result = subprocess.run(
[node, "-e", probe], cwd=repo, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
pytest.skip(f"product-identity.cjs did not evaluate: {result.stderr.strip()[:200]}")
assert result.stdout.strip() == lde.APP_ID
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.platforms("macos")
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.platforms("windows")
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_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
@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_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"#!/usr/bin/env 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"#!/usr/bin/env 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.
"""
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"#!/usr/bin/env 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 256x256/ like
any other unknown-size raster."""
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" / "256x256" / "apps" / "hermes.png"
assert dest.is_file()
def test_hicolor_subdir_puts_rasters_in_indexed_dirs_never_scalable():
"""Panel lookup uses fixed sizes. scalable/ is SVG-only."""
assert lde._hicolor_subdir(None) == "256x256"
assert lde._hicolor_subdir((1024, 1024)) == "256x256"
assert lde._hicolor_subdir((512, 512)) == "512x512"
assert lde._hicolor_subdir((256, 256)) == "256x256"
assert lde._hicolor_subdir((48, 48)) == "48x48"
assert lde._hicolor_subdir((24, 24)) == "24x24"
assert lde._hicolor_subdir((64, 32)) == "256x256"
def test_install_places_1024_png_in_256x256_not_scalable(
tmp_path, xdg_home, monkeypatch
):
"""The shipped desktop asset is 1024×1024. A PNG in scalable/ is what
Cinnamon's panel rasterizes as a mangled low-res icon."""
root = _make_project(tmp_path)
lde.icon_path(root).write_bytes(_png_ihdr(1024, 1024))
_stub_install(tmp_path, monkeypatch)
entry = lde.install_desktop_entry(root)
values = _parse(entry.read_text(encoding="utf-8"))
dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
assert values["Icon"] == "hermes"
assert dest.is_file()
assert dest.read_bytes() == lde.icon_path(root).read_bytes()
assert not stale.exists()
def test_install_removes_stale_scalable_png(tmp_path, xdg_home, monkeypatch):
"""v2026.8.31 wrote the PNG into scalable/. A later hermes desktop
must delete that leftover so Cinnamon does not keep using it."""
root = _make_project(tmp_path)
lde.icon_path(root).write_bytes(_png_ihdr(1024, 1024))
_stub_install(tmp_path, monkeypatch)
stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
stale.parent.mkdir(parents=True)
stale.write_bytes(b"old scalable png")
lde.install_desktop_entry(root)
dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
assert dest.is_file()
assert not stale.exists()
def test_install_exact_48_png_uses_48x48_dir(tmp_path, xdg_home, monkeypatch):
root = _make_project(tmp_path)
lde.icon_path(root).write_bytes(_png_ihdr(48, 48))
_stub_install(tmp_path, monkeypatch)
lde.install_desktop_entry(root)
dest = xdg_home / "icons" / "hicolor" / "48x48" / "apps" / "hermes.png"
assert dest.is_file()
assert not (
xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
).exists()
def test_install_resizes_decodable_png_to_panel_sizes(
tmp_path, xdg_home, monkeypatch
):
"""A decodeable PNG is Lanczos-resized so the 24px slot is actually 24px."""
from PIL import Image
root = _make_project(tmp_path)
im = Image.new("RGBA", (64, 64), (255, 255, 255, 255))
for x in range(16, 48):
for y in range(16, 48):
im.putpixel((x, y), (0, 0, 0, 255))
buf = io.BytesIO()
im.save(buf, format="PNG")
lde.icon_path(root).write_bytes(buf.getvalue())
_stub_install(tmp_path, monkeypatch)
lde.install_desktop_entry(root)
dest_24 = xdg_home / "icons" / "hicolor" / "24x24" / "apps" / "hermes.png"
dest_256 = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
assert dest_24.is_file()
assert dest_256.is_file()
assert not stale.exists()
assert struct.unpack(">II", dest_24.read_bytes()[16:24]) == (24, 24)
assert struct.unpack(">II", dest_256.read_bytes()[16:24]) == (256, 256)
def test_deferred_install_skips_heal_after_exit_without_reveal():
"""Electron exiting without ever revealing a window (boot crash, --version, early quit) must
NOT heal the entry: gnome-shell keeps the ShellApp in STARTING until the startup-notification
sequence completes or times out, not until the process dies, so a write right after the exit
is exactly the #111906 arming condition. The next terminal/updater or revealed launch heals."""
calls: list[Path] = []
deferred = lde.DeferredDesktopEntryInstall(
Path("/proj"), install=lambda root: calls.append(root) or Path("/entry"), settle_seconds=0
)
deferred.start()
time.sleep(0.05)
assert calls == [] # nothing is written while the app may still be STARTING
deferred.finish()
assert calls == []
assert not deferred._thread.is_alive()