# -*- coding: utf-8 -*-
"""tests/docgen/test_websearch.py —— 联网模版参考验收用例(DC-UNIT-WEB-*)。
2026-09-04 手工测试需求2:库内无匹配模版时联网搜索相似内容结构。
口径:monkeypatch websearch._make_client 注入 MockTransport,全程离线。
执行:bash tests/docgen/run_all.sh unit
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
import httpx
import pytest
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "sdk"))
from app.services.docgen import websearch # noqa: E402
pytestmark = pytest.mark.unit
BING_PAGE = (
'
'
''
''
"
"
)
RESULT_PAGE = (
"安全生产通知范文"
"关于安全生产的通知
正文……
"
"一、总体要求
二、重点任务
2.1 隐患排查
"
""
)
def _install(monkeypatch, handler, *, force_enabled=True):
"""注入 MockTransport 客户端工厂;force_enabled 绕过环境闸。"""
calls = []
async def wrapped(request):
calls.append(str(request.url))
return await handler(request)
def factory(timeout):
return httpx.AsyncClient(transport=httpx.MockTransport(wrapped))
monkeypatch.setattr(websearch, "_make_client", factory)
if force_enabled:
monkeypatch.setattr(websearch, "web_search_enabled", lambda: True)
return calls
def _run(description):
return asyncio.run(websearch.search_template_outline(description))
async def _handler_page(request):
if "bing.com" in request.url.host:
return httpx.Response(200, text=BING_PAGE)
return httpx.Response(200, text=RESULT_PAGE)
class TestWebsearch:
def test_DC_UNIT_WEB_01_outline_extracted(self, monkeypatch):
calls = _install(monkeypatch, _handler_page)
result = _run("帮我写一份关于安全生产的通知")
assert result is not None
assert result.source_url == "https://example.com/fanwen/1"
assert result.title == "安全生产通知范文"
assert result.outline == [
"关于安全生产的通知",
"一、总体要求",
"二、重点任务",
"2.1 隐患排查",
]
assert len(calls) == 2 # bing 一次 + 抓取页一次
def test_DC_UNIT_WEB_02_network_failure_none(self, monkeypatch):
async def handler(request):
raise httpx.ConnectError("boom", request=request)
_install(monkeypatch, handler)
assert _run("通知") is None
def test_DC_UNIT_WEB_03_oversize_page_none(self, monkeypatch):
big = "一
" + "x" * (websearch.MAX_RESULT_BYTES + 10)
async def handler(request):
if "bing.com" in request.url.host:
return httpx.Response(200, text=BING_PAGE)
return httpx.Response(200, text=big)
_install(monkeypatch, handler)
# 截断后标题数 < MIN_HEADINGS → None
assert _run("通知") is None
def test_DC_UNIT_WEB_04_too_few_headings_none(self, monkeypatch):
async def handler(request):
if "bing.com" in request.url.host:
return httpx.Response(200, text=BING_PAGE)
return httpx.Response(200, text="没有标题的页面
")
_install(monkeypatch, handler)
assert _run("通知") is None
def test_DC_UNIT_WEB_05_gb18030_decodes(self, monkeypatch):
page = (
"通知范文"
"关于开展检查的通知
一、检查范围
"
).encode("gb18030")
async def handler(request):
if "bing.com" in request.url.host:
return httpx.Response(200, text=BING_PAGE)
return httpx.Response(200, content=page, headers={"content-type": "text/html"})
_install(monkeypatch, handler)
result = _run("通知")
assert result is not None
assert result.outline == ["关于开展检查的通知", "一、检查范围"]
def test_DC_UNIT_WEB_06_env_gate_off(self, monkeypatch):
async def handler(request):
return httpx.Response(200, text=BING_PAGE)
calls = _install(monkeypatch, handler, force_enabled=False)
monkeypatch.setattr(websearch, "web_search_enabled", lambda: False)
assert _run("通知") is None
assert not calls # 关闸后不发任何网络请求