forked from admin/PlantAI-rag-new
- vstore 独立安装包(8094):解析/嵌入/加密 vault/faiss 检索,存储路径支持本地 file 与 http(s):// URL,启动握手校验,集群化预留(embed 独立模块) - BFF docgen 路由:上传入库/generate/编辑/导出/下载/主库管理,意图护栏(与前端同词典),vstore 失败兜底,quota 507 与 TTL 惰性清理 - 前端:自然触发意图路由、拖拽上传 chips、能力徽标、右侧文档 TAB(WYSIWYG 编辑/prompt 修改/PDF+DOCX 导出下载)、右栏拖宽+手柄仿左栏 - 批量导入工具 docgen_import.py(并发 8、导入即加密+删源明文) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
164 lines
6.3 KiB
Python
164 lines
6.3 KiB
Python
"""tooling/scripts/test_docgen_import.py —— 批量导入工具单测(P2,本地运行,不连 vstore)。
|
||
|
||
用 httpx.MockTransport 注入假 vstore(ingest / rebuild-index 两个端点),
|
||
覆盖:metadata.csv 映射、并发 ingest、--dry-run 不删目录、部分失败不删目录、
|
||
成功后删除源明文目录、--keep-source 保留。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
_RealClient = httpx.Client # 捕获原实现(必须在 monkeypatch httpx.Client 之前)
|
||
|
||
SCRIPT = Path(__file__).resolve().parent / "docgen_import.py"
|
||
|
||
# 把脚本加载为模块(tooling 不进 sdk 编译链,测试独立跑)
|
||
import importlib.util as _ilu
|
||
|
||
_spec = _ilu.spec_from_file_location("docgen_import", SCRIPT)
|
||
docgen_import = _ilu.module_from_spec(_spec)
|
||
_spec.loader.exec_module(docgen_import)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 假 vstore:记录 ingest 请求,成功返回 document_id
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class FakeVstore:
|
||
def __init__(self):
|
||
self.ingested: list[dict] = []
|
||
self.rebuilds = 0
|
||
|
||
def handler(self, request: httpx.Request) -> httpx.Response:
|
||
if request.url.path == "/rebuild-index":
|
||
self.rebuilds += 1
|
||
return httpx.Response(200, json={"rebuilt": True})
|
||
if request.url.path == "/ingest":
|
||
# multipart body 里取 file 名(解析 bytes 中 filename=)
|
||
body = request.content
|
||
name = "unknown"
|
||
if b'filename="' in body:
|
||
name = body.split(b'filename="')[1].split(b'"')[0].decode("utf-8", "replace")
|
||
self.ingested.append(
|
||
{"name": name, "fields": dict(request.url.params) if False else {}}
|
||
)
|
||
# multipart 表单字段在 body 里;粗略提取 as_template 便于断言
|
||
as_template = b"as_template" in body
|
||
self.ingested[-1]["as_template_field_present"] = as_template
|
||
return httpx.Response(200, json={"document_id": f"doc-{len(self.ingested)}"})
|
||
return httpx.Response(404, json={"detail": "not found"})
|
||
|
||
|
||
def _client(fake: FakeVstore) -> httpx.Client:
|
||
return _RealClient(transport=httpx.MockTransport(fake.handler), base_url="http://vstore.test")
|
||
|
||
|
||
def _make_source_dir(tmp_path: Path, files: dict[str, str] | None = None,
|
||
csv_content: str | None = None) -> Path:
|
||
src = tmp_path / "src"
|
||
src.mkdir()
|
||
for rel, content in (files or {"a.md": "内容A"}).items():
|
||
p = src / rel
|
||
p.parent.mkdir(parents=True, exist_ok=True)
|
||
p.write_text(content, encoding="utf-8")
|
||
if csv_content is not None:
|
||
(src / "metadata.csv").write_text(csv_content, encoding="utf-8")
|
||
return src
|
||
|
||
|
||
def _run_main(argv: list[str], monkeypatch) -> int:
|
||
import docgen_import as di
|
||
|
||
monkeypatch.setattr(sys, "argv", ["docgen_import.py", *argv])
|
||
try:
|
||
return di.main()
|
||
except SystemExit as exc:
|
||
code = exc.code
|
||
if code is None:
|
||
return 0
|
||
if isinstance(code, int):
|
||
return code
|
||
print(code, file=sys.stderr) # fail() 打印的信息
|
||
return 2
|
||
|
||
|
||
def test_dry_run_lists_files_and_keeps_source(tmp_path, monkeypatch):
|
||
src = _make_source_dir(tmp_path, {"a.md": "内容A", "sub/b.txt": "内容B"},
|
||
csv_content="file,name,as_template\na.md,资料甲,false\n")
|
||
code = _run_main(
|
||
["--dir", str(src), "--dry-run", "--vstore", "http://vstore.test"], monkeypatch
|
||
)
|
||
assert code == 0
|
||
assert src.is_dir() # dry-run 不删目录
|
||
assert (src / "a.md").exists()
|
||
|
||
|
||
def test_ingest_ok_and_source_removed(tmp_path, monkeypatch):
|
||
src = _make_source_dir(tmp_path, {"a.md": "内容A"})
|
||
fake = FakeVstore()
|
||
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
||
code = _run_main(["--dir", str(src), "--vstore", "http://vstore.test",
|
||
"--workers", "2"], monkeypatch)
|
||
assert code == 0
|
||
assert len(fake.ingested) == 1
|
||
assert fake.ingested[0]["name"] == "a.md"
|
||
assert fake.rebuilds == 1
|
||
assert not src.exists() # 导入完成删除源明文目录
|
||
|
||
|
||
def test_keep_source_retains_dir(tmp_path, monkeypatch):
|
||
src = _make_source_dir(tmp_path, {"a.md": "内容A"})
|
||
fake = FakeVstore()
|
||
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
||
code = _run_main(["--dir", str(src), "--keep-source"], monkeypatch)
|
||
assert code == 0
|
||
assert src.is_dir()
|
||
assert (src / "a.md").exists()
|
||
|
||
|
||
def test_metadata_csv_mapping(tmp_path, monkeypatch):
|
||
src = _make_source_dir(tmp_path, {"a.md": "内容A"},
|
||
csv_content="file,name,as_template\na.md,资料甲,false\n")
|
||
fake = FakeVstore()
|
||
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
||
code = _run_main(["--dir", str(src)], monkeypatch)
|
||
assert code == 0
|
||
# as_template=false 会以 "false" 字符串出现在 multipart 字段里
|
||
body_fields = fake.ingested[0]
|
||
assert body_fields["name"] == "a.md"
|
||
|
||
|
||
def test_partial_failure_keeps_source(tmp_path, monkeypatch):
|
||
src = _make_source_dir(tmp_path, {"a.md": "内容A", "b.md": "内容B"})
|
||
fake = FakeVstore()
|
||
|
||
def flaky(request: httpx.Request) -> httpx.Response:
|
||
if b'filename="b.md"' in request.content:
|
||
return httpx.Response(500, json={"detail": "boom"})
|
||
return fake.handler(request)
|
||
|
||
monkeypatch.setattr(
|
||
docgen_import.httpx, "Client",
|
||
lambda **kw: _RealClient(transport=httpx.MockTransport(flaky), base_url="http://vstore.test"),
|
||
)
|
||
code = _run_main(["--dir", str(src)], monkeypatch)
|
||
assert code == 2 # 有失败 → 非零退出
|
||
assert src.is_dir() # 失败不删源目录
|
||
assert fake.rebuilds == 0 # 失败不重建索引
|
||
|
||
|
||
def test_missing_dir_fails(tmp_path, monkeypatch):
|
||
code = _run_main(["--dir", str(tmp_path / "nope")], monkeypatch)
|
||
assert code == 2
|
||
|
||
|
||
def test_collect_skips_unsupported_and_metadata_csv(tmp_path):
|
||
src = _make_source_dir(tmp_path, {"a.md": "x", "b.exe": "x"},
|
||
csv_content="file,name\na.md,甲\n")
|
||
files = docgen_import.collect_files(src)
|
||
assert [p.name for p in files] == ["a.md"]
|