194 lines
7.5 KiB
Python
194 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app.services.mcp_config.runtime import DuckDuckGoSearchRuntime, MicrosoftLearnMCPRuntime
|
|
|
|
|
|
def _sse(payload: dict) -> httpx.Response:
|
|
body = f"event: message\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
return httpx.Response(
|
|
200,
|
|
headers={"content-type": "text/event-stream", "mcp-session-id": "test-session"},
|
|
text=body,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_microsoft_learn_runtime_probe_initializes_and_lists_tools() -> None:
|
|
calls: list[dict] = []
|
|
|
|
def _handler(request: httpx.Request) -> httpx.Response:
|
|
payload = json.loads(request.content.decode("utf-8"))
|
|
calls.append(payload)
|
|
if payload["method"] == "initialize":
|
|
return _sse(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": payload["id"],
|
|
"result": {
|
|
"serverInfo": {"name": "Microsoft Learn MCP Server", "version": "1.0.0"},
|
|
},
|
|
}
|
|
)
|
|
if payload["method"] == "tools/list":
|
|
assert request.headers["mcp-session-id"] == "test-session"
|
|
return _sse(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": payload["id"],
|
|
"result": {
|
|
"tools": [
|
|
{"name": "microsoft_docs_search"},
|
|
{"name": "microsoft_docs_fetch"},
|
|
]
|
|
},
|
|
}
|
|
)
|
|
raise AssertionError(f"unexpected method: {payload['method']}")
|
|
|
|
runtime = MicrosoftLearnMCPRuntime(
|
|
endpoint_url="https://learn.microsoft.com/api/mcp",
|
|
client_factory=lambda: httpx.AsyncClient(transport=httpx.MockTransport(_handler)),
|
|
)
|
|
|
|
probe = await runtime.probe()
|
|
|
|
assert probe["status"] == "ready"
|
|
assert probe["tool_names"] == ["microsoft_docs_search", "microsoft_docs_fetch"]
|
|
assert [item["method"] for item in calls] == ["initialize", "tools/list"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_microsoft_learn_runtime_search_returns_external_evidence() -> None:
|
|
def _handler(request: httpx.Request) -> httpx.Response:
|
|
payload = json.loads(request.content.decode("utf-8"))
|
|
if payload["method"] == "initialize":
|
|
return _sse(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": payload["id"],
|
|
"result": {
|
|
"serverInfo": {"name": "Microsoft Learn MCP Server", "version": "1.0.0"},
|
|
},
|
|
}
|
|
)
|
|
if payload["method"] == "tools/call":
|
|
assert payload["params"]["name"] == "microsoft_docs_search"
|
|
assert payload["params"]["arguments"]["query"] == "azure openai quickstart"
|
|
return _sse(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": payload["id"],
|
|
"result": {
|
|
"content": [
|
|
{
|
|
"type": "text",
|
|
"text": json.dumps(
|
|
{
|
|
"results": [
|
|
{
|
|
"title": "Quickstart: Azure OpenAI",
|
|
"content": "Use Azure OpenAI with official quickstart guidance.",
|
|
"contentUrl": "https://learn.microsoft.com/azure/ai-foundry/openai/chatgpt-quickstart",
|
|
}
|
|
]
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
}
|
|
]
|
|
},
|
|
}
|
|
)
|
|
raise AssertionError(f"unexpected method: {payload['method']}")
|
|
|
|
runtime = MicrosoftLearnMCPRuntime(
|
|
endpoint_url="https://learn.microsoft.com/api/mcp",
|
|
client_factory=lambda: httpx.AsyncClient(transport=httpx.MockTransport(_handler)),
|
|
)
|
|
|
|
evidence = await runtime.search(query="azure openai quickstart", result_limit=3)
|
|
|
|
assert len(evidence) == 1
|
|
assert evidence[0].source_type == "mcp_search"
|
|
assert evidence[0].title == "Quickstart: Azure OpenAI"
|
|
assert evidence[0].target_url == "https://learn.microsoft.com/azure/ai-foundry/openai/chatgpt-quickstart"
|
|
assert evidence[0].authority_score >= 0.9
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duckduckgo_runtime_probe_reports_text_and_news_tools() -> None:
|
|
runtime = DuckDuckGoSearchRuntime(ddgs_factory=lambda: SimpleNamespace())
|
|
|
|
probe = await runtime.probe()
|
|
|
|
assert probe["status"] == "ready"
|
|
assert probe["tool_names"] == ["ddg-text-search", "ddg-news-search"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duckduckgo_runtime_search_returns_news_evidence() -> None:
|
|
class _FakeDDGS:
|
|
def news(self, *, keywords: str, region: str, safesearch: str, timelimit: str | None, max_results: int): # noqa: ANN001
|
|
assert keywords == "OpenAI latest news"
|
|
assert region == "wt-wt"
|
|
assert safesearch == "moderate"
|
|
assert timelimit == "d"
|
|
assert max_results == 3
|
|
return [
|
|
{
|
|
"title": "OpenAI ships new release",
|
|
"url": "https://news.example/openai-release",
|
|
"body": "Latest OpenAI release details.",
|
|
"date": "2026-04-16T10:14:00+00:00",
|
|
"source": "Example News",
|
|
}
|
|
]
|
|
|
|
runtime = DuckDuckGoSearchRuntime(ddgs_factory=lambda: _FakeDDGS())
|
|
|
|
evidence = await runtime.search(query="OpenAI latest news", result_limit=3, query_kind="news")
|
|
|
|
assert len(evidence) == 1
|
|
assert evidence[0].source_type == "mcp_search"
|
|
assert evidence[0].source_label == "DuckDuckGo Search"
|
|
assert evidence[0].title == "OpenAI ships new release"
|
|
assert evidence[0].target_url == "https://news.example/openai-release"
|
|
assert evidence[0].raw_metadata["provider"] == "duckduckgo_backend_direct"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duckduckgo_runtime_search_falls_back_to_text_when_news_is_rate_limited() -> None:
|
|
class _FakeDDGS:
|
|
def news(self, **kwargs): # noqa: ANN003
|
|
_ = kwargs
|
|
raise RuntimeError("403 Ratelimit")
|
|
|
|
def text(self, *, keywords: str, region: str, safesearch: str, timelimit: str | None, max_results: int): # noqa: ANN001
|
|
assert keywords == "OpenAI latest news"
|
|
assert region == "wt-wt"
|
|
assert safesearch == "moderate"
|
|
assert timelimit == "w"
|
|
assert max_results == 2
|
|
return [
|
|
{
|
|
"title": "OpenAI text fallback result",
|
|
"href": "https://example.com/openai-fallback",
|
|
"body": "Fallback web result after news rate limit.",
|
|
}
|
|
]
|
|
|
|
runtime = DuckDuckGoSearchRuntime(ddgs_factory=lambda: _FakeDDGS())
|
|
|
|
evidence = await runtime.search(query="OpenAI latest news", result_limit=2, query_kind="news")
|
|
|
|
assert len(evidence) == 1
|
|
assert evidence[0].title == "OpenAI text fallback result"
|
|
assert evidence[0].target_url == "https://example.com/openai-fallback"
|
|
assert evidence[0].grounded_fields == ["ddg-text-search"]
|