Files
PlantAI-rag-new/tooling/scripts/test_docgen_import.py
T
jerryandClaude Fable 5 5db3ead73e feat(docgen): doc_writer 能力全链——vstore 独立服务 + BFF docgen 路由 + 前端文档工作台
- 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>
2026-09-03 21:13:37 +08:00

164 lines
6.3 KiB
Python
Raw 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.
"""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"]