mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
* test(e2e): add enum values, auto-discovering label gates and secret hiding for e2e metadata * test(e2e): tag management tests with Subject metadata and record management client steps * docs(e2e): name every markerless harness test file that carries no Subject * test(e2e): keep the step discovery comprehensions to one for clause * test(e2e): keep the prompt out of the chat_status step so polled retries collapse
355 lines
14 KiB
Python
355 lines
14 KiB
Python
"""Live e2e: the MCP server and toolset management routes' lifecycle contract.
|
|
|
|
Two customer defects sit on these routes, and each step here is the read-back that
|
|
would have caught one of them: a dashboard edit that took several saves to stick
|
|
because the read landed on a replica the write had not reached, and a toolset whose
|
|
tools were stored under one name and read back under another, so it granted
|
|
nothing. Every read-back therefore polls every replica that serves the route
|
|
(ProxyClient.read_back_everywhere) and asserts the exact values written, and both
|
|
update routes are held to the same partial-update contract: a field left out of the
|
|
payload keeps its stored value, a field sent as null is cleared. The server URL is
|
|
unreachable on purpose; only persistence is under test, never a tool call.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from e2e_config import unique_marker
|
|
from e2e_http import unwrap
|
|
from e2e_metadata import Domain, Route, Subject, meta
|
|
from lifecycle import ResourceManager
|
|
from management_client import ManagementClient
|
|
from models import (
|
|
McpInfo,
|
|
McpServerCreateBody,
|
|
McpServerListResponse,
|
|
McpServerRow,
|
|
McpServerUpdateBody,
|
|
ToolsetCreateBody,
|
|
ToolsetListResponse,
|
|
ToolsetRow,
|
|
ToolsetTool,
|
|
ToolsetUpdateBody,
|
|
)
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
UNREACHABLE_URL: Final = "https://e2e-fake-mcp.test.local/mcp"
|
|
|
|
|
|
def _create_server(client: ManagementClient, resources: ResourceManager) -> tuple[McpServerCreateBody, str]:
|
|
name: Final = f"e2e_mcp_lifecycle_{unique_marker()}"
|
|
body: Final = McpServerCreateBody(
|
|
server_name=name,
|
|
alias=name,
|
|
url=UNREACHABLE_URL,
|
|
transport="http",
|
|
description="e2e lifecycle server",
|
|
mcp_info=McpInfo(
|
|
server_name=f"{name} (display)",
|
|
description="shown on the MCP page",
|
|
logo_url="https://e2e.test.local/logo.png",
|
|
),
|
|
)
|
|
server_id: Final = client.create_mcp_server(body).server_id
|
|
resources.defer(lambda: client.delete_mcp_server(server_id))
|
|
return body, server_id
|
|
|
|
|
|
def _assert_server_matches(row: McpServerRow, written: McpServerCreateBody, *, where: str) -> None:
|
|
stored: Final = (row.server_name, row.alias, row.url, row.transport, row.description, row.mcp_info)
|
|
expected: Final = (
|
|
written.server_name,
|
|
written.alias,
|
|
written.url,
|
|
written.transport,
|
|
written.description,
|
|
written.mcp_info,
|
|
)
|
|
assert stored == expected, f"{where}: stored {stored}, expected {expected}"
|
|
|
|
|
|
def _server_everywhere(
|
|
client: ManagementClient, server_id: str, *, settled: Callable[[McpServerRow], bool]
|
|
) -> Mapping[str, McpServerRow]:
|
|
return client.proxy.read_body_back_everywhere(f"/v1/mcp/server/{server_id}", McpServerRow, settled=settled)
|
|
|
|
|
|
def _listed_server_everywhere(client: ManagementClient, server_id: str) -> Mapping[str, McpServerRow]:
|
|
listings: Final = client.proxy.read_body_back_everywhere(
|
|
"/v1/mcp/server",
|
|
McpServerListResponse,
|
|
settled=lambda rows: any(row.server_id == server_id for row in rows.root),
|
|
)
|
|
return {replica: next(row for row in rows.root if row.server_id == server_id) for replica, rows in listings.items()}
|
|
|
|
|
|
class TestMcpServerLifecycle:
|
|
@pytest.mark.covers("mgmt.mcp_server.new.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_create_persists_every_field_on_every_replica(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
body, server_id = _create_server(client, resources)
|
|
|
|
by_id: Final = _server_everywhere(client, server_id, settled=lambda row: row.server_id == server_id)
|
|
for replica, row in by_id.items():
|
|
_assert_server_matches(row, body, where=f"GET /v1/mcp/server/{server_id} on {replica}")
|
|
|
|
@pytest.mark.skip(
|
|
reason=(
|
|
"product gap: GET /v1/mcp/server builds each row from the in-memory registry, whose "
|
|
"_build_mcp_server_table sets description from mcp_info['description'], so the list "
|
|
"reports the mcp_info description while GET /v1/mcp/server/{server_id} reports the "
|
|
"stored description column. A server created with both set to different text reads "
|
|
"back with two different descriptions depending on the route"
|
|
)
|
|
)
|
|
@pytest.mark.covers("mgmt.mcp_server.list.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_created_server_is_listed_with_every_field(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
body, server_id = _create_server(client, resources)
|
|
|
|
for replica, row in _listed_server_everywhere(client, server_id).items():
|
|
_assert_server_matches(row, body, where=f"GET /v1/mcp/server on {replica}")
|
|
|
|
@pytest.mark.covers("mgmt.mcp_server.update.preserves_unrelated_fields")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_updating_only_the_alias_keeps_every_other_field_on_every_replica(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
body, server_id = _create_server(client, resources)
|
|
renamed: Final = f"{body.alias}_renamed"
|
|
|
|
_ = client.update_mcp_server(McpServerUpdateBody(server_id=server_id, alias=renamed))
|
|
|
|
after_one_put: Final = _server_everywhere(client, server_id, settled=lambda row: row.alias == renamed)
|
|
for replica, row in after_one_put.items():
|
|
_assert_server_matches(
|
|
row,
|
|
body.model_copy(update={"alias": renamed}),
|
|
where=f"GET /v1/mcp/server/{server_id} on {replica} after one PUT of alias",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_server.update.clear_persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_clearing_the_description_with_null_reads_back_null(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
body, server_id = _create_server(client, resources)
|
|
|
|
_ = client.update_mcp_server(McpServerUpdateBody(server_id=server_id, description=None))
|
|
|
|
cleared: Final = _server_everywhere(client, server_id, settled=lambda row: row.description is None)
|
|
for replica, row in cleared.items():
|
|
_assert_server_matches(
|
|
row,
|
|
body.model_copy(update={"description": None}),
|
|
where=f"GET /v1/mcp/server/{server_id} on {replica} after PUT description=null",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_server.delete.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_delete_removes_the_server_from_every_replica(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
|
|
_ = unwrap(client.delete_mcp_server(server_id))
|
|
|
|
gone: Final = client.proxy.gone_everywhere(f"/v1/mcp/server/{server_id}")
|
|
assert set(gone.values()) == {404}, f"a deleted server must 404 on every replica; got {dict(gone)}"
|
|
listings: Final = client.proxy.read_body_back_everywhere(
|
|
"/v1/mcp/server",
|
|
McpServerListResponse,
|
|
settled=lambda rows: all(row.server_id != server_id for row in rows.root),
|
|
)
|
|
for replica, rows in listings.items():
|
|
assert all(row.server_id != server_id for row in rows.root), (
|
|
f"GET /v1/mcp/server on {replica} still lists the deleted server {server_id}"
|
|
)
|
|
|
|
|
|
def _create_toolset(
|
|
client: ManagementClient, resources: ResourceManager, server_id: str
|
|
) -> tuple[ToolsetCreateBody, str]:
|
|
body: Final = ToolsetCreateBody(
|
|
toolset_name=f"e2e_toolset_{unique_marker()}",
|
|
description="e2e lifecycle toolset",
|
|
tools=[
|
|
ToolsetTool(server_id=server_id, tool_name="search_datadog_logs"),
|
|
ToolsetTool(server_id=server_id, tool_name="get_datadog_metric"),
|
|
],
|
|
)
|
|
toolset_id: Final = client.proxy.create_toolset(body).toolset_id
|
|
resources.defer(lambda: client.proxy.delete_toolset(toolset_id))
|
|
return body, toolset_id
|
|
|
|
|
|
def _assert_toolset_matches(row: ToolsetRow, written: ToolsetCreateBody, *, where: str) -> None:
|
|
stored: Final = (row.toolset_name, row.description, row.tools)
|
|
expected: Final = (written.toolset_name, written.description, written.tools)
|
|
assert stored == expected, f"{where}: stored {stored}, expected {expected}"
|
|
|
|
|
|
def _toolset_everywhere(
|
|
client: ManagementClient, toolset_id: str, *, settled: Callable[[ToolsetRow], bool]
|
|
) -> Mapping[str, ToolsetRow]:
|
|
return client.proxy.read_body_back_everywhere(f"/v1/mcp/toolset/{toolset_id}", ToolsetRow, settled=settled)
|
|
|
|
|
|
class TestMcpToolsetLifecycle:
|
|
@pytest.mark.covers("mgmt.mcp_toolset.new.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_create_persists_both_tools_under_the_exact_names_written(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
body, toolset_id = _create_toolset(client, resources, server_id)
|
|
|
|
by_id: Final = _toolset_everywhere(client, toolset_id, settled=lambda row: row.toolset_id == toolset_id)
|
|
for replica, row in by_id.items():
|
|
_assert_toolset_matches(row, body, where=f"GET /v1/mcp/toolset/{toolset_id} on {replica}")
|
|
listings: Final = client.proxy.read_body_back_everywhere(
|
|
"/v1/mcp/toolset",
|
|
ToolsetListResponse,
|
|
settled=lambda rows: any(row.toolset_id == toolset_id for row in rows.root),
|
|
)
|
|
for replica, rows in listings.items():
|
|
_assert_toolset_matches(
|
|
next(row for row in rows.root if row.toolset_id == toolset_id),
|
|
body,
|
|
where=f"GET /v1/mcp/toolset on {replica}",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_toolset.update.preserves_unrelated_fields")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_updating_only_the_description_keeps_the_tools_and_name(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
body, toolset_id = _create_toolset(client, resources, server_id)
|
|
|
|
_ = client.proxy.update_toolset(ToolsetUpdateBody(toolset_id=toolset_id, description="edited"))
|
|
|
|
edited: Final = _toolset_everywhere(client, toolset_id, settled=lambda row: row.description == "edited")
|
|
for replica, row in edited.items():
|
|
_assert_toolset_matches(
|
|
row,
|
|
body.model_copy(update={"description": "edited"}),
|
|
where=f"GET /v1/mcp/toolset/{toolset_id} on {replica} after PUT of description",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_toolset.update.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_updating_the_tools_to_one_entry_reads_back_exactly_that_entry(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
body, toolset_id = _create_toolset(client, resources, server_id)
|
|
kept: Final = body.tools[:1]
|
|
|
|
_ = client.proxy.update_toolset(ToolsetUpdateBody(toolset_id=toolset_id, tools=kept))
|
|
|
|
narrowed: Final = _toolset_everywhere(client, toolset_id, settled=lambda row: row.tools == kept)
|
|
for replica, row in narrowed.items():
|
|
_assert_toolset_matches(
|
|
row,
|
|
body.model_copy(update={"tools": kept}),
|
|
where=f"GET /v1/mcp/toolset/{toolset_id} on {replica} after PUT of one tool",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_toolset.update.clear_persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_clearing_the_description_with_null_reads_back_null(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
body, toolset_id = _create_toolset(client, resources, server_id)
|
|
|
|
_ = client.proxy.update_toolset(ToolsetUpdateBody(toolset_id=toolset_id, description=None))
|
|
|
|
cleared: Final = _toolset_everywhere(client, toolset_id, settled=lambda row: row.description is None)
|
|
for replica, row in cleared.items():
|
|
_assert_toolset_matches(
|
|
row,
|
|
body.model_copy(update={"description": None}),
|
|
where=f"GET /v1/mcp/toolset/{toolset_id} on {replica} after PUT description=null",
|
|
)
|
|
|
|
@pytest.mark.covers("mgmt.mcp_toolset.delete.persists")
|
|
@meta(
|
|
Subject(
|
|
domain=Domain.MCP,
|
|
route=Route.MCP,
|
|
)
|
|
)
|
|
def test_delete_removes_the_toolset_from_every_replica(
|
|
self, client: ManagementClient, resources: ResourceManager
|
|
) -> None:
|
|
_, server_id = _create_server(client, resources)
|
|
_, toolset_id = _create_toolset(client, resources, server_id)
|
|
|
|
_ = unwrap(client.proxy.delete_toolset(toolset_id))
|
|
|
|
gone: Final = client.proxy.gone_everywhere(f"/v1/mcp/toolset/{toolset_id}")
|
|
assert set(gone.values()) == {404}, f"a deleted toolset must 404 on every replica; got {dict(gone)}"
|
|
listings: Final = client.proxy.read_body_back_everywhere(
|
|
"/v1/mcp/toolset",
|
|
ToolsetListResponse,
|
|
settled=lambda rows: all(row.toolset_id != toolset_id for row in rows.root),
|
|
)
|
|
for replica, rows in listings.items():
|
|
assert all(row.toolset_id != toolset_id for row in rows.root), (
|
|
f"GET /v1/mcp/toolset on {replica} still lists the deleted toolset {toolset_id}"
|
|
)
|