Files
hermes-agent/tests/hermes_cli/test_profile_export_special_files.py
ethernet 59624b7ace tests: replace every bare host skipif with platforms()
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.
2026-09-21 19:18:15 -04:00

90 lines
3.4 KiB
Python

"""Tests for special-file handling during profile export.
:func:`shutil.copytree` cannot copy Unix sockets, FIFOs, or device nodes —
it collects "[Errno 6] No such device or address" and raises
``shutil.Error`` at the end of the walk. Profiles accumulate such files in
normal operation (e.g. a stale agent-browser control socket under
``home/.agent-browser/``), so exports must skip them instead of failing
the whole archive.
"""
import os
import socket
import sys
import tarfile
import pytest
from hermes_cli.profiles import export_profile
pytestmark = pytest.mark.platforms("posix") # Unix sockets and FIFOs are not available on Windows
def _patch_named_profile(monkeypatch, profiles_root, profile_dir):
monkeypatch.setattr("hermes_cli.profiles._get_profiles_root", lambda: profiles_root)
monkeypatch.setattr("hermes_cli.profiles.get_profile_dir", lambda n: profile_dir)
monkeypatch.setattr("hermes_cli.profiles.validate_profile_name", lambda n: None)
def _bind_unix_socket(monkeypatch, path):
"""Bind an AF_UNIX socket at *path*.
Binds via a cwd-relative name so pytest's long tmp paths stay under the
~104-byte ``sun_path`` limit on macOS. The filesystem entry outlives the
close — exactly the stale-socket state a real profile ends up with.
"""
monkeypatch.chdir(path.parent)
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.bind(path.name)
def test_named_profile_export_survives_unix_socket(tmp_path, monkeypatch):
"""Sockets and FIFOs in a named profile are skipped, not fatal."""
profiles_root = tmp_path / "profiles"
profile_dir = profiles_root / "sockety"
browser_dir = profile_dir / "home" / ".agent-browser"
browser_dir.mkdir(parents=True)
(profile_dir / "config.yaml").write_text("model: gpt-4\n")
(browser_dir / "state.json").write_text("{}\n")
# One socket the *.sock suffix rule catches, one only lstat can catch,
# and a FIFO — none of them may fail or enter the export.
_bind_unix_socket(monkeypatch, browser_dir / "dev-12345.sock")
_bind_unix_socket(monkeypatch, browser_dir / "control")
os.mkfifo(browser_dir / "events")
_patch_named_profile(monkeypatch, profiles_root, profile_dir)
result = export_profile("sockety", str(tmp_path / "sockety.tar.gz"))
with tarfile.open(result, "r:gz") as tf:
names = tf.getnames()
assert any("config.yaml" in n for n in names)
assert any("state.json" in n for n in names)
assert not any(".sock" in n for n in names)
assert not any(n.endswith(("control", "events")) for n in names)
def test_default_profile_export_survives_unix_socket(tmp_path, monkeypatch):
"""The default-profile export skips suffixless sockets in allowed dirs."""
profile_dir = tmp_path / "hermes_home"
sessions_dir = profile_dir / "sessions"
sessions_dir.mkdir(parents=True)
(profile_dir / "config.yaml").write_text("model: gpt-4\n")
(sessions_dir / "log.jsonl").write_text("{}\n")
_bind_unix_socket(monkeypatch, sessions_dir / "ipc")
_patch_named_profile(monkeypatch, tmp_path / "profiles", profile_dir)
result = export_profile("default", str(tmp_path / "default.tar.gz"))
with tarfile.open(result, "r:gz") as tf:
names = tf.getnames()
assert any("config.yaml" in n for n in names)
assert any("log.jsonl" in n for n in names)
assert not any(n.endswith("ipc") for n in names)