- 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>
214 lines
7.6 KiB
Python
214 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""tests/docgen/test_docgen_conf.py —— DC-CONF-01~06(路径配置三形态 + 握手拒绝 + env + EMBED_URL)。
|
||
|
||
v1.2 签名适配(dev1 权威签名):
|
||
- parse_backend(vault_spec, index_spec) 两参;同形态才合法,混合抛 BackendError
|
||
- handshake(backend) 独立函数:health + 空检索,失败抛 BackendError
|
||
- get_fernet_key() 调用即抛(VSTORE_KEY 缺失 → RuntimeError)
|
||
- VstorePaths(root/vault_path/index_path/embed_url/embed_model) + set_paths 注入
|
||
|
||
DC-CONF-03/04 用受控假远端(tests/docgen/fake_remote_vstore.py,本机随机端口),不碰生产端口。
|
||
执行:bash tests/docgen/run_all.sh conf
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import socket
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
REPO = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(REPO))
|
||
VENV_PY = "/home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python"
|
||
|
||
|
||
def _importable(mod):
|
||
try:
|
||
__import__(mod)
|
||
return True
|
||
except ImportError:
|
||
return False
|
||
|
||
|
||
def _free_port():
|
||
with socket.socket() as s:
|
||
s.bind(("127.0.0.1", 0))
|
||
return s.getsockname()[1]
|
||
|
||
|
||
def _start_fake_remote(port, reject=False):
|
||
proc = subprocess.Popen(
|
||
[VENV_PY, str(REPO / "tests/docgen/fake_remote_vstore.py"), "--port", str(port)]
|
||
+ (["--reject"] if reject else []),
|
||
stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL,
|
||
)
|
||
import time
|
||
|
||
for _ in range(50):
|
||
time.sleep(0.1)
|
||
try:
|
||
# 就绪探测=端口可连(reject 模式 /health 本就返回 404,不能以 200 判定)
|
||
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
||
return proc
|
||
except OSError:
|
||
continue
|
||
proc.terminate()
|
||
pytest.skip("假远端启动失败(本组前置环境)")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DC-CONF-01/02:裸路径与 file:// → LocalBackend(两参)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_DC_CONF_01_bare_path(tmp_path):
|
||
if not _importable("vstore.backend"):
|
||
pytest.skip("PENDING: vstore.backend 未交付")
|
||
from vstore.backend import parse_backend
|
||
|
||
be = parse_backend(str(tmp_path / "va"), str(tmp_path / "ix"))
|
||
assert be.kind == "local"
|
||
assert Path(be.vault_path) == tmp_path / "va"
|
||
assert Path(be.index_path) == tmp_path / "ix"
|
||
|
||
|
||
def test_DC_CONF_02_file_url(tmp_path):
|
||
if not _importable("vstore.backend"):
|
||
pytest.skip("PENDING: vstore.backend 未交付")
|
||
from vstore.backend import parse_backend
|
||
|
||
be = parse_backend(
|
||
"file://" + str(tmp_path / "va"),
|
||
"file://" + str(tmp_path / "ix"),
|
||
)
|
||
assert be.kind == "local" # file:// 不算远程
|
||
assert str(be.vault_path) == str(tmp_path / "va") # 前缀剥掉
|
||
|
||
|
||
def test_DC_CONF_02b_mixed_forms_rejected(tmp_path):
|
||
"""混合形态(本地 + 远程)抛 BackendError(dev1 权威签名)。"""
|
||
if not _importable("vstore.backend"):
|
||
pytest.skip("PENDING: vstore.backend 未交付")
|
||
from vstore.backend import BackendError, parse_backend
|
||
|
||
with pytest.raises(BackendError):
|
||
parse_backend(str(tmp_path / "va"), "http://127.0.0.1:1/ix")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DC-CONF-03:http(s):// URL → RemoteBackend + 真实受控握手通过
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_DC_CONF_03_remote_backend_real_handshake():
|
||
if not _importable("vstore.backend"):
|
||
pytest.skip("PENDING: vstore.backend 未交付")
|
||
from vstore.backend import handshake, parse_backend
|
||
|
||
port = _free_port()
|
||
proc = _start_fake_remote(port)
|
||
try:
|
||
be = parse_backend(f"http://127.0.0.1:{port}", f"http://127.0.0.1:{port}")
|
||
assert be.kind == "remote"
|
||
handshake(be) # 不应抛异常(受控远端正常)
|
||
finally:
|
||
proc.terminate()
|
||
proc.wait(timeout=10)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DC-CONF-04:远程握手失败 → BackendError(拒绝启动)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_DC_CONF_04_handshake_reject():
|
||
if not _importable("vstore.backend"):
|
||
pytest.skip("PENDING: vstore.backend 未交付")
|
||
from vstore.backend import BackendError, handshake, parse_backend
|
||
|
||
port = _free_port()
|
||
proc = _start_fake_remote(port, reject=True)
|
||
try:
|
||
be = parse_backend(f"http://127.0.0.1:{port}", f"http://127.0.0.1:{port}")
|
||
with pytest.raises(BackendError):
|
||
handshake(be)
|
||
finally:
|
||
proc.terminate()
|
||
proc.wait(timeout=10)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DC-CONF-05:缺 VSTORE_KEY → get_fernet_key() 调用即抛
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_DC_CONF_05_missing_key(monkeypatch):
|
||
if not _importable("vstore.settings"):
|
||
pytest.skip("PENDING: vstore.settings 未交付")
|
||
from vstore.settings import get_fernet_key
|
||
|
||
monkeypatch.delenv("VSTORE_KEY", raising=False)
|
||
with pytest.raises(RuntimeError) as ei:
|
||
get_fernet_key()
|
||
assert "VSTORE_KEY" in str(ei.value)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# DC-CONF-06:VSTORE_EMBED_URL 可配置(集群化预留:embed_batch 打到配置地址)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_DC_CONF_06_embed_url_configurable():
|
||
if not _importable("vstore.embed"):
|
||
pytest.skip("PENDING: vstore.embed 未交付")
|
||
import json
|
||
import threading
|
||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||
|
||
import vstore.embed as emod
|
||
import vstore.settings as smod
|
||
|
||
hits = []
|
||
|
||
class Handler(BaseHTTPRequestHandler):
|
||
def do_POST(self):
|
||
hits.append(self.path)
|
||
length = int(self.headers.get("Content-Length") or 0)
|
||
if length:
|
||
self.rfile.read(length)
|
||
body = json.dumps(
|
||
{"model": "bge-m3:567m", "embeddings": [[0.1] * 1024]}
|
||
).encode("utf-8")
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Content-Length", str(len(body)))
|
||
self.end_headers()
|
||
self.wfile.write(body)
|
||
|
||
def log_message(self, *args):
|
||
pass
|
||
|
||
srv = HTTPServer(("127.0.0.1", 0), Handler)
|
||
port = srv.server_address[1]
|
||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||
try:
|
||
# dev1 权威注入:set_paths(VstorePaths 含 embed_url)
|
||
smod.set_paths(
|
||
smod.VstorePaths(
|
||
root="/tmp/docgen-conf-root",
|
||
vault_path="/tmp/docgen-conf-root/vault",
|
||
index_path="/tmp/docgen-conf-root/index",
|
||
embed_url=f"http://127.0.0.1:{port}",
|
||
embed_model="bge-m3:567m",
|
||
)
|
||
)
|
||
try:
|
||
res = emod.embed_batch(["占位文本一", "占位文本二"])
|
||
finally:
|
||
smod.set_paths(smod.VstorePaths()) # 恢复默认(env 驱动)配置
|
||
assert hits, "embed_batch 未向配置地址发出 HTTP 请求"
|
||
assert "/api/embed" in hits[0], f"请求路径异常: {hits[0]}"
|
||
assert all("11434" not in h for h in hits), "请求打到了默认 11434 而非配置地址"
|
||
assert len(res) == 2
|
||
finally:
|
||
srv.shutdown()
|