Files
hqt/tests/test_tmux_smoke.py
felixm d78d8ee149 test: add isolated real-tmux smoke tests
Drive TmuxRunner against a throwaway tmux server (isolated via TMUX_TMPDIR)
so the tests never touch the user's live tmux. Register the 'tmux' pytest marker.
Skips when no tmux binary is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:56:58 -04:00

116 lines
3.9 KiB
Python

"""Real-tmux smoke tests.
These drive the actual TmuxRunner against a throwaway tmux server whose socket
lives in a temp TMUX_TMPDIR, so they never touch the user's live tmux sessions.
They skip when no tmux binary is available.
"""
import asyncio
import os
import shutil
import subprocess
import pytest
from hqt.tmux.runner import TmuxRunner
pytestmark = [
pytest.mark.tmux,
pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux not installed"),
]
SESSION = "hqt-test"
def _tmux(env, *args):
"""Run a raw tmux command against the isolated server, return the result."""
return subprocess.run(
["tmux", *args],
env=env,
capture_output=True,
text=True,
check=False,
)
@pytest.fixture
def tmux_env(tmp_path, monkeypatch):
# Isolate tmux completely from the user's live server:
# - TMUX_TMPDIR points the default socket at a throwaway temp dir.
# - TMUX / TMUX_PANE are unset. When TMUX is set (i.e. the test runner is
# itself inside a tmux session), tmux talks to THAT server and ignores
# TMUX_TMPDIR, so new-session/kill-server would hit the user's live
# server and evict them. delenv removes them from os.environ so both the
# raw `_tmux` calls AND the TmuxRunner subprocesses (which inherit
# os.environ) stay on the isolated server.
monkeypatch.setenv("TMUX_TMPDIR", str(tmp_path))
monkeypatch.delenv("TMUX", raising=False)
monkeypatch.delenv("TMUX_PANE", raising=False)
env = {**os.environ, "TMUX_TMPDIR": str(tmp_path)}
env.pop("TMUX", None)
env.pop("TMUX_PANE", None)
_tmux(env, "new-session", "-d", "-s", SESSION, "-x", "200", "-y", "50")
# Tripwire: a freshly isolated server has exactly one session. If isolation
# silently failed we'd see the user's sessions here — fail loudly BEFORE any
# test does something destructive. (kill-server in teardown still targets
# only the isolated socket, because env has TMUX removed + TMUX_TMPDIR set.)
listed = _tmux(env, "list-sessions", "-F", "#{session_name}").stdout.split()
assert listed == [SESSION], f"tmux isolation failed; saw sessions: {listed}"
try:
yield env
finally:
_tmux(env, "kill-server")
@pytest.fixture
def runner(tmux_env):
return TmuxRunner(tmux_path="tmux", session_name=SESSION)
async def _wait_pane_dead(runner, window, timeout=2.0):
deadline = asyncio.get_running_loop().time() + timeout
while asyncio.get_running_loop().time() < deadline:
if await runner.is_pane_dead(window):
return True
await asyncio.sleep(0.05)
return await runner.is_pane_dead(window)
@pytest.mark.asyncio
async def test_new_window_appears_in_list(runner, tmp_path):
window_id = await runner.new_window("hqt-1", str(tmp_path), "sleep 60")
assert window_id is not None
assert "hqt-1" in await runner.list_windows()
@pytest.mark.asyncio
async def test_set_window_label_round_trips(runner, tmp_path, tmux_env):
await runner.new_window("hqt-1", str(tmp_path), "sleep 60")
await runner.set_window_label("hqt-1", "•hqt-1")
shown = _tmux(
tmux_env,
"show-options",
"-w",
"-t",
f"{SESSION}:=hqt-1",
"@hqt_label",
).stdout
assert "•hqt-1" in shown
@pytest.mark.asyncio
async def test_respawn_revives_dead_pane(runner, tmp_path):
# Window whose command exits immediately -> pane dies (remain-on-exit keeps it).
await runner.new_window("hqt-1", str(tmp_path), "true")
assert await _wait_pane_dead(runner, "hqt-1") is True
assert await runner.respawn_pane("hqt-1", "sleep 60", str(tmp_path)) is True
alive, _ = await runner.verify_window_alive("hqt-1", timeout=1.0)
assert alive is True
@pytest.mark.asyncio
async def test_apply_theme_sets_session_options(runner, tmux_env):
await runner.apply_theme()
shown = _tmux(tmux_env, "show-options", "-t", SESSION, "status-justify").stdout
assert "left" in shown