Files
jerryandClaude Fable 5 5888e1d5c1 test(docgen): 测试治理资产——131 条验收用例 + 三轮留痕 + E2E/回归套件
- tests/docgen/:DC-UNIT/API/CONF/E2E/UI/REGRESSION/PERF 分组用例与可复跑脚本,报告逐条留痕(R1-R3 全记录)
- 意图 30 条前后端同表双跑(pytest + node)
- Playwright headless:docgen-ui/docgen-regression spec + b3 benchpanel 第 5 TAB 断言更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:13:47 +08:00

177 lines
6.6 KiB
Python
Raw Permalink 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.
"""test_client.py —— vstore HTTP 客户端(MockTransport,不连真 vstore)。"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import httpx # noqa: E402
import pytest # noqa: E402
from app.services.docgen import client as cli # noqa: E402
def _client_with(handler) -> httpx.AsyncClient:
return httpx.AsyncClient(transport=httpx.MockTransport(handler), base_url="http://vstore.test")
@pytest.fixture(autouse=True)
def _fast_retry(monkeypatch):
"""重试退避归零,测试秒级完成。"""
monkeypatch.setattr(cli, "RETRY_BACKOFF", (0, 0, 0))
monkeypatch.setattr(cli, "MAX_RETRIES", 2)
def _json(payload, status=200) -> httpx.Response:
return httpx.Response(status, json=payload, request=httpx.Request("GET", "http://vstore.test/x"))
# ---------------------------------------------------------------------------
# ingest
# ---------------------------------------------------------------------------
def test_ingest_success(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
assert request.method == "POST"
assert request.url.path == "/api/vstore/ingest"
return _json({"ok": True, "item": {
"document_id": "d1", "name": "n", "category": "c", "tags": "t",
"description": "d", "as_template": True, "enc_path": "vault/d1.enc",
"embed_chunks": 1,
}})
monkeypatch.setattr(cli, "_client", _client_with(handler))
result = asyncio_run(cli.ingest_bytes("a.txt", b"hello", name="n", category="c"))
assert result["ok"] is True
assert result["document_id"] == "d1"
def test_ingest_upstream_validation(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
return _json({"ok": False, "error": {"code": "VALIDATION", "message": "bad"}}, status=422)
monkeypatch.setattr(cli, "_client", _client_with(handler))
# 4xx → DocgenUnavailable(router 捕获 → VALIDATION 信封)
with pytest.raises(cli.DocgenUnavailable):
asyncio_run(cli.ingest_bytes("a.txt", b"x", name="n"))
def test_ingest_retry_then_success(monkeypatch):
calls = {"n": 0}
async def handler(request: httpx.Request) -> httpx.Response:
calls["n"] += 1
if calls["n"] <= 2:
raise httpx.ConnectError("down", request=request)
return _json({"ok": True, "item": {"document_id": "d1", "enc_path": "v"}})
monkeypatch.setattr(cli, "_client", _client_with(handler))
result = asyncio_run(cli.ingest_bytes("a.txt", b"x", name="n"))
assert result["document_id"] == "d1"
assert calls["n"] == 3 # 2 失败 + 1 成功
def test_ingest_retries_exhausted(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("down", request=request)
monkeypatch.setattr(cli, "_client", _client_with(handler))
with pytest.raises(cli.DocgenUnavailable):
asyncio_run(cli.ingest_bytes("a.txt", b"x", name="n"))
# ---------------------------------------------------------------------------
# search / get_document / delete / rebuild / health
# ---------------------------------------------------------------------------
def test_search_documents_ok(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/vstore/search"
assert request.url.params["q"] == "小麦"
return _json({"ok": True, "mode": "vector", "items": [{"id": "t1", "name": "模版"}]})
monkeypatch.setattr(cli, "_client", _client_with(handler))
items = asyncio_run(cli.search_documents("小麦", as_template=True, top_k=20))
assert items == [{"id": "t1", "name": "模版"}]
def test_search_documents_down_returns_empty(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("down", request=request)
monkeypatch.setattr(cli, "_client", _client_with(handler))
# 检索失败 → DocgenUnavailable(router 兜底 → 空模版集)
with pytest.raises(cli.DocgenUnavailable):
asyncio_run(cli.search_documents("q"))
def test_get_document_ok(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
return _json({"ok": True, "document": {"title": "T", "paragraphs": ["p"]}})
monkeypatch.setattr(cli, "_client", _client_with(handler))
doc = asyncio_run(cli.get_document("d1"))
assert doc == {"title": "T", "paragraphs": ["p"]}
def test_get_document_404_returns_none(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(404, json={"detail": "not found"},
request=httpx.Request("GET", "http://vstore.test/x"))
monkeypatch.setattr(cli, "_client", _client_with(handler))
assert asyncio_run(cli.get_document("nope")) is None
def test_get_style_ok_and_missing(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path.endswith("/style"):
return _json({"ok": True, "style_hints": {"body_font": "宋体"}})
return httpx.Response(404, request=httpx.Request("GET", "http://vstore.test/x"))
monkeypatch.setattr(cli, "_client", _client_with(handler))
assert asyncio_run(cli.get_style("d1")) == {"body_font": "宋体"}
def test_delete_document(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
assert request.method == "DELETE"
return _json({"ok": True, "removed": True})
monkeypatch.setattr(cli, "_client", _client_with(handler))
assert asyncio_run(cli.delete_document("d1"))["ok"] is True
def test_rebuild_index(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
assert request.method == "POST"
return _json({"ok": True, "count": 3})
monkeypatch.setattr(cli, "_client", _client_with(handler))
result = asyncio_run(cli.rebuild_index())
assert result["count"] == 3
def test_update_metadata_no_patch_endpoint(monkeypatch):
"""vstore V1 无 PATCH 端点:返回错误结构(router 容错忽略)。"""
result = asyncio_run(cli.update_metadata("d1", {"name": "x"}))
assert result["ok"] is False
assert result["error"]["code"] == "NOT_IMPLEMENTED"
def test_health_down_returns_error_structure(monkeypatch):
async def handler(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("down", request=request)
monkeypatch.setattr(cli, "_client", _client_with(handler))
result = asyncio_run(cli.health())
assert result["ok"] is False
assert "error" in result
def asyncio_run(coro):
import anyio
return anyio.run(lambda: coro)