From 67aac4d863db4e1b5a462672d4cb6bdf7a3196ef Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:55:19 -0700 Subject: [PATCH] fix(gateway): control-socket fallback survives deep TMPDIR; tests bind-location-aware CI runners put pytest tmp roots past sun_path, which routed every test home through the fallback: two tests assumed in-home binding. Tests now assert against the resolved bind location, and the fallback itself prefers /tmp when tempfile.gettempdir() is too deep to fit sun_path. --- gateway/control_socket.py | 17 +++++++++++++++-- tests/gateway/test_control_socket.py | 28 +++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/gateway/control_socket.py b/gateway/control_socket.py index 9f07faa4d3..3962de58a5 100644 --- a/gateway/control_socket.py +++ b/gateway/control_socket.py @@ -96,8 +96,21 @@ def _default_socket_path(home: Path) -> Path: def _fallback_socket_path(home: Path) -> Path: - """Short temp-dir path for homes whose direct socket path exceeds sun_path.""" - return Path(tempfile.gettempdir()) / f"hermes-gw-{_home_hash(home)}.sock" + """Short temp-dir path for homes whose direct socket path exceeds sun_path. + + Prefers ``tempfile.gettempdir()``; when even that yields a too-long path + (deep $TMPDIR), falls back to ``/tmp`` on POSIX. If nothing fits, the + tempdir candidate is returned anyway — bind will fail non-fatally and + consumers use the scan layer. + """ + name = f"hermes-gw-{_home_hash(home)}.sock" + candidates = [Path(tempfile.gettempdir()) / name] + if not _IS_WINDOWS: + candidates.append(Path("/tmp") / name) + for candidate in candidates: + if len(str(candidate).encode("utf-8")) <= _MAX_UNIX_PATH: + return candidate + return candidates[0] def resolve_server_socket_path(home: Path) -> tuple[Path, Optional[Path]]: diff --git a/tests/gateway/test_control_socket.py b/tests/gateway/test_control_socket.py index b1cbe156db..2e8a7bb1df 100644 --- a/tests/gateway/test_control_socket.py +++ b/tests/gateway/test_control_socket.py @@ -44,10 +44,24 @@ def _serve(home: Path, handlers=None): # Path resolution # --------------------------------------------------------------------------- -def test_short_home_binds_in_home(home: Path): - bind, pointer = resolve_server_socket_path(home) - assert bind == home / "gateway.sock" - assert pointer is None +def test_short_home_binds_in_home(tmp_path: Path): + # A home short enough for sun_path binds in-home with no pointer. + # tmp_path can exceed the limit on CI runners, so build one in the + # system temp root directly. + import tempfile + + short_root = Path(tempfile.mkdtemp(prefix="hgw-", dir="/tmp")) + try: + short_home = short_root / ".hermes" + short_home.mkdir() + assert len(str(short_home / "gateway.sock").encode()) <= 100 + bind, pointer = resolve_server_socket_path(short_home) + assert bind == short_home / "gateway.sock" + assert pointer is None + finally: + import shutil + + shutil.rmtree(short_root, ignore_errors=True) def test_long_home_uses_pointer_fallback(tmp_path: Path): @@ -163,7 +177,11 @@ def test_stop_removes_socket_and_pointer(home: Path): def test_stale_socket_file_is_replaced_on_bind(home: Path): - (home / "gateway.sock").touch() # crashed predecessor's leftover + # Plant the stale file at wherever the server will actually bind + # (in-home OR the temp-dir fallback, depending on path length). + bind, _ = resolve_server_socket_path(home) + bind.parent.mkdir(parents=True, exist_ok=True) + bind.touch() # crashed predecessor's leftover async def scenario(): server = GatewayControlServer(