Files
hermes-agent/tests/hermes_cli/test_desktop_socket_tmpdir.py
teknium1 8860e1d78b fix(desktop): only Electron's singleton socket gets the short TMPDIR
The launcher replaced TMPDIR with /tmp for the whole Electron process tree, so
the Python backend inherited TMPDIR=/tmp while TMP/TEMP/HERMES_SCRATCH_DIR
still named the scratch dir; apply_scratch_tmp_env then treated /tmp as
user-set and the agent subtree wrote temp files to /tmp.

The launcher now passes the original in HERMES_DESKTOP_TMPDIR and Electron main
restores it right after requestSingleInstanceLock(), so every child gets the
profile scratch dir back.

The override threshold is Chromium's real budget instead of the shared 50-byte
socket constant: sun_path holds 107 bytes and Chromium appends
/scoped_dirXXXXXX/SingletonSocket (33 bytes, measured on Electron 40), so a
TMPDIR up to 74 bytes is kept.
2026-09-27 11:44:31 -07:00

64 lines
2.1 KiB
Python

"""The Electron child needs a socket-safe temp root, not a long CLI scratch path (#124688)."""
import argparse
import os
import secrets
import shutil
import socket
import tempfile
from pathlib import Path
import pytest
from hermes_cli import main_desktop
def _launch_env(tmp_path) -> dict:
env, _flags = main_desktop._desktop_launch_env(argparse.Namespace(cwd=str(tmp_path)))
return env
def _bind_singleton_socket(tmpdir: str) -> None:
# Chromium's ProcessSingleton binds $TMPDIR/scoped_dirXXXXXX/SingletonSocket (6 random chars).
directory = Path(tmpdir) / f"scoped_dir{secrets.token_hex(3)}"
directory.mkdir()
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
server.bind(str(directory / "SingletonSocket"))
finally:
shutil.rmtree(directory)
@pytest.mark.platforms("linux")
def test_long_scratch_tmpdir_hosts_the_singleton_socket_and_is_kept_for_children(monkeypatch, tmp_path):
scratch = tmp_path / ("long-home-" * 8) / "cache" / "scratch"
scratch.mkdir(parents=True)
for key in ("TMPDIR", "TMP", "TEMP", "HERMES_SCRATCH_DIR"):
monkeypatch.setenv(key, str(scratch))
monkeypatch.setattr(tempfile, "tempdir", None)
env = _launch_env(tmp_path)
_bind_singleton_socket(env["TMPDIR"])
# Electron main restores this for the backend, so the agent keeps writing to scratch.
assert env["HERMES_DESKTOP_TMPDIR"] == str(scratch)
@pytest.mark.platforms("linux")
def test_tmpdir_that_fits_the_socket_budget_is_passed_through(monkeypatch, tmp_path):
from hermes_constants import socket_safe_tmpdir
with tempfile.TemporaryDirectory(prefix="dt-", dir=socket_safe_tmpdir()) as root:
# Longest TMPDIR whose scoped_dirXXXXXX/SingletonSocket still fits sun_path.
tmpdir = os.path.join(root, "x" * (74 - len(root) - 1))
os.mkdir(tmpdir)
assert len(os.fsencode(tmpdir)) == 74
_bind_singleton_socket(tmpdir)
monkeypatch.setenv("TMPDIR", tmpdir)
monkeypatch.setattr(tempfile, "tempdir", None)
env = _launch_env(tmp_path)
assert env["TMPDIR"] == tmpdir
assert "HERMES_DESKTOP_TMPDIR" not in env