mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-16 12:40:18 +00:00
test(#5364): fully restore sys.modules in the profiles import harness
The regression test re-imports api.profiles in isolation by stubbing flask/yaml/agent in sys.modules and deleting+reloading the real api / api.profiles modules. Teardown only popped api/api.* (never restoring the real modules) and left the flask/yaml stubs behind, so the manipulation LEAKED: subsequent tests re-imported api.config/api.routes against the stub yaml (safe_load->None) and a half-populated api package, silently breaking ~120 unrelated tests in the full serial suite (e.g. MCP/provider/config tests whose get_config patch no longer saw real config). Snapshot every sys.modules key we touch and restore it exactly (real modules back, injected stubs removed) in a finally block. Full suite now matches master baseline (11537 passed, only the 2 known cron-isolation artifacts).
This commit is contained in:
@@ -38,7 +38,17 @@ import pytest
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_profiles_module():
|
||||
"""Import api.profiles with minimal stubs for heavy dependencies."""
|
||||
"""Import api.profiles with minimal stubs for heavy dependencies.
|
||||
|
||||
Returns ``(mod, saved_modules, injected_names)`` so the caller can fully
|
||||
restore ``sys.modules`` on teardown. This test intentionally manipulates
|
||||
``sys.modules`` (stubbing flask/yaml/agent and re-importing api.profiles
|
||||
in isolation); if those mutations leak, later tests re-import api.config /
|
||||
api.routes against the stub ``yaml`` (whose ``safe_load`` returns None) and
|
||||
a half-populated ``api`` package, which silently breaks unrelated suites
|
||||
(e.g. MCP/provider/config tests whose ``get_config`` patch no longer sees
|
||||
real config). So we snapshot every key we touch and restore it exactly.
|
||||
"""
|
||||
stubs = {
|
||||
"flask": types.ModuleType("flask"),
|
||||
"yaml": types.ModuleType("yaml"),
|
||||
@@ -61,6 +71,13 @@ def _make_profiles_module():
|
||||
su.parse_frontmatter = MagicMock(return_value=({}, ""))
|
||||
su.skill_matches_platform = MagicMock(return_value=True)
|
||||
|
||||
# Snapshot every sys.modules key we are about to mutate so teardown can
|
||||
# restore the real modules (not just delete them). Keys absent now are
|
||||
# recorded as sentinel so teardown removes any stub we introduced.
|
||||
_ABSENT = object()
|
||||
touched = set(stubs) | {"api", "api.profiles"}
|
||||
saved_modules = {k: sys.modules.get(k, _ABSENT) for k in touched}
|
||||
|
||||
for name, mod in stubs.items():
|
||||
sys.modules.setdefault(name, mod)
|
||||
|
||||
@@ -80,16 +97,24 @@ def _make_profiles_module():
|
||||
spec.loader.exec_module(mod)
|
||||
except Exception:
|
||||
pass
|
||||
return mod
|
||||
return mod, saved_modules, _ABSENT
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mod(tmp_path):
|
||||
saved_modules = {}
|
||||
_ABSENT = object()
|
||||
try:
|
||||
m = _make_profiles_module()
|
||||
m, saved_modules, _ABSENT = _make_profiles_module()
|
||||
assert hasattr(m, "_get_profile_skills_stats")
|
||||
assert hasattr(m, "_skills_stats_lock_for")
|
||||
except Exception:
|
||||
# Restore anything we already mutated before skipping.
|
||||
for k, v in saved_modules.items():
|
||||
if v is _ABSENT:
|
||||
sys.modules.pop(k, None)
|
||||
else:
|
||||
sys.modules[k] = v
|
||||
pytest.skip("api.profiles not importable in this environment")
|
||||
# Reset both caches + the per-profile lock registry for isolation.
|
||||
if hasattr(m, "_SKILLS_STATS_CACHE"):
|
||||
@@ -98,11 +123,17 @@ def mod(tmp_path):
|
||||
m._SKILLS_STATS_LOCKS.clear()
|
||||
if hasattr(m, "_LIST_PROFILES_CACHE"):
|
||||
m._LIST_PROFILES_CACHE = None
|
||||
yield m
|
||||
saved = {k: v for k, v in list(sys.modules.items())
|
||||
if k == "api" or k.startswith("api.")}
|
||||
for k in list(saved):
|
||||
sys.modules.pop(k, None)
|
||||
try:
|
||||
yield m
|
||||
finally:
|
||||
# Fully restore sys.modules: put back the real modules we evicted and
|
||||
# drop any stub we introduced, so the manipulation cannot leak into
|
||||
# subsequent tests (they re-import the real api.config/api.routes).
|
||||
for k, v in saved_modules.items():
|
||||
if v is _ABSENT:
|
||||
sys.modules.pop(k, None)
|
||||
else:
|
||||
sys.modules[k] = v
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user