Files
xiaozhi-esp32-server/main/xiaozhi-server/tests/plugins_func/test_web_search_outcrawl.py
T

120 lines
4.1 KiB
Python

"""Tests for the Outcrawl source of the web_search plugin.
The HTTP layer is replaced with a fake client and every request is recorded.
Nothing here touches the network.
"""
import httpx
import pytest
from plugins_func.functions import web_search as web_search_module
from plugins_func.register import Action
class FakeResponse:
def __init__(self, status_code=200, payload=None):
self.status_code = status_code
self.payload = payload
def json(self):
return self.payload
class FakeAsyncClient:
"""Stands in for httpx.AsyncClient and records what was requested."""
requests = []
response = None
def __init__(self, *args, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *exc_info):
return False
async def post(self, url, json=None, headers=None):
FakeAsyncClient.requests.append({"url": url, "json": json, "headers": headers})
return FakeAsyncClient.response
@pytest.fixture
def fake_http(monkeypatch):
"""Replace httpx.AsyncClient with the recording fake."""
FakeAsyncClient.requests = []
FakeAsyncClient.response = FakeResponse(payload={"hits": []})
monkeypatch.setattr(httpx, "AsyncClient", FakeAsyncClient)
return FakeAsyncClient
class FakeConn:
def __init__(self, web_search_config):
self.config = {"plugins": {"web_search": web_search_config}}
async def test_outcrawl_sends_bearer_key_and_limit(fake_http):
await web_search_module._search_outcrawl("oc-key", "上海天气", 5)
[request] = fake_http.requests
assert request["url"] == "https://api.outcrawl.ai/v1/search"
assert request["json"] == {"query": "上海天气", "limit": 5}
assert request["headers"]["Authorization"] == "Bearer oc-key"
assert request["headers"]["User-Agent"] == "xiaozhi-esp32-server"
@pytest.mark.parametrize(("max_results", "limit"), [(0, 1), (50, 20)])
async def test_outcrawl_clamps_limit(fake_http, max_results, limit):
await web_search_module._search_outcrawl("oc-key", "q", max_results)
assert fake_http.requests[0]["json"]["limit"] == limit
async def test_outcrawl_formats_titles_and_snippets_without_urls(fake_http):
fake_http.response = FakeResponse(
payload={
"hits": [
{"url": "https://a.example/1", "title": "标题一", "snippet": "摘要一", "rank": 1},
{"url": "https://a.example/2", "title": None, "snippet": "", "rank": 2},
{"url": "https://a.example/3", "title": "标题三", "snippet": "摘要三", "rank": 3},
],
"usage": {"credits": "1.5"},
}
)
result = await web_search_module._search_outcrawl("oc-key", "q", 2)
assert result == "【联网搜索结果】\n1. 标题:标题一\n 摘要:摘要一\n2. 标题:无标题"
assert "https://" not in result
async def test_outcrawl_empty_hits(fake_http):
fake_http.response = FakeResponse(payload={"hits": []})
assert await web_search_module._search_outcrawl("oc-key", "q", 3) == "未找到相关搜索结果。"
@pytest.mark.parametrize(
("status_code", "message"),
[
(401, "联网搜索请求失败,请检查API Key是否正确。"),
(402, "联网搜索额度已用完,请检查Outcrawl账户余额。"),
(429, "联网搜索请求失败,请检查API Key是否正确。"),
],
)
async def test_outcrawl_error_status(fake_http, status_code, message):
fake_http.response = FakeResponse(status_code=status_code, payload={"message": "denied"})
assert await web_search_module._search_outcrawl("oc-key", "q", 3) == message
async def test_web_search_dispatches_to_outcrawl(fake_http):
fake_http.response = FakeResponse(payload={"hits": [{"url": "https://a.example", "title": "T", "snippet": "S"}]})
conn = FakeConn({"provider": "Outcrawl", "api_key": "oc-key", "max_results": 3})
response = await web_search_module.web_search(conn, "q")
assert response.action == Action.REQLLM
assert response.result == "【联网搜索结果】\n1. 标题:T\n 摘要:S"
assert fake_http.requests[0]["json"] == {"query": "q", "limit": 3}