Files
hunjing/backend/tests/application/tool_runtime/test_web_tools.py
T
2026-08-21 16:00:13 +08:00

118 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""fetch_url executor 单元测试。
mock 边界:
- ``respx`` 模拟 httpx.AsyncClient 的 GET(不真实联网)。
- executor 直接调用,不经过 ToolRuntime(runtime 集成路径由 test_runtime 覆盖)。
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from typing import Any
import httpx
import pytest
import respx
from app.application.tool_runtime.web_tools import _fetch_url_executor
@pytest.fixture
async def http_client() -> AsyncIterator[httpx.AsyncClient]:
async with httpx.AsyncClient() as client:
yield client
def _ctx(http_client: httpx.AsyncClient, **extra: Any) -> dict[str, Any]:
"""构造 executor 上下文。"""
base: dict[str, Any] = {"http_client": http_client, "trace_id": "trace_test"}
base.update(extra)
return base
@respx.mock
async def test_fetch_url_converts_html_to_markdown(
http_client: httpx.AsyncClient,
) -> None:
"""fetch_url 把 HTML 转成 markdown 并截断。"""
html = (
"<html><head><title>T</title></head>"
"<body><h1>标题</h1><p>这是一段正文。</p>"
'<a href="https://example.com">链接</a></body></html>'
)
respx.get("https://example.com/page").mock(return_value=httpx.Response(200, text=html))
result = await _fetch_url_executor({"url": "https://example.com/page"}, _ctx(http_client))
assert result.ok
assert "标题" in result.stdout
assert "这是一段正文。" in result.stdout
# html2text 默认保留链接
assert "example.com" in result.stdout
@respx.mock
async def test_fetch_url_truncates_to_max_chars(
http_client: httpx.AsyncClient,
) -> None:
"""max_chars 截断生效。"""
long_text = "A" * 500
html = f"<html><body><p>{long_text}</p></body></html>"
respx.get("https://example.com/big").mock(return_value=httpx.Response(200, text=html))
result = await _fetch_url_executor(
{"url": "https://example.com/big", "max_chars": 50}, _ctx(http_client)
)
assert result.ok
assert "已截断" in result.stdout
assert len(result.stdout) < 500
@respx.mock
async def test_fetch_url_http_error_returns_error(
http_client: httpx.AsyncClient,
) -> None:
"""HTTP 错误返回 ok=False。"""
respx.get("https://example.com/missing").mock(
return_value=httpx.Response(404, text="not found")
)
result = await _fetch_url_executor({"url": "https://example.com/missing"}, _ctx(http_client))
assert not result.ok
assert "404" in result.stderr
async def test_fetch_url_missing_url_returns_error(
http_client: httpx.AsyncClient,
) -> None:
"""缺 url 输入时返回 ok=False。"""
result = await _fetch_url_executor({}, _ctx(http_client))
assert not result.ok
assert "url" in result.stderr
async def test_fetch_url_missing_http_client_returns_error() -> None:
"""缺注入的 http_client 时返回 ok=False。"""
result = await _fetch_url_executor({"url": "https://example.com"}, None)
assert not result.ok
assert "http_client" in result.stderr
@respx.mock
async def test_fetch_url_network_error_returns_error(
http_client: httpx.AsyncClient,
) -> None:
"""网络错误(HTTPError)返回 ok=False 而不抛异常。"""
respx.get("https://example.com/neterr").mock(
side_effect=httpx.ConnectError("connection refused")
)
result = await _fetch_url_executor({"url": "https://example.com/neterr"}, _ctx(http_client))
assert not result.ok
assert "请求失败" in result.stderr