From b1535744a129ec50de6c961aa0bcb600766af800 Mon Sep 17 00:00:00 2001 From: malice Date: Fri, 17 Jul 2026 14:25:11 -0600 Subject: [PATCH] fix(tests): stop poisoning sys.modules session-wide with a bare MagicMock (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three test files (test_llm_scoping, test_fix_meshcore_save_and_llm_test, test_config_partial_save_merge) did `sys.modules.setdefault(_mod, MagicMock())` to stub an optional import. `setdefault` installs the MagicMock into sys.modules for the ENTIRE pytest session even when the real package is present — so any LATER test that does `await .(...)` (e.g. `await aiosqlite.connect(...)`) fails with "object MagicMock can't be used in 'await' expression" / "Event loop is closed". This is what made test_fire_tracker_phase4::test_natural_language_fire_ question_routes_to_llm pass in isolation but fail in full-suite order — the leak came from an earlier file, not the victim. #140 hardened the victim's own config/history isolation but couldn't fix an external sys.modules poison. Fix: only fall back to the MagicMock when the real module genuinely fails to import (guarded assignment), so a present package is never replaced. Root-cause fix in the polluters, not a skip on the victim. Suite: 2422 passed, 0 failed, 72 skipped — fully green (was 6 failed before #140, then 1 order-dependent failure after). Confirmed deterministic across repeated full runs. Co-authored-by: Matt Johnson Co-authored-by: Claude Opus 4.8 (1M context) --- work/tests/test_config_partial_save_merge.py | 17 ++++++++++++++++- .../test_fix_meshcore_save_and_llm_test.py | 17 ++++++++++++++++- work/tests/test_llm_scoping.py | 19 ++++++++++++++++--- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/work/tests/test_config_partial_save_merge.py b/work/tests/test_config_partial_save_merge.py index 650c345..0ffa4b5 100644 --- a/work/tests/test_config_partial_save_merge.py +++ b/work/tests/test_config_partial_save_merge.py @@ -32,6 +32,7 @@ omitted keys keep their live values while explicitly-sent keys still apply. """ from __future__ import annotations +import importlib import sys from unittest.mock import MagicMock @@ -41,8 +42,22 @@ from fastapi import FastAPI from fastapi.testclient import TestClient # Stub heavy optional deps so config_routes imports without them. +# +# Try the real import first -- sys.modules.setdefault() alone is only a +# no-op "in production where the real packages are installed" if some +# earlier-run test has already imported the real module. When this file +# collects first (test order is alphabetical, not guaranteed), setdefault() +# permanently replaces a genuinely-installed module (e.g. aiosqlite) with a +# bare MagicMock for the rest of the pytest session -- every later test's +# `await aiosqlite.connect(...)` then breaks with "MagicMock can't be used +# in 'await' expression". Only fall back to the mock when the real package +# truly isn't importable. for _mod in ("openai", "aiosqlite", "anthropic", "google", "google.genai"): - sys.modules.setdefault(_mod, MagicMock()) + if _mod not in sys.modules: + try: + importlib.import_module(_mod) + except ImportError: + sys.modules[_mod] = MagicMock() from meshai.config import ( # noqa: E402 Config, diff --git a/work/tests/test_fix_meshcore_save_and_llm_test.py b/work/tests/test_fix_meshcore_save_and_llm_test.py index c2c40a8..fe92bd4 100644 --- a/work/tests/test_fix_meshcore_save_and_llm_test.py +++ b/work/tests/test_fix_meshcore_save_and_llm_test.py @@ -12,6 +12,7 @@ Bug 2 — POST /api/config/test-llm "string indices must be integers, not 'str'" """ from __future__ import annotations +import importlib import sys from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch @@ -20,9 +21,23 @@ import pytest # ----------------------------------------------------------------------- # Stub heavy optional deps so config_routes can be imported without them. +# +# Try the real import first -- sys.modules.setdefault() alone is only a +# no-op "in production where the real packages are installed" if some +# earlier-run test has already imported the real module. When this file +# collects first (test order is alphabetical, not guaranteed), setdefault() +# permanently replaces a genuinely-installed module (e.g. aiosqlite) with a +# bare MagicMock for the rest of the pytest session -- every later test's +# `await aiosqlite.connect(...)` then breaks with "MagicMock can't be used +# in 'await' expression". Only fall back to the mock when the real package +# truly isn't importable. # ----------------------------------------------------------------------- for _mod in ("openai", "aiosqlite", "anthropic", "google", "google.genai"): - sys.modules.setdefault(_mod, MagicMock()) + if _mod not in sys.modules: + try: + importlib.import_module(_mod) + except ImportError: + sys.modules[_mod] = MagicMock() # ========================================================================== diff --git a/work/tests/test_llm_scoping.py b/work/tests/test_llm_scoping.py index 9a647f0..c185c5c 100644 --- a/work/tests/test_llm_scoping.py +++ b/work/tests/test_llm_scoping.py @@ -7,6 +7,7 @@ Covers: (d) ContextConfig.max_age default is 14 days (1_209_600 seconds) """ +import importlib import sys from unittest.mock import MagicMock @@ -14,11 +15,23 @@ import pytest # --------------------------------------------------------------------------- # Stub optional heavy deps so meshai.router can be imported in this env. -# These stubs are set before any meshai.router import in this process; -# they are no-ops in production where the real packages are installed. +# +# Try the real import first -- sys.modules.setdefault() alone is only a +# no-op "in production where the real packages are installed" if some +# earlier-run test has already imported the real module. When this file +# collects first (test order is alphabetical, not guaranteed), setdefault() +# permanently replaces a genuinely-installed module (e.g. aiosqlite) with a +# bare MagicMock for the rest of the pytest session -- every later test's +# `await aiosqlite.connect(...)` then breaks with "MagicMock can't be used +# in 'await' expression". Only fall back to the mock when the real package +# truly isn't importable. # --------------------------------------------------------------------------- for _mod in ("openai", "aiosqlite", "anthropic", "google", "google.genai"): - sys.modules.setdefault(_mod, MagicMock()) + if _mod not in sys.modules: + try: + importlib.import_module(_mod) + except ImportError: + sys.modules[_mod] = MagicMock() from meshai.config import ( # noqa: E402 BotConfig,