From 81827be215ffe4149125ccdb94b4ed4b8e857bc5 Mon Sep 17 00:00:00 2001 From: Julio Quinteros Pro Date: Tue, 17 Feb 2026 22:33:06 -0300 Subject: [PATCH 1/2] fix: prevent sys.modules["langfuse"] import failures in langfuse unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three test failures caused by the real langfuse SDK import being triggered at test time: 1. test_langfuse_prompt_management.py: Both tests create LangfusePromptManagement() which calls `import langfuse`. Since earlier TestLangfuseUsageDetails tests remove sys.modules["langfuse"] via patch.dict teardown, the real langfuse import runs and fails on Python 3.14 (pydantic v1 incompatibility). Fix: add setup_method/teardown_method to mock sys.modules["langfuse"]. 2. test_langfuse.py::test_max_langfuse_clients_limit: Same root cause — creates LangFuseLogger() without mocking sys.modules["langfuse"]. Fix: wrap test body with patch.dict("sys.modules", {"langfuse": mock}). 3. test_langfuse_otel.py::test_extract_langfuse_metadata_with_header_enrichment: Replaces sys.modules["litellm.integrations.langfuse.langfuse"] with a stub without restoring it, causing patch() in later tests to target the stub instead of the real module. Fix: use monkeypatch.setitem() which auto-restores after the test. Co-Authored-By: Claude Sonnet 4.6 --- .../langfuse/test_langfuse_prompt_management.py | 16 +++++++++++++++- tests/test_litellm/integrations/test_langfuse.py | 9 ++++++++- .../integrations/test_langfuse_otel.py | 8 ++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py b/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py index 5389cdf7377..c557bdb67ee 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py @@ -1,5 +1,5 @@ import os -from unittest.mock import patch +from unittest.mock import MagicMock, patch from litellm.integrations.langfuse.langfuse_prompt_management import ( LangfusePromptManagement, @@ -7,6 +7,20 @@ from litellm.integrations.langfuse.langfuse_prompt_management import ( class TestLangfusePromptManagement: + def setup_method(self): + # Mock langfuse package to avoid triggering real import. + # The real langfuse import fails on Python 3.14 due to pydantic v1 incompatibility. + # This also prevents test-ordering issues when earlier tests remove sys.modules["langfuse"]. + self._mock_langfuse = MagicMock() + self._mock_langfuse.version.__version__ = "3.0.0" + self._langfuse_patcher = patch.dict( + "sys.modules", {"langfuse": self._mock_langfuse} + ) + self._langfuse_patcher.start() + + def teardown_method(self): + self._langfuse_patcher.stop() + def test_get_prompt_from_id(self): langfuse_prompt_management = LangfusePromptManagement() with patch.object( diff --git a/tests/test_litellm/integrations/test_langfuse.py b/tests/test_litellm/integrations/test_langfuse.py index 8da7dd8917b..77ff5dbb97e 100644 --- a/tests/test_litellm/integrations/test_langfuse.py +++ b/tests/test_litellm/integrations/test_langfuse.py @@ -472,8 +472,15 @@ def test_max_langfuse_clients_limit(): """ Test that the max langfuse clients limit is respected when initializing multiple clients """ + # Mock langfuse package to avoid triggering real import. + # The real langfuse import fails on Python 3.14 due to pydantic v1 incompatibility, + # and sys.modules["langfuse"] may be absent after other tests in the suite clean up. + mock_langfuse = MagicMock() + mock_langfuse.version.__version__ = "3.0.0" # Set max clients to 2 for testing - with patch.object(langfuse_module, "MAX_LANGFUSE_INITIALIZED_CLIENTS", 2): + with patch.dict("sys.modules", {"langfuse": mock_langfuse}), patch.object( + langfuse_module, "MAX_LANGFUSE_INITIALIZED_CLIENTS", 2 + ): # Reset the counter litellm.initialized_langfuse_clients = 0 diff --git a/tests/test_litellm/integrations/test_langfuse_otel.py b/tests/test_litellm/integrations/test_langfuse_otel.py index ba4a096be24..44853d9dce5 100644 --- a/tests/test_litellm/integrations/test_langfuse_otel.py +++ b/tests/test_litellm/integrations/test_langfuse_otel.py @@ -157,8 +157,12 @@ class TestLangfuseOtelIntegration: stub_module.LangFuseLogger = StubLFLogger # type: ignore - # Register stub in sys.modules so import inside method succeeds - sys.modules["litellm.integrations.langfuse.langfuse"] = stub_module # type: ignore + # Register stub in sys.modules so import inside method succeeds. + # Use monkeypatch so the real module is restored after the test runs, + # preventing sys.modules corruption that would break patch() targets in + # later tests (the patch would hit the stub while the real module's + # globals remain unpatched). + monkeypatch.setitem(sys.modules, "litellm.integrations.langfuse.langfuse", stub_module) # type: ignore kwargs = {"litellm_params": {"metadata": {"foo": "bar"}}} extracted = LangfuseOtelLogger._extract_langfuse_metadata(kwargs) From 2e0a8b3cf892ee4fb179fc502f0e43f597b36e2c Mon Sep 17 00:00:00 2001 From: Julio Quinteros Pro Date: Wed, 18 Feb 2026 14:08:48 -0300 Subject: [PATCH 2/2] fix(tests): resolve MCP test isolation failures in parallel execution Three test isolation issues fixed: 1. test_mcp_debug.py: Replace deprecated asyncio.get_event_loop().run_until_complete() with asyncio.run() in TestWrapSendWithDebugHeaders. In Python 3.10+, get_event_loop() raises RuntimeError when no event loop is set in the current thread, causing test_injects_headers and test_body_messages_unchanged to fail in isolation. 2. test_mcp_server_manager.py: After _reload_mcp_manager_module() creates a new global_mcp_server_manager instance, server.py still holds a stale reference to the old instance. Tests in test_mcp_server.py that populate the new manager's registry and then call server.py functions (e.g. _get_tools_from_mcp_servers) get empty results because server.py reads from the old manager. Fix: update server.py's module-level reference after each reload. 3. test_litellm_pre_call_utils.py: test_add_litellm_metadata_from_request_headers sets litellm.callbacks without restoring it afterward. Add cleanup to restore original callbacks after the test to prevent state leaking to subsequent tests. Co-Authored-By: Claude Sonnet 4.6 --- .../proxy/_experimental/mcp_server/test_mcp_debug.py | 4 ++-- .../mcp_server/test_mcp_server_manager.py | 11 ++++++++++- .../test_litellm/proxy/test_litellm_pre_call_utils.py | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_debug.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_debug.py index 0fb299a57cd..de2037793c5 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_debug.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_debug.py @@ -230,7 +230,7 @@ class TestWrapSendWithDebugHeaders: ) message = {"type": "http.response.start", "status": 200, "headers": []} - asyncio.get_event_loop().run_until_complete(wrapped(message)) + asyncio.run(wrapped(message)) assert len(captured) == 1 headers = dict(captured[0]["headers"]) @@ -247,6 +247,6 @@ class TestWrapSendWithDebugHeaders: ) body_msg = {"type": "http.response.body", "body": b"hello"} - asyncio.get_event_loop().run_until_complete(wrapped(body_msg)) + asyncio.run(wrapped(body_msg)) assert captured[0] == body_msg diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 1a50cacd308..464e5238325 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -39,7 +39,16 @@ def _reload_mcp_manager_module(): "litellm.proxy._experimental.mcp_server.mcp_server_manager" ] importlib.reload(utils_module) - return importlib.reload(manager_module) + reloaded = importlib.reload(manager_module) + # After reload, server.py still holds a stale reference to the old + # global_mcp_server_manager. Update it so tests that exercise server.py + # functions (e.g. _get_tools_from_mcp_servers) use the fresh instance. + server_module = sys.modules.get( + "litellm.proxy._experimental.mcp_server.server" + ) + if server_module is not None and hasattr(server_module, "global_mcp_server_manager"): + server_module.global_mcp_server_manager = reloaded.global_mcp_server_manager + return reloaded class TestMCPServerManager: diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 452db3902c0..3bf783c09d4 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -1014,6 +1014,7 @@ async def test_add_litellm_metadata_from_request_headers(): # Set up test logger litellm._turn_on_debug() test_logger = TestCustomLogger() + original_callbacks = litellm.callbacks litellm.callbacks = [test_logger] # Prepare test data (ensure no streaming, add mock_response and api_key to route to litellm.acompletion) @@ -1098,7 +1099,9 @@ async def test_add_litellm_metadata_from_request_headers(): SPEND_LOGS_METADATA = standard_logging_obj["metadata"]["spend_logs_metadata"] assert SPEND_LOGS_METADATA == dict(json.loads(headers["x-litellm-spend-logs-metadata"])), "spend_logs_metadata should be the same as the headers" - + litellm.callbacks = original_callbacks + + def test_get_internal_user_header_from_mapping_returns_expected_header(): mappings = [