From be9198f1e16a46df3385c78364a73492972be389 Mon Sep 17 00:00:00 2001 From: Teknium Date: Fri, 10 Apr 2026 20:02:27 -0700 Subject: [PATCH] fix: guard mautrix imports for gateway-safe fallback + fix test isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up fixes for the matrix-nio → mautrix migration: 1. Module-level mautrix.types import now wrapped in try/except with proper stub classes. Without this, importing gateway.platforms.matrix crashes the entire gateway when mautrix isn't installed — even for users who don't use Matrix. The stubs mirror mautrix's real attribute names so tests that exercise adapter methods (send, reactions, etc.) work without the real SDK. 2. Removed _ensure_mautrix_mock() from test_matrix_mention.py — it permanently installed MagicMock modules in sys.modules via setdefault(), polluting later tests in the suite. No longer needed since the module imports cleanly without mautrix. 3. Fixed thread persistence tests to use direct class reference in monkeypatch.setattr() instead of string-based paths, which broke when the module was reimported by other tests. 4. Moved the module-importability test to a subprocess to prevent it from polluting sys.modules (reimporting creates a second module object with different __dict__, breaking patch.object in subsequent tests). --- gateway/platforms/matrix.py | 60 +++++++++++++++++++----- tests/gateway/test_matrix.py | 39 +++++++++++++++- tests/gateway/test_matrix_mention.py | 69 +++++----------------------- 3 files changed, 97 insertions(+), 71 deletions(-) diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index 4a1cd2e9e..409d2d6e4 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -35,18 +35,54 @@ from typing import Any, Dict, Optional, Set from html import escape as _html_escape -from mautrix.types import ( - ContentURI, - EventID, - EventType, - PaginationDirection, - PresenceState, - RoomCreatePreset, - RoomID, - SyncToken, - TrustState, - UserID, -) +try: + from mautrix.types import ( + ContentURI, + EventID, + EventType, + PaginationDirection, + PresenceState, + RoomCreatePreset, + RoomID, + SyncToken, + TrustState, + UserID, + ) +except ImportError: + # Stubs so the module is importable without mautrix installed. + # check_matrix_requirements() will return False and the adapter + # won't be instantiated in production, but tests may exercise + # adapter methods so stubs must have the right attributes. + ContentURI = EventID = RoomID = SyncToken = UserID = str # type: ignore[misc,assignment] + + class _EventTypeStub: # type: ignore[no-redef] + ROOM_MESSAGE = "m.room.message" + REACTION = "m.reaction" + ROOM_ENCRYPTED = "m.room.encrypted" + ROOM_NAME = "m.room.name" + EventType = _EventTypeStub # type: ignore[misc,assignment] + + class _PaginationDirectionStub: # type: ignore[no-redef] + BACKWARD = "b" + FORWARD = "f" + PaginationDirection = _PaginationDirectionStub # type: ignore[misc,assignment] + + class _PresenceStateStub: # type: ignore[no-redef] + ONLINE = "online" + OFFLINE = "offline" + UNAVAILABLE = "unavailable" + PresenceState = _PresenceStateStub # type: ignore[misc,assignment] + + class _RoomCreatePresetStub: # type: ignore[no-redef] + PRIVATE = "private_chat" + PUBLIC = "public_chat" + TRUSTED_PRIVATE = "trusted_private_chat" + RoomCreatePreset = _RoomCreatePresetStub # type: ignore[misc,assignment] + + class _TrustStateStub: # type: ignore[no-redef] + UNVERIFIED = 0 + VERIFIED = 1 + TrustState = _TrustStateStub # type: ignore[misc,assignment] from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( diff --git a/tests/gateway/test_matrix.py b/tests/gateway/test_matrix.py index 5c79e476b..469bae030 100644 --- a/tests/gateway/test_matrix.py +++ b/tests/gateway/test_matrix.py @@ -601,6 +601,40 @@ class TestMatrixDisplayName: # Requirements check # --------------------------------------------------------------------------- +class TestMatrixModuleImport: + def test_module_importable_without_mautrix(self): + """gateway.platforms.matrix must be importable even when mautrix is + not installed — otherwise the gateway crashes for ALL platforms. + + This test uses a subprocess to avoid polluting the current process's + sys.modules (reimporting a module creates a second module object whose + classes don't share globals with the original — breaking patch.object + in subsequent tests). + """ + import subprocess + result = subprocess.run( + [sys.executable, "-c", ( + "import sys\n" + "# Block mautrix completely\n" + "class _Blocker:\n" + " def find_module(self, name, path=None):\n" + " if name.startswith('mautrix'): return self\n" + " def load_module(self, name):\n" + " raise ImportError(f'blocked: {name}')\n" + "sys.meta_path.insert(0, _Blocker())\n" + "for k in list(sys.modules):\n" + " if k.startswith('mautrix'): del sys.modules[k]\n" + "from gateway.platforms.matrix import check_matrix_requirements\n" + "assert not check_matrix_requirements()\n" + "print('OK')\n" + )], + capture_output=True, text=True, timeout=10, + ) + assert result.returncode == 0, ( + f"Subprocess failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) + + class TestMatrixRequirements: def test_check_requirements_with_token(self, monkeypatch): monkeypatch.setenv("MATRIX_ACCESS_TOKEN", "syt_test") @@ -738,7 +772,7 @@ class TestMatrixE2EEHardFail: @pytest.mark.asyncio async def test_connect_fails_when_encryption_true_but_no_e2ee_deps(self): - from gateway.platforms.matrix import MatrixAdapter + from gateway.platforms.matrix import MatrixAdapter, _check_e2ee_deps config = PlatformConfig( enabled=True, @@ -768,7 +802,8 @@ class TestMatrixE2EEHardFail: from gateway.platforms import matrix as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False): with patch.dict("sys.modules", fake_mautrix_mods): - result = await adapter.connect() + with patch.object(adapter, "_sync_loop", AsyncMock(return_value=None)): + result = await adapter.connect() assert result is False diff --git a/tests/gateway/test_matrix_mention.py b/tests/gateway/test_matrix_mention.py index c0533741a..d36c2b765 100644 --- a/tests/gateway/test_matrix_mention.py +++ b/tests/gateway/test_matrix_mention.py @@ -11,59 +11,10 @@ import pytest from gateway.config import PlatformConfig -def _ensure_mautrix_mock(): - """Install mock mautrix modules when mautrix-python isn't available.""" - if "mautrix" in sys.modules and hasattr(sys.modules["mautrix"], "__file__"): - return - - # Root module - mautrix_mod = MagicMock() - - # mautrix.types — commonly imported types - types_mod = MagicMock() - types_mod.EventType = MagicMock() - types_mod.RoomID = str - types_mod.UserID = str - types_mod.EventID = str - types_mod.ContentURI = str - types_mod.RoomCreatePreset = MagicMock() - types_mod.PresenceState = MagicMock() - types_mod.PaginationDirection = MagicMock() - types_mod.SyncToken = str - types_mod.TrustState = MagicMock() - - # mautrix.client - client_mod = MagicMock() - client_mod.Client = MagicMock() - client_mod.InternalEventType = MagicMock() - - # mautrix.client.state_store - state_store_mod = MagicMock() - state_store_mod.MemoryStateStore = MagicMock() - state_store_mod.MemorySyncStore = MagicMock() - - # mautrix.api - api_mod = MagicMock() - api_mod.HTTPAPI = MagicMock() - - # mautrix.crypto - crypto_mod = MagicMock() - crypto_mod.OlmMachine = MagicMock() - crypto_store_mod = MagicMock() - crypto_store_mod.MemoryCryptoStore = MagicMock() - crypto_attachments_mod = MagicMock() - - sys.modules.setdefault("mautrix", mautrix_mod) - sys.modules.setdefault("mautrix.types", types_mod) - sys.modules.setdefault("mautrix.client", client_mod) - sys.modules.setdefault("mautrix.client.state_store", state_store_mod) - sys.modules.setdefault("mautrix.api", api_mod) - sys.modules.setdefault("mautrix.crypto", crypto_mod) - sys.modules.setdefault("mautrix.crypto.store", crypto_store_mod) - sys.modules.setdefault("mautrix.crypto.attachments", crypto_attachments_mod) - - -_ensure_mautrix_mock() +# The matrix adapter module is importable without mautrix installed +# (module-level imports use try/except with stubs). No need for +# module-level mock installation — tests that call adapter methods +# needing real mautrix APIs mock them individually. def _make_adapter(tmp_path=None): @@ -410,8 +361,9 @@ async def test_auto_thread_tracks_participation(monkeypatch): class TestThreadPersistence: def test_empty_state_file(self, tmp_path, monkeypatch): """No state file → empty set.""" + from gateway.platforms.matrix import MatrixAdapter monkeypatch.setattr( - "gateway.platforms.matrix.MatrixAdapter._thread_state_path", + MatrixAdapter, "_thread_state_path", staticmethod(lambda: tmp_path / "matrix_threads.json"), ) adapter = _make_adapter() @@ -420,9 +372,10 @@ class TestThreadPersistence: def test_track_thread_persists(self, tmp_path, monkeypatch): """_track_thread writes to disk.""" + from gateway.platforms.matrix import MatrixAdapter state_path = tmp_path / "matrix_threads.json" monkeypatch.setattr( - "gateway.platforms.matrix.MatrixAdapter._thread_state_path", + MatrixAdapter, "_thread_state_path", staticmethod(lambda: state_path), ) adapter = _make_adapter() @@ -433,10 +386,11 @@ class TestThreadPersistence: def test_threads_survive_reload(self, tmp_path, monkeypatch): """Persisted threads are loaded by a new adapter instance.""" + from gateway.platforms.matrix import MatrixAdapter state_path = tmp_path / "matrix_threads.json" state_path.write_text(json.dumps(["$t1", "$t2"])) monkeypatch.setattr( - "gateway.platforms.matrix.MatrixAdapter._thread_state_path", + MatrixAdapter, "_thread_state_path", staticmethod(lambda: state_path), ) adapter = _make_adapter() @@ -445,9 +399,10 @@ class TestThreadPersistence: def test_cap_max_tracked_threads(self, tmp_path, monkeypatch): """Thread set is trimmed to _MAX_TRACKED_THREADS.""" + from gateway.platforms.matrix import MatrixAdapter state_path = tmp_path / "matrix_threads.json" monkeypatch.setattr( - "gateway.platforms.matrix.MatrixAdapter._thread_state_path", + MatrixAdapter, "_thread_state_path", staticmethod(lambda: state_path), ) adapter = _make_adapter()