Files
PlantAI-rag-new/tooling/scripts/test_docgen_import.py
T

164 lines
6.3 KiB
Python
Raw Normal View History

"""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"]