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>
This commit is contained in:
@@ -27,6 +27,11 @@ dev = [
|
||||
"ty>=0.0.47",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"tmux: real-tmux smoke tests; require a tmux binary and run on an isolated TMUX_TMPDIR server",
|
||||
]
|
||||
|
||||
[tool.hqt.quality]
|
||||
required_after_code_or_test_changes = [
|
||||
"uv run ruff format src tests",
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user