From 5c7e6b80c9e4274b9582e7ffc898f91b224f5351 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:18:15 +0000 Subject: [PATCH 1/4] test: isolate global MCP registry and pin savings tests to bundled cost map Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../_experimental/mcp_server/conftest.py | 31 +++++++++++++++++++ .../proxy/spend_tracking/test_savings.py | 2 ++ 2 files changed, 33 insertions(+) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py b/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py index b477bf3f406..c559e023c47 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py @@ -2,6 +2,37 @@ import os import pytest +from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( + global_mcp_server_manager, +) + + +@pytest.fixture(autouse=True) +def _hermetic_mcp_server_registry(): + """Snapshot and restore the global manager's server-registry state around every test. + + ``global_mcp_server_manager`` is a module-global singleton, and many tests in this + package seed ``registry``/``config_mcp_servers`` (or clear them) without cleaning up. + In a shared CI shard the leaked entries poison later tests in the same worker, e.g. + the ``all_proxy_servers`` sentinel expansion in ``auth/`` suddenly sees a bridge + server registered by a discovery test, so the outcome depends on xdist scheduling. + Restoring the state here makes ordering irrelevant. + """ + saved_registry = dict(global_mcp_server_manager.registry) + saved_config_servers = dict(global_mcp_server_manager.config_mcp_servers) + saved_tool_mapping = dict(global_mcp_server_manager.tool_name_to_mcp_server_name_mapping) + saved_oauth_slots = global_mcp_server_manager._oauth_discovery_slots + try: + yield + finally: + global_mcp_server_manager.registry.clear() + global_mcp_server_manager.registry.update(saved_registry) + global_mcp_server_manager.config_mcp_servers.clear() + global_mcp_server_manager.config_mcp_servers.update(saved_config_servers) + global_mcp_server_manager.tool_name_to_mcp_server_name_mapping.clear() + global_mcp_server_manager.tool_name_to_mcp_server_name_mapping.update(saved_tool_mapping) + global_mcp_server_manager._oauth_discovery_slots = saved_oauth_slots + @pytest.fixture(autouse=True) def _hermetic_server_root_path(): diff --git a/tests/test_litellm/proxy/spend_tracking/test_savings.py b/tests/test_litellm/proxy/spend_tracking/test_savings.py index c3297ee6ae9..7dd18587df3 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_savings.py +++ b/tests/test_litellm/proxy/spend_tracking/test_savings.py @@ -14,6 +14,8 @@ from litellm.proxy.spend_tracking.savings import ( from litellm.router import Router from litellm.types.utils import Usage +pytestmark = pytest.mark.usefixtures("local_model_cost_map") + def _anthropic_costs(model: str) -> tuple[float, float]: info = litellm.get_model_info(model=model, custom_llm_provider="anthropic") From dd031f1036eaff49c428da4a133e904b7b3a702f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:42:07 +0000 Subject: [PATCH 2/4] fix(ci): parse paginated gh api output without splitting on unicode line breaks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/scripts/close_duplicate_issues.py | 30 +++++++----- .../test_github_close_duplicate_issues.py | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 tests/test_litellm/test_github_close_duplicate_issues.py diff --git a/.github/scripts/close_duplicate_issues.py b/.github/scripts/close_duplicate_issues.py index ec522af4f88..4c837c06418 100755 --- a/.github/scripts/close_duplicate_issues.py +++ b/.github/scripts/close_duplicate_issues.py @@ -39,6 +39,23 @@ def gh(*args: str) -> str: return result.stdout +def parse_concatenated_json(raw: str) -> list[dict]: + """Parse the concatenated JSON documents that `gh api --paginate` emits.""" + decoder = json.JSONDecoder() + issues: list[dict] = [] + idx = 0 + while idx < len(raw): + if raw[idx].isspace(): + idx += 1 + continue + parsed, idx = decoder.raw_decode(raw, idx) + if isinstance(parsed, list): + issues.extend(parsed) + else: + issues.append(parsed) + return issues + + def fetch_open_issues(repo: str | None) -> list[dict]: """Fetch all open issues (excluding PRs) via gh api --paginate.""" if repo: @@ -49,18 +66,7 @@ def fetch_open_issues(repo: str | None) -> list[dict]: endpoint = "repos/{owner}/{repo}/issues?state=open&per_page=100&sort=created&direction=asc" cmd = ["api", "--paginate", endpoint] - raw = gh(*cmd) - # gh --paginate concatenates JSON arrays, so we may get multiple arrays - issues = [] - for line in raw.strip().splitlines(): - line = line.strip() - if not line: - continue - parsed = json.loads(line) - if isinstance(parsed, list): - issues.extend(parsed) - else: - issues.append(parsed) + issues = parse_concatenated_json(gh(*cmd)) # Filter out pull requests (they also appear in the issues endpoint) return [i for i in issues if "pull_request" not in i] diff --git a/tests/test_litellm/test_github_close_duplicate_issues.py b/tests/test_litellm/test_github_close_duplicate_issues.py new file mode 100644 index 00000000000..0ee9b3f096d --- /dev/null +++ b/tests/test_litellm/test_github_close_duplicate_issues.py @@ -0,0 +1,46 @@ +"""Unit tests for `.github/scripts/close_duplicate_issues.py`.""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +SCRIPT_PATH = ( + Path(__file__).resolve().parents[2] + / ".github" + / "scripts" + / "close_duplicate_issues.py" +) + + +@pytest.fixture(scope="module") +def dedupe_module(): + spec = importlib.util.spec_from_file_location("close_duplicate_issues", SCRIPT_PATH) + assert spec and spec.loader, f"Could not load spec for {SCRIPT_PATH}" + module = importlib.util.module_from_spec(spec) + sys.modules["close_duplicate_issues"] = module + spec.loader.exec_module(module) + return module + + +def test_parse_concatenated_json_joins_paginated_arrays(dedupe_module): + page_one = json.dumps([{"number": 1, "title": "a"}, {"number": 2, "title": "b"}]) + page_two = json.dumps([{"number": 3, "title": "c"}]) + issues = dedupe_module.parse_concatenated_json(page_one + page_two) + assert [i["number"] for i in issues] == [1, 2, 3] + + +@pytest.mark.parametrize("separator", ["\u2028", "\u2029", "\x85"]) +def test_parse_concatenated_json_survives_unicode_line_breaks_in_bodies( + dedupe_module, separator +): + body = f"first{separator}second" + page_one = json.dumps([{"number": 1, "title": "a", "body": body}]) + page_two = json.dumps([{"number": 2, "title": "b", "body": "plain"}]) + issues = dedupe_module.parse_concatenated_json(page_one + page_two) + assert [i["number"] for i in issues] == [1, 2] + assert issues[0]["body"] == body From 95c7ca8801b2fdde31763f8d5869355646bb93e4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:59:08 +0000 Subject: [PATCH 3/4] fix(tests): restore module attributes after reload in mcp identity env tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../mcp_server/test_mcp_server_identity_env.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py index ac7082c2668..934810c305f 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py @@ -23,6 +23,14 @@ MGMT_MODULE = "litellm.proxy.management_endpoints.mcp_management_endpoints" @contextlib.contextmanager def _env_and_reload(**env): saved = {key: os.environ.get(key) for key in env} + utils_module = importlib.import_module(UTILS_MODULE) + mgmt_module = importlib.import_module(MGMT_MODULE) + # Restore the pre-reload module attributes afterwards instead of reloading + # a third time: a reload re-creates every class in the module, so modules + # that imported names like MCPMissingUserEnvVarsError before this test + # would keep raising the old class while pytest.raises in later tests + # matches the new one + snapshots = {module: dict(vars(module)) for module in (utils_module, mgmt_module)} def _apply_env(values): for key, value in values.items(): @@ -32,8 +40,8 @@ def _env_and_reload(**env): os.environ[key] = value def _reload(): - utils = importlib.reload(importlib.import_module(UTILS_MODULE)) - mgmt = importlib.reload(importlib.import_module(MGMT_MODULE)) + utils = importlib.reload(utils_module) + mgmt = importlib.reload(mgmt_module) return utils, mgmt try: @@ -41,7 +49,10 @@ def _env_and_reload(**env): yield _reload() finally: _apply_env(saved) - _reload() + for module, snapshot in snapshots.items(): + for key in [key for key in vars(module) if key not in snapshot]: + delattr(module, key) + vars(module).update(snapshot) def test_defaults_used_when_env_unset(): From 5f44bdd1c1230e4ce18515453c8747cb55d7cf4a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:06:08 +0000 Subject: [PATCH 4/4] test: trim mcp fixture docstring and reload comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/_experimental/mcp_server/conftest.py | 11 ++--------- .../mcp_server/test_mcp_server_identity_env.py | 8 +++----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py b/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py index c559e023c47..2ccba2b2055 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/conftest.py @@ -9,15 +9,8 @@ from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( @pytest.fixture(autouse=True) def _hermetic_mcp_server_registry(): - """Snapshot and restore the global manager's server-registry state around every test. - - ``global_mcp_server_manager`` is a module-global singleton, and many tests in this - package seed ``registry``/``config_mcp_servers`` (or clear them) without cleaning up. - In a shared CI shard the leaked entries poison later tests in the same worker, e.g. - the ``all_proxy_servers`` sentinel expansion in ``auth/`` suddenly sees a bridge - server registered by a discovery test, so the outcome depends on xdist scheduling. - Restoring the state here makes ordering irrelevant. - """ + """Restore the singleton ``global_mcp_server_manager``'s registry state around every + test, so entries seeded by one test never leak into another on a shared shard.""" saved_registry = dict(global_mcp_server_manager.registry) saved_config_servers = dict(global_mcp_server_manager.config_mcp_servers) saved_tool_mapping = dict(global_mcp_server_manager.tool_name_to_mcp_server_name_mapping) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py index 934810c305f..1c65adac4c6 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_identity_env.py @@ -25,11 +25,9 @@ def _env_and_reload(**env): saved = {key: os.environ.get(key) for key in env} utils_module = importlib.import_module(UTILS_MODULE) mgmt_module = importlib.import_module(MGMT_MODULE) - # Restore the pre-reload module attributes afterwards instead of reloading - # a third time: a reload re-creates every class in the module, so modules - # that imported names like MCPMissingUserEnvVarsError before this test - # would keep raising the old class while pytest.raises in later tests - # matches the new one + # Restore pre-reload module attributes afterwards instead of reloading again: + # a reload re-creates the module's classes, breaking exception identity for + # modules that imported them earlier snapshots = {module: dict(vars(module)) for module in (utils_module, mgmt_module)} def _apply_env(values):