Skip to content

Commit dbe1676

Browse files
fix(tests): clean up gptme_N sessions in tmux test fixture (#926)
* fix(tests): clean up gptme_N sessions in tmux test fixture The cleanup fixture only cleaned up sessions matching gptme_{worker_id}_* but new_session() creates sessions named gptme_N (simple numeric suffix). These sessions were not cleaned up between tests, causing failures when sessions from previous tests still existed. This fix extends the cleanup to also remove gptme_N pattern sessions (where N is just digits) in addition to the worker-prefixed ones. Fixes flaky test: TestNewSession::test_increments_session_id * fix(tests): use worker-isolated session in test_lists_created_sessions The test was using new_session() which creates gptme_N format sessions. In parallel testing with pytest-xdist, other workers' cleanup could remove these sessions before the test verifies them, causing race condition failures. Now uses _create_test_session() which creates worker-prefixed sessions (gptme_gw0_1, etc.) that are isolated from other workers' cleanup. * fix(tests): add tmux session cleanup for test_cli::test_tmux Add cleanup_tmux_sessions fixture to prevent cross-test contamination when running tests in parallel. The test_cli::test_tmux test runs the actual gptme CLI which creates gptme_N sessions internally, and these can leak between test runs. Changes: - Add cleanup_tmux_sessions fixture in conftest.py that cleans up gptme_N sessions before and after tests - Apply fixture to test_tmux in test_cli.py - Add xdist_group marker to ensure tmux tests run on same worker Fixes the CI failure where test_tmux was capturing output from a different test run's tmux session. * fix(tests): add xdist_group to all tmux tests for serialization All tmux tests now run on the same worker to prevent race conditions where tests creating gptme_N sessions interfere with each other. The test_cli::test_tmux was still failing because test_tools_tmux.py tests (like test_increments_session_id) create gptme_N sessions without the xdist_group marker, allowing parallel execution that causes session name collisions. By adding xdist_group('tmux') at the module level to test_tools_tmux.py, all tmux-related tests now run serially on the same worker. * Revert "fix(tests): add xdist_group to all tmux tests for serialization" This reverts commit ffc35da. * style(tests): use regex for tmux session pattern matching Use re.compile(r'^gptme_\d+$') for cleaner session name matching instead of string splitting. Improves readability as suggested in code review. Addresses review feedback on PR #926. * fix(tests): serialize tests using new_session() to avoid race conditions Tests that call new_session() directly create gptme_N sessions that can conflict with other workers' cleanup. This commit: 1. Removes automatic cleanup of gptme_N sessions from the autouse fixture (only keeps worker-specific gptme_{worker_id}_* cleanup) 2. Creates a new cleanup_new_session_sessions fixture for tests that need gptme_N session cleanup (non-autouse, must be explicitly requested) 3. Marks TestNewSession, TestKillSession, and test_auto_prefixes_session_id with @pytest.mark.xdist_group('tmux_new_session') to ensure they run serially on the same worker, preventing race conditions This approach isolates the problematic tests without affecting the performance of other tmux tests that use worker-isolated sessions. * fix(tests): align xdist_group name for test_cli::test_tmux Changed xdist_group from 'tmux' to 'tmux_new_session' to match the group used in test_tools_tmux.py. This ensures ALL tests that create gptme_N sessions run on the same worker, preventing race conditions. * fix(tests): use worker-isolated sessions in TestKillSession The test_kills_session test was failing in CI because new_session() creates gptme_N sessions which can be killed by other workers' cleanup. Fix: Use _create_test_session() which creates worker-isolated sessions (gptme_{worker_id}_N) that aren't affected by other workers' cleanup. This removes the need for xdist_group on TestKillSession since the sessions are now worker-isolated by design.
1 parent 1ee053a commit dbe1676

File tree

3 files changed

+107
-25
lines changed

3 files changed

+107
-25
lines changed

tests/conftest.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,51 @@ def init_():
151151
init(None, interactive=False, tool_allowlist=None, tool_format="markdown")
152152

153153

154+
@pytest.fixture
155+
def cleanup_tmux_sessions():
156+
"""Clean up gptme_* tmux sessions before and after a test.
157+
158+
This prevents cross-test contamination when tests run the gptme CLI
159+
which creates gptme_N sessions internally.
160+
"""
161+
import subprocess
162+
163+
def _cleanup():
164+
"""Kill all gptme_* sessions except worker-specific ones."""
165+
import re
166+
167+
# Match simple gptme_N sessions (N = digits only)
168+
# but NOT worker-specific ones like gptme_gw0_test_*
169+
simple_session_pattern = re.compile(r"^gptme_\d+$")
170+
171+
try:
172+
result = subprocess.run(
173+
["tmux", "list-sessions", "-F", "#{session_name}"],
174+
capture_output=True,
175+
text=True,
176+
timeout=5,
177+
)
178+
if result.returncode == 0:
179+
for session in result.stdout.strip().split("\n"):
180+
session = session.strip()
181+
if not session:
182+
continue
183+
if simple_session_pattern.match(session):
184+
subprocess.run(
185+
["tmux", "kill-session", "-t", session],
186+
capture_output=True,
187+
timeout=5,
188+
)
189+
except (subprocess.TimeoutExpired, FileNotFoundError):
190+
pass # tmux not available or timed out
191+
192+
# Cleanup before test
193+
_cleanup()
194+
yield
195+
# Cleanup after test
196+
_cleanup()
197+
198+
154199
@pytest.fixture
155200
def server_thread():
156201
"""Start a server in a thread for testing."""

tests/test_cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,8 @@ def test_chain(args: list[str], runner: CliRunner):
344344
# TODO: move elsewhere
345345
@pytest.mark.slow
346346
@pytest.mark.requires_api
347-
def test_tmux(args: list[str], runner: CliRunner):
347+
@pytest.mark.xdist_group("tmux_new_session")
348+
def test_tmux(args: list[str], runner: CliRunner, cleanup_tmux_sessions):
348349
"""
349350
$ gptme '/impersonate lets find out the current load
350351
```tmux

tests/test_tools_tmux.py

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,16 @@ def worker_id():
6767

6868
@pytest.fixture(autouse=True)
6969
def cleanup_sessions(worker_id):
70-
"""Clean up any test sessions before and after each test."""
70+
"""Clean up worker-specific test sessions before and after each test.
71+
72+
Only cleans up sessions with worker prefix (gptme_{worker_id}_*) to avoid
73+
race conditions in parallel test runs. Tests that use new_session() directly
74+
should use the cleanup_new_session_sessions fixture instead.
75+
"""
7176

7277
def cleanup():
7378
for session in get_sessions():
74-
# Only clean up sessions with our worker prefix to avoid killing other workers' sessions
79+
# Clean up sessions with our worker prefix (for parallel test isolation)
7580
if session.startswith(f"gptme_{worker_id}_"):
7681
subprocess.run(
7782
["tmux", "kill-session", "-t", session],
@@ -83,6 +88,31 @@ def cleanup():
8388
cleanup()
8489

8590

91+
@pytest.fixture
92+
def cleanup_new_session_sessions():
93+
"""Clean up gptme_N sessions for tests that use new_session() directly.
94+
95+
This fixture is NOT autouse - only apply to specific tests that need it.
96+
Tests using this should also use @pytest.mark.xdist_group("tmux_new_session")
97+
to ensure they run serially and avoid race conditions with other workers.
98+
"""
99+
import re
100+
101+
def cleanup():
102+
for session in get_sessions():
103+
if session.startswith("gptme_"):
104+
# Match gptme_N where N is just digits (not gptme_gw0_*, etc.)
105+
if re.fullmatch(r"gptme_\d+", session):
106+
subprocess.run(
107+
["tmux", "kill-session", "-t", session],
108+
capture_output=True,
109+
)
110+
111+
cleanup()
112+
yield
113+
cleanup()
114+
115+
86116
class TestGetSessions:
87117
"""Tests for get_sessions function."""
88118

@@ -94,16 +124,21 @@ def test_empty_when_no_sessions(self, worker_id):
94124
assert len(worker_sessions) == 0
95125

96126

127+
@pytest.mark.xdist_group("tmux_new_session")
97128
class TestNewSession:
98-
"""Tests for new_session function."""
129+
"""Tests for new_session function.
130+
131+
These tests use new_session() directly which creates gptme_N sessions.
132+
They are grouped to run serially to avoid race conditions.
133+
"""
99134

100-
def test_creates_session(self):
135+
def test_creates_session(self, cleanup_new_session_sessions):
101136
"""Should create a new tmux session."""
102137
msg = new_session("echo 'hello world'")
103138
assert "gptme_" in msg.content
104139
assert "hello world" in msg.content or "Running" in msg.content
105140

106-
def test_increments_session_id(self):
141+
def test_increments_session_id(self, cleanup_new_session_sessions):
107142
"""Should create sessions with incrementing IDs."""
108143
msg1 = new_session("echo 'first'")
109144
msg2 = new_session("echo 'second'")
@@ -122,32 +157,29 @@ def test_increments_session_id(self):
122157
class TestListSessions:
123158
"""Tests for list_sessions function."""
124159

125-
def test_lists_created_sessions(self):
126-
"""Should list sessions that were created."""
127-
msg = new_session("echo 'test'")
128-
# Extract the session ID from the creation message
129-
import re
160+
def test_lists_created_sessions(self, worker_id):
161+
"""Should list sessions that were created.
130162
131-
match = re.search(r"gptme_(\d+)", msg.content)
132-
assert match
133-
session_id = f"gptme_{match.group(1)}"
163+
Uses worker-isolated session to avoid race conditions in parallel tests.
164+
"""
165+
# Create a worker-isolated session to avoid race conditions
166+
# where other workers' cleanup might remove our session
167+
session_id = _create_test_session("echo 'test'", worker_id)
134168

135169
msg = list_sessions()
136170
assert session_id in msg.content
137171

138172

139173
class TestKillSession:
140-
"""Tests for kill_session function."""
174+
"""Tests for kill_session function.
175+
176+
Uses worker-isolated sessions to avoid race conditions in parallel tests.
177+
"""
141178

142-
def test_kills_session(self):
179+
def test_kills_session(self, worker_id):
143180
"""Should kill an existing session."""
144-
msg = new_session("echo 'to kill'")
145-
# Extract the session ID from the creation message
146-
import re
147-
148-
match = re.search(r"gptme_(\d+)", msg.content)
149-
assert match
150-
session_id = f"gptme_{match.group(1)}"
181+
# Use worker-isolated session to avoid race conditions
182+
session_id = _create_test_session("echo 'to kill'", worker_id, 1)
151183

152184
msg = kill_session(session_id)
153185
assert "Killed" in msg.content
@@ -187,8 +219,12 @@ def test_timeout_for_ongoing_command(self, worker_id):
187219
assert "timed out" in msg.content
188220
assert elapsed >= 4 # Should have waited for timeout
189221

190-
def test_auto_prefixes_session_id(self):
191-
"""Should automatically add gptme_ prefix if missing."""
222+
@pytest.mark.xdist_group("tmux_new_session")
223+
def test_auto_prefixes_session_id(self, cleanup_new_session_sessions):
224+
"""Should automatically add gptme_ prefix if missing.
225+
226+
Uses new_session() directly, so grouped with other new_session tests.
227+
"""
192228
# This test verifies the prefix behavior of wait_for_output
193229
# We use new_session here because we need a gptme_N style session
194230
msg = new_session("echo 'prefix test'")

0 commit comments

Comments
 (0)