# -*- 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()