From 300140e006bd1e356db69772b5ba35914b9d4008 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 28 May 2026 18:16:54 -0700 Subject: [PATCH] test(tui_gateway): stop reloading server module in fixture teardown (#34217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tui_gateway.server registers two atexit hooks at module load time: ThreadPoolExecutor shutdown (line 170) and _shutdown_sessions (line 336). Three test files reloaded the module on each fixture teardown to reset per-test state. Each reload re-runs module-level code, including the atexit registrations — duplicates accumulate across the test session. At pytest interpreter shutdown the duplicated atexit hooks race the stderr buffer flush: Fatal Python error: _enter_buffered_busy: could not acquire lock for <_io.BufferedWriter name=''> at interpreter shutdown, possibly due to daemon threads pytest reports 'tests passed but the slice exited non-zero', and the shard turns red on CI. Surfaced today on PR #34193's test slice 1 (204 files, 3572 tests passed, then Fatal Python error during exit). Fix: drop importlib.reload(mod) from the three fixtures that have it. Per-test reset is handled by clearing the mutable session dicts (_sessions, _pending, _answers). _methods is also no longer cleared — it's populated at module import time and would only be re-populated by a reload, so clearing it without reload broke session.resume / command.dispatch / slash.exec method registration across tests. Affected fixtures: - tests/tui_gateway/test_goal_command.py - tests/tui_gateway/test_protocol.py - tests/tui_gateway/test_review_summary_callback.py The second reload in test_protocol.py at line 211 (reload of tui_gateway.transport) is preserved — transport.py has no atexit hooks or threads, so reload is safe there. Tests: 84/84 in tests/tui_gateway/ pass cleanly with exit code 0; no Fatal Python error at interpreter shutdown. --- tests/tui_gateway/test_goal_command.py | 10 ++++++++-- tests/tui_gateway/test_protocol.py | 10 ++++++++-- tests/tui_gateway/test_review_summary_callback.py | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/tui_gateway/test_goal_command.py b/tests/tui_gateway/test_goal_command.py index 050b36bc877..d06f5b8fbbd 100644 --- a/tests/tui_gateway/test_goal_command.py +++ b/tests/tui_gateway/test_goal_command.py @@ -44,11 +44,17 @@ def server(hermes_home): ): mod = importlib.import_module("tui_gateway.server") yield mod + # Reset module-level session state without re-importing. importlib.reload + # would re-register the module's atexit hooks (ThreadPoolExecutor + # shutdown, _shutdown_sessions); the duplicates race the stderr + # buffer at interpreter shutdown and surface as Fatal Python error: + # _enter_buffered_busy. Clearing the per-session dicts gives the + # next test a clean slate; _methods is NOT cleared because it's + # populated at module import time and re-registration only happens + # via reload (which we don't do). mod._sessions.clear() mod._pending.clear() mod._answers.clear() - mod._methods.clear() - importlib.reload(mod) @pytest.fixture() diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index a26a360a24d..f2f68efbf55 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -30,11 +30,17 @@ def server(): import importlib mod = importlib.import_module("tui_gateway.server") yield mod + # Reset module-level session state without re-importing. importlib.reload + # would re-register the module's atexit hooks (ThreadPoolExecutor + # shutdown, _shutdown_sessions); the duplicates race the stderr + # buffer at interpreter shutdown and surface as Fatal Python error: + # _enter_buffered_busy. Clearing the per-session dicts gives the + # next test a clean slate; _methods is NOT cleared because it's + # populated at module import time and re-registration only happens + # via reload (which we don't do). mod._sessions.clear() mod._pending.clear() mod._answers.clear() - mod._methods.clear() - importlib.reload(mod) @pytest.fixture() diff --git a/tests/tui_gateway/test_review_summary_callback.py b/tests/tui_gateway/test_review_summary_callback.py index 9fc7f54ddc6..f2d97f1a487 100644 --- a/tests/tui_gateway/test_review_summary_callback.py +++ b/tests/tui_gateway/test_review_summary_callback.py @@ -34,11 +34,17 @@ def server(): mod = importlib.import_module("tui_gateway.server") yield mod + # Reset module-level session state without re-importing. importlib.reload + # would re-register the module's atexit hooks (ThreadPoolExecutor + # shutdown, _shutdown_sessions); the duplicates race the stderr + # buffer at interpreter shutdown and surface as Fatal Python error: + # _enter_buffered_busy. Clearing the per-session dicts gives the + # next test a clean slate; _methods is NOT cleared because it's + # populated at module import time and re-registration only happens + # via reload (which we don't do). mod._sessions.clear() mod._pending.clear() mod._answers.clear() - mod._methods.clear() - importlib.reload(mod) def test_init_session_attaches_background_review_callback(server, monkeypatch):