litellm/tests/e2e/mcp/test_mcp_datadog_e2e.py
mubashir1osmani fdf380d0e3
test(e2e): harden stage flakes for batches, UI, and MCP (#33831)
* test(e2e): harden stage flakes for batches, UI, and MCP

Unique batch model names avoid load-balancing onto stale azure-batch
deployments that still pointed at the retired gpt-4.1-mini-batch, which
only the managed/unified path was hitting. Retry batch retrieve on 500
and /ui/api-keys navigation on ERR_ABORTED. Skip the MCP key-access suite
when the compose-only mcp-upstream is unreachable on stage k8s

* test(e2e): cover Datadog remote MCP via search_datadog_logs

Register the regional Datadog MCP endpoint with DD-API-KEY /
DD-APPLICATION-KEY static headers (CI-safe header auth; browser OAuth is
not headless-automatable). Seed a chat completion marked e2e-datadog-mcp-*,
assert the proxy shipped it, list tools, call search_datadog_logs for the
marker, and delete the server on teardown. Math-upstream key-access tests
only skip when that compose service is unreachable

* test(e2e): drop compose math MCP upstream; use Datadog only

Key-access denial and happy-path MCP e2e both register the real regional
Datadog remote MCP server with DD-API-KEY / DD-APPLICATION-KEY headers.
Remove the mcp-upstream compose service and FastMCP add/multiply fixture

* docs(e2e): require real Datadog MCP for all mcp suite tests

Document that tests/e2e/mcp must register via datadog_mcp helpers against
mcp.<site>/v1/mcp and must not introduce compose or fake MCP upstreams

* chore: restore mcp_e2e_upstream_server.py

Keep the FastMCP fixture file; e2e no longer wires it in compose, but the
module itself is not part of the Datadog-only cleanup

* fix(e2e): load tests/e2e/.env and fix datadog_reader importlib load

pytest on the host never inherited compose env_file keys, so DD_API_KEY
stayed empty. load_dotenv tests/e2e/.env in e2e_config. Register the
dynamically loaded datadog_reader module in sys.modules so dataclasses
do not crash under Python 3.12

* test(e2e/batches): harden azure/vertex unified lifecycle flakes

Put the provider deployment name in every JSONL body so Azure does not
depend on a perfect model rewrite. Retry create/retrieve/cancel on
transient statuses with backoff. Drop cancel assertions for azure and
vertex (registry only has a shared basic cell; create+retrieve prove
routing, cancel stays best-effort cleanup)

* test(e2e/ui): treat api-keys shell as success after SPA ERR_ABORTED

Post-login client redirects abort the first /ui/api-keys/ goto on stage.
Wait off /ui/login after cookie set, then accept the page once Create New
Key is visible even if goto raised ERR_ABORTED

* test(e2e): drop flaky key models dropdown Playwright suite

API management e2e already covers key generate/update persistence. The
UI Models-dropdown sentinel cases only added SPA ERR_ABORTED noise and
no unique product signal. Remove the suite and unused browser fixtures
2026-07-18 19:11:54 +00:00

108 lines
4 KiB
Python

"""Live e2e: the proxy brokers the real Datadog remote MCP server.
Seeds a chat completion whose prompt carries a unique `e2e-datadog-mcp-*`
marker so the proxy's DataDogLogger ships a StandardLoggingPayload the org can
search. Registers the regional Datadog MCP endpoint with DD_API_KEY /
DD_APP_KEY as static headers (Datadog's documented CI/header auth). A key
granted that server lists tools, calls search_datadog_logs for the marker, and
the response must contain it. The dual read via datadog_reader proves the log
is also in the Logs Search API. The MCP server row is deleted on teardown.
"""
from __future__ import annotations
import pytest
from conftest import DdLogsReader
from datadog_mcp import SEARCH_LOGS_TOOL, assert_dd_mcp_creds, register_datadog_mcp
from e2e_config import CHEAP_ANTHROPIC_MODEL, DD_SEARCH_FROM, unique_marker
from e2e_http import NoBody, unwrap
from lifecycle import ResourceManager
from mcp_client import McpClient
from models import ChatBody, ChatMessage
from proxy_client import ProxyClient
pytestmark = pytest.mark.e2e
DD_LOGGER_NAME = "DataDogLogger"
MARKER_PREFIX = "e2e-datadog-mcp-"
def _assert_datadog_logger_active(proxy: ProxyClient) -> None:
result = proxy.probe("/health/readiness/details", params=NoBody())
assert result.status_code == 200, (
f"/health/readiness/details must answer 200, got {result.status_code}: {result.body[:300]}"
)
assert DD_LOGGER_NAME in result.body, (
f"the proxy must report the {DD_LOGGER_NAME} callback active "
f"(callbacks + DD_* env); got: {result.body[:400]}"
)
def _seed_completion(proxy: ProxyClient, *, key: str, marker: str) -> None:
body = ChatBody(
model=CHEAP_ANTHROPIC_MODEL,
messages=[ChatMessage(role="user", content=f"reply with one word {marker}")],
max_tokens=16,
)
unwrap(proxy.chat(key, body))
class TestDatadogMcpRoundTrip:
@pytest.mark.covers("mcp.list_tools.api_key.succeeds", "mcp.call_tool.api_key.succeeds")
def test_search_logs_finds_seeded_completion(
self,
client: McpClient,
dd_logs: DdLogsReader,
resources: ResourceManager,
) -> None:
assert_dd_mcp_creds()
_assert_datadog_logger_active(client.proxy)
server_id = register_datadog_mcp(client, resources)
marker = f"{MARKER_PREFIX}{unique_marker()}"
key = client.generate_key(
user_id=f"e2e-dd-mcp-{unique_marker()}",
mcp_servers=[server_id],
models=[CHEAP_ANTHROPIC_MODEL],
)
resources.defer(lambda: client.proxy.delete_key(key))
_seed_completion(client.proxy, key=key, marker=marker)
shipped = dd_logs.poll_events_for_marker(marker)
assert shipped, (
f"proxy DataDogLogger never shipped a log containing {marker!r} "
"within the poll deadline; MCP search would have nothing to find"
)
tools = unwrap(client.list_tools(key))
tool_name = tools.tool_name_containing(server_id, SEARCH_LOGS_TOOL)
assert tool_name is not None, (
f"granted key never saw {SEARCH_LOGS_TOOL} on server {server_id}; "
f"tools={tools.tool_names_for_server(server_id)}"
)
call = unwrap(
client.call_tool(
key,
server_id=server_id,
name=tool_name,
arguments={
"query": marker,
"from": DD_SEARCH_FROM,
"to": "now",
"max_tokens": 5000,
"telemetry": {
"intent": "e2e assert seeded litellm completion log is searchable via MCP"
},
},
)
)
assert call.is_error is not True, f"search_datadog_logs errored: {call}"
body = call.all_text
assert marker in body, (
f"search_datadog_logs response must include the seeded marker {marker!r}; "
f"got: {body[:800]!r}"
)