- 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>
177 lines
6.6 KiB
Python
177 lines
6.6 KiB
Python
"""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)
|