"""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