The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.
Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.
Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
skip-on-Windows moves onto the 11 methods that had no host mark; the
11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
platforms("linux") in favour of the module's "posix" (mkfifo exists on
macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
or wider marks removed.
134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
"""Tests for ``remove_path_from_shell_configs`` — the uninstaller's shell-rc rewrite.
|
|
|
|
This rewrites files Hermes does not own (``~/.bashrc``, ``~/.zshrc``, ...) and
|
|
takes no backup of them, so the rewrite has to be atomic: a bare
|
|
``write_text()`` truncates the rc file before the new content lands, and the
|
|
caller wraps everything in ``except Exception: log_warn(...)``, so a partial
|
|
write is downgraded to a warning and the user's next login starts a bare shell.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import stat
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import uninstall
|
|
|
|
|
|
ZSHRC = (
|
|
"export EDITOR=vim\n"
|
|
"alias ll='ls -la'\n"
|
|
"\n"
|
|
"# Hermes Agent\n"
|
|
'export PATH="$HOME/.local/bin:$PATH"\n'
|
|
"\n"
|
|
"source ~/.work-profile\n"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Point both ``Path.home()`` and ``HERMES_HOME`` at a throwaway dir."""
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
monkeypatch.setattr(Path, "home", classmethod(lambda cls: home))
|
|
monkeypatch.setenv("HERMES_HOME", str(home / ".hermes"))
|
|
return home
|
|
|
|
|
|
class TestHappyPath:
|
|
def test_hermes_path_block_is_removed(self, fake_home: Path):
|
|
rc = fake_home / ".zshrc"
|
|
rc.write_text(ZSHRC, encoding="utf-8")
|
|
|
|
removed = uninstall.remove_path_from_shell_configs()
|
|
|
|
assert removed == [rc]
|
|
text = rc.read_text(encoding="utf-8")
|
|
assert "# Hermes Agent" not in text
|
|
# The user's own lines are untouched.
|
|
assert "export EDITOR=vim" in text
|
|
assert "source ~/.work-profile" in text
|
|
|
|
def test_untouched_rc_is_not_reported(self, fake_home: Path):
|
|
rc = fake_home / ".zshrc"
|
|
rc.write_text("export EDITOR=vim\n", encoding="utf-8")
|
|
|
|
assert uninstall.remove_path_from_shell_configs() == []
|
|
assert rc.read_text(encoding="utf-8") == "export EDITOR=vim\n"
|
|
|
|
def test_all_shell_rc_candidates_are_swept(self, fake_home: Path):
|
|
"""Every rc name the shell-config resolver knows gets the PATH sweep.
|
|
|
|
Guards the single resolver's coverage: an rc the resolver misses keeps
|
|
a stale Hermes PATH entry after uninstall.
|
|
"""
|
|
names = (".bashrc", ".bash_profile", ".profile", ".zshrc", ".zprofile")
|
|
for name in names:
|
|
(fake_home / name).write_text(ZSHRC, encoding="utf-8")
|
|
|
|
removed = uninstall.remove_path_from_shell_configs()
|
|
|
|
assert sorted(p.name for p in removed) == sorted(names)
|
|
|
|
|
|
class TestCrashDurability:
|
|
def test_shell_config_survives_an_interrupted_rewrite(self, fake_home: Path):
|
|
"""An interrupted rewrite must leave the rc file byte-identical.
|
|
|
|
There is no backup of the user's shell rc anywhere in this code path,
|
|
so a truncated write is unrecoverable.
|
|
"""
|
|
rc = fake_home / ".zshrc"
|
|
rc.write_text(ZSHRC, encoding="utf-8")
|
|
original = rc.read_bytes()
|
|
|
|
def boom(fd):
|
|
raise OSError("simulated crash mid-write")
|
|
|
|
# Scoped context so restoring os.fsync doesn't also undo the
|
|
# Path.home()/HERMES_HOME patches the fake_home fixture installed.
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.setattr(os, "fsync", boom)
|
|
removed = uninstall.remove_path_from_shell_configs()
|
|
|
|
# The write failed, so the rc must not be reported as modified...
|
|
assert removed == []
|
|
# ...and it must still be exactly what the user had.
|
|
assert rc.read_bytes() == original
|
|
# The aborted write must not leave a temp file behind in $HOME.
|
|
assert list(fake_home.glob("*.tmp")) == []
|
|
|
|
@pytest.mark.require_symlinks
|
|
def test_symlinked_shell_config_stays_a_symlink(self, fake_home: Path):
|
|
"""A dotfiles-repo ``~/.zshrc`` is a symlink; replacing it with a
|
|
regular file silently detaches the user's dotfiles."""
|
|
dotfiles = fake_home / "dotfiles"
|
|
dotfiles.mkdir()
|
|
real = dotfiles / "zshrc"
|
|
real.write_text(ZSHRC, encoding="utf-8")
|
|
rc = fake_home / ".zshrc"
|
|
rc.symlink_to(real)
|
|
|
|
removed = uninstall.remove_path_from_shell_configs()
|
|
|
|
assert removed == [rc]
|
|
assert rc.is_symlink(), "the symlink was replaced by a regular file"
|
|
assert "# Hermes Agent" not in real.read_text(encoding="utf-8")
|
|
assert "export EDITOR=vim" in real.read_text(encoding="utf-8")
|
|
|
|
@pytest.mark.platforms("posix") # POSIX permission bits
|
|
def test_existing_file_mode_is_preserved(self, fake_home: Path):
|
|
"""Shell rc files are normally 0644; uninstalling must not change that."""
|
|
rc = fake_home / ".zshrc"
|
|
rc.write_text(ZSHRC, encoding="utf-8")
|
|
os.chmod(rc, 0o644)
|
|
|
|
uninstall.remove_path_from_shell_configs()
|
|
|
|
mode = stat.S_IMODE(rc.stat().st_mode)
|
|
assert mode == 0o644, f"mode changed to {oct(mode)}"
|