93 lines
3.4 KiB
Python
93 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.skipif(
|
|
sys.platform == "win32",
|
|
reason="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)
|