mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
Some checks failed
Adds integration contracts for MCP lifecycle, protocol errors and OAuth configuration, A2A wire versions, the OpenAI consumer path, persisted toolsets, callback delivery, guardrail effects, configured prices, the filtered spend ledger, and a CircleCI-owned browser flow for project detachment, with the ASGI, browser-state, client and MCP helpers they use. Consolidates the eleven commits previously stacked on litellm_integration_providers onto its rebased tip
123 lines
6.4 KiB
Python
123 lines
6.4 KiB
Python
import uuid
|
|
from contextlib import ExitStack
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from hypothesis import strategies as st
|
|
from hypothesis.stateful import RuleBasedStateMachine, invariant, rule, run_state_machine_as_test
|
|
|
|
from integration._support.client import Gateway
|
|
from integration._support.database import read_rows
|
|
from integration._support.generation import LIFECYCLE_SETTINGS, bounded_http_requests
|
|
from integration._support.mcp import call_tool, mcp_peer, register_mcp, tool_names
|
|
|
|
|
|
@pytest.mark.covers("mcp.call_tool.saved_headers.reach_actual_transport")
|
|
def test_saved_headers_reach_real_mcp_tool_and_survive_unrelated_edit(gateway: Gateway) -> None:
|
|
with mcp_peer() as peer, gateway.scenario() as scenario:
|
|
alias: Final = "integration" + uuid.uuid4().hex
|
|
identity: Final = register_mcp(
|
|
scenario, peer, alias, static_headers={"X-Integration-Saved": "synthetic-header-value"}
|
|
)
|
|
key: Final = scenario.key(object_permission={"mcp_servers": [identity]})
|
|
for generation in range(2):
|
|
names: Final = tool_names(gateway, key, identity)
|
|
assert set(names) == {"add", "multiply", "fail"}
|
|
peer.drain()
|
|
response: Final = call_tool(gateway, key, identity, names["add"], {"a": 3, "b": 5})
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["isError"] is False
|
|
assert len(response.json()["content"]) == 1
|
|
assert response.json()["content"][0]["type"] == "text"
|
|
assert response.json()["content"][0]["text"] == "8"
|
|
calls: Final = tuple(item for item in peer.drain() if item["body"].get("method") == "tools/call")
|
|
assert len(calls) == 1
|
|
assert calls[0]["headers"][b"x-integration-saved"] == b"synthetic-header-value"
|
|
assert calls[0]["body"]["params"]["name"] == "add"
|
|
assert calls[0]["body"]["params"]["arguments"] == {"a": 3, "b": 5}
|
|
if generation == 0:
|
|
updated: Final = gateway.request(
|
|
"PUT", "/v1/mcp/server", {"server_id": identity, "server_name": alias + "renamed"}
|
|
)
|
|
assert updated.status_code == 202, updated.text
|
|
rows: Final = read_rows('SELECT server_name FROM "LiteLLM_MCPServerTable" WHERE server_id = %s', (identity,))
|
|
assert rows == [{"server_name": alias + "renamed"}]
|
|
|
|
|
|
@pytest.mark.covers("mcp.call_tool.errors.tool_failure_is_not_success")
|
|
def test_tool_error_remains_error_and_healthy_sibling_returns_value(gateway: Gateway) -> None:
|
|
with mcp_peer() as peer, gateway.scenario() as scenario:
|
|
identity: Final = register_mcp(scenario, peer, "integration" + uuid.uuid4().hex)
|
|
key: Final = scenario.key(object_permission={"mcp_servers": [identity]})
|
|
names: Final = tool_names(gateway, key, identity)
|
|
failure: Final = call_tool(gateway, key, identity, names["fail"], {})
|
|
assert failure.status_code == 200, failure.text
|
|
assert failure.json()["isError"] is True
|
|
assert "synthetic tool failure" in failure.json()["content"][0]["text"]
|
|
healthy: Final = call_tool(gateway, key, identity, names["multiply"], {"a": 3, "b": 5})
|
|
assert healthy.status_code == 200, healthy.text
|
|
assert healthy.json()["isError"] is False
|
|
assert healthy.json()["content"][0]["text"] == "15"
|
|
|
|
|
|
@pytest.mark.timeout(180)
|
|
@pytest.mark.covers("other.mcp.lifecycle.generated_save_reload_preserves_effective_headers")
|
|
def test_generated_mcp_edits_preserve_actual_headers_and_tool_results(gateway: Gateway) -> None:
|
|
with mcp_peer() as peer, bounded_http_requests((gateway,), limit=1500) as budget:
|
|
|
|
class Servers(RuleBasedStateMachine):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.resources = ExitStack()
|
|
self.marker = "first"
|
|
self.name = "integration" + uuid.uuid4().hex
|
|
try:
|
|
scenario = self.resources.enter_context(gateway.scenario())
|
|
self.identity = register_mcp(
|
|
scenario, peer, self.name, static_headers={"X-Integration-Saved": self.marker}
|
|
)
|
|
self.key = scenario.key(object_permission={"mcp_servers": [self.identity]})
|
|
except BaseException:
|
|
with budget.cleanup():
|
|
self.resources.close()
|
|
raise
|
|
|
|
@rule(value=st.sampled_from(("first", "second", "third")))
|
|
def header(self, value: str) -> None:
|
|
response: Final = gateway.request(
|
|
"PUT",
|
|
"/v1/mcp/server",
|
|
{"server_id": self.identity, "static_headers": {"X-Integration-Saved": value}},
|
|
)
|
|
assert response.status_code == 202, response.text
|
|
self.marker = value
|
|
|
|
@rule(value=st.sampled_from(("original", "renamed")))
|
|
def rename(self, value: str) -> None:
|
|
response: Final = gateway.request(
|
|
"PUT", "/v1/mcp/server", {"server_id": self.identity, "server_name": self.name + value}
|
|
)
|
|
assert response.status_code == 202, response.text
|
|
|
|
@invariant()
|
|
def persisted_configuration_controls_actual_tools(self) -> None:
|
|
names: Final = tool_names(gateway, self.key, self.identity)
|
|
assert set(names) == {"add", "multiply", "fail"}
|
|
peer.drain()
|
|
result: Final = call_tool(gateway, self.key, self.identity, names["add"], {"a": 3, "b": 5})
|
|
assert result.status_code == 200 and result.json()["isError"] is False, result.text
|
|
assert result.json()["content"][0]["text"] == "8"
|
|
calls: Final = tuple(item for item in peer.drain() if item["body"].get("method") == "tools/call")
|
|
assert len(calls) == 1 and calls[0]["headers"][b"x-integration-saved"] == self.marker.encode()
|
|
assert (
|
|
len(
|
|
read_rows('SELECT server_id FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (self.identity,))
|
|
)
|
|
== 1
|
|
)
|
|
|
|
def teardown(self) -> None:
|
|
with budget.cleanup():
|
|
self.resources.close()
|
|
|
|
run_state_machine_as_test(Servers, settings=LIFECYCLE_SETTINGS)
|