diff --git a/CHANGES b/CHANGES index 51f05bd..a858074 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,16 @@ _Notes on upcoming releases will be added here_ +### Testing + +- Added a session-scoped autouse fixture in `tests/conftest.py` that + reaps leaked `libtmux_test*` tmux daemons and socket files under + `/tmp/tmux-/` after every test run. Works around upstream + [tmux-python/libtmux#661](https://github.com/tmux-python/libtmux/pull/661), + whose pytest plugin does not reliably kill servers or unlink socket + files during teardown. Idempotent and safe under `pytest-xdist` + (#20). + ## libtmux-mcp 0.1.0a2 (2026-04-19) _FastMCP alignment: new tools, prompts, and middleware (#15)_ diff --git a/tests/conftest.py b/tests/conftest.py index 3f7dc93..2da7c05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,19 +2,67 @@ from __future__ import annotations +import contextlib +import os +import pathlib import typing as t import pytest +from libtmux.server import Server from libtmux_mcp._utils import _server_cache if t.TYPE_CHECKING: from libtmux.pane import Pane - from libtmux.server import Server from libtmux.session import Session from libtmux.window import Window +@pytest.fixture(scope="session", autouse=True) +def _reap_leaked_libtmux_test_sockets() -> t.Generator[None, None, None]: + """Reap leaked ``libtmux_test*`` daemons and socket files post-suite. + + libtmux's pytest plugin creates per-test tmux servers on + ``libtmux_test`` sockets but does not reliably kill the daemons + or ``unlink`` the socket files on teardown — see + `tmux-python/libtmux#660 `_. + Without this finalizer ``/tmp/tmux-/`` accumulates hundreds of + stale socket entries across test runs (10k+ on long-lived dev + machines per the #20 report). + + Scope is ``session``: runs after every ``pytest`` invocation. Prefix + match on ``libtmux_test`` only — matches the literal prefix set by + libtmux's ``pytest_plugin.py`` and never touches the developer's + real ``default`` socket or any non-test socket. Safe under ``xdist``: + each worker is its own pytest session and the socket operations + (``kill_server`` / ``unlink``) are idempotent. + """ + yield + + # ``geteuid`` is Unix-only; the tmux server socket directory only + # exists on POSIX. Skip on platforms without it rather than erroring. + if not hasattr(os, "geteuid"): + return + + tmpdir = pathlib.Path(f"/tmp/tmux-{os.geteuid()}") + if not tmpdir.is_dir(): + return + + for socket_path in tmpdir.glob("libtmux_test*"): + # Defensive cleanup: if the server is still alive, kill it; then + # unlink the socket file whether or not kill succeeded (tmux + # sometimes leaves the file on disk after the daemon exits). + # Any step may fail because the socket has already vanished, + # permissions changed, or a concurrent run raced us — none of + # that is actionable here, so swallow the error and move on. + with contextlib.suppress(Exception): + server = Server(socket_name=socket_path.name) + if server.is_alive(): + server.kill() + with contextlib.suppress(OSError): + socket_path.unlink(missing_ok=True) + + @pytest.fixture(autouse=True) def _clear_server_cache() -> t.Generator[None, None, None]: """Clear the MCP server cache between tests."""