669 lines
23 KiB
Python
669 lines
23 KiB
Python
"""Integration tests for ``parse_docling`` with the unified sidecar pipeline.
|
|
|
|
Stubs :class:`DoclingRawClient.download_into` so no real docling-serve is
|
|
contacted; the focus is on:
|
|
|
|
- happy path: cache miss → fake bundle written → sidecar emitted with all
|
|
expected files at the spec-compliant locations
|
|
- cache hit: a pre-existing valid ``*.docling_raw/`` + manifest causes
|
|
``DoclingRawClient.download_into`` NOT to be called
|
|
- ``LIGHTRAG_FORCE_REPARSE_DOCLING=true`` forces a re-download even when
|
|
the manifest is valid
|
|
- source content swap → cache miss
|
|
- options_signature change (``DOCLING_OCR_LANG`` toggle) → cache miss
|
|
- adapter sees zero blocks → parse fails loudly (no half-baked sidecar)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from lightrag import LightRAG
|
|
from lightrag.constants import FULL_DOCS_FORMAT_LIGHTRAG
|
|
from lightrag.parser.external import (
|
|
Manifest,
|
|
ManifestFile,
|
|
compute_size_and_hash,
|
|
write_manifest,
|
|
)
|
|
from lightrag.parser.external.docling.cache import (
|
|
compute_options_signature,
|
|
snapshot_tunable_env,
|
|
)
|
|
from lightrag.parser.external.docling.client import FIXED_CONSTANTS
|
|
from lightrag.parser.base import ParseContext
|
|
from lightrag.parser.registry import get_parser
|
|
from lightrag.utils import EmbeddingFunc, Tokenizer
|
|
|
|
|
|
async def _parse_via_registry(rag, engine, doc_id, file_path, content_data):
|
|
"""Drive a parser the way the pipeline worker does (registry dispatch)."""
|
|
result = await get_parser(engine).parse(
|
|
ParseContext(rag, doc_id, file_path, content_data)
|
|
)
|
|
return result.to_dict()
|
|
|
|
|
|
class _SimpleTokenizerImpl:
|
|
def encode(self, content: str) -> list[int]:
|
|
return [ord(ch) for ch in content]
|
|
|
|
def decode(self, tokens: list[int]) -> str:
|
|
return "".join(chr(t) for t in tokens)
|
|
|
|
|
|
async def _mock_embedding(texts: list[str]) -> np.ndarray:
|
|
return np.random.rand(len(texts), 32)
|
|
|
|
|
|
async def _mock_llm(prompt: Any, **kwargs: Any) -> str:
|
|
return '{"name":"x","summary":"s","detail_description":"d"}'
|
|
|
|
|
|
def _new_rag(tmp_path: Path) -> LightRAG:
|
|
return LightRAG(
|
|
working_dir=str(tmp_path),
|
|
workspace=f"test-docling-sidecar-{tmp_path.name}",
|
|
llm_model_func=_mock_llm,
|
|
embedding_func=EmbeddingFunc(
|
|
embedding_dim=32,
|
|
max_token_size=4096,
|
|
func=_mock_embedding,
|
|
),
|
|
tokenizer=Tokenizer("mock-tokenizer", _SimpleTokenizerImpl()),
|
|
vlm_process_enable=False,
|
|
)
|
|
|
|
|
|
_FAKE_DOCLING_JSON = {
|
|
"schema_name": "DoclingDocument",
|
|
"version": "1.10.0",
|
|
"origin": {"filename": "demo.pdf", "mimetype": "application/pdf"},
|
|
"body": {
|
|
"self_ref": "#/body",
|
|
"children": [
|
|
{"$ref": "#/texts/0"},
|
|
],
|
|
"content_layer": "body",
|
|
"label": "unspecified",
|
|
},
|
|
"groups": [],
|
|
"texts": [
|
|
{
|
|
"self_ref": "#/texts/0",
|
|
"label": "section_header",
|
|
"text": "Intro",
|
|
"orig": "Intro",
|
|
"level": 1,
|
|
"content_layer": "body",
|
|
"children": [
|
|
{"$ref": "#/texts/1"},
|
|
{"$ref": "#/tables/0"},
|
|
{"$ref": "#/pictures/0"},
|
|
{"$ref": "#/texts/2"},
|
|
],
|
|
"prov": [
|
|
{
|
|
"page_no": 1,
|
|
"bbox": {
|
|
"l": 10.0,
|
|
"t": 100.0,
|
|
"r": 200.0,
|
|
"b": 80.0,
|
|
"coord_origin": "BOTTOMLEFT",
|
|
},
|
|
"charspan": [0, 5],
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"self_ref": "#/texts/1",
|
|
"label": "text",
|
|
"text": "Body paragraph.",
|
|
"orig": "Body paragraph.",
|
|
"content_layer": "body",
|
|
"prov": [
|
|
{
|
|
"page_no": 1,
|
|
"bbox": {
|
|
"l": 10.0,
|
|
"t": 60.0,
|
|
"r": 200.0,
|
|
"b": 40.0,
|
|
"coord_origin": "BOTTOMLEFT",
|
|
},
|
|
"charspan": [0, 15],
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"self_ref": "#/texts/2",
|
|
"label": "formula",
|
|
"text": "E = mc^2",
|
|
"orig": "E = mc^2",
|
|
"content_layer": "body",
|
|
"prov": [],
|
|
},
|
|
],
|
|
"tables": [
|
|
{
|
|
"self_ref": "#/tables/0",
|
|
"label": "table",
|
|
"content_layer": "body",
|
|
"data": {
|
|
"num_rows": 2,
|
|
"num_cols": 2,
|
|
"grid": [
|
|
[{"text": "h1"}, {"text": "h2"}],
|
|
[{"text": "a"}, {"text": "b"}],
|
|
],
|
|
},
|
|
"prov": [],
|
|
}
|
|
],
|
|
"pictures": [
|
|
{
|
|
"self_ref": "#/pictures/0",
|
|
"label": "picture",
|
|
"content_layer": "body",
|
|
"image": {"uri": "artifacts/img_000000.png", "mimetype": "image/png"},
|
|
"prov": [],
|
|
}
|
|
],
|
|
"key_value_items": [],
|
|
"form_items": [],
|
|
"pages": {"1": {"size": {"width": 612.0, "height": 792.0}, "page_no": 1}},
|
|
}
|
|
|
|
|
|
def _install_fake_download(monkeypatch: pytest.MonkeyPatch) -> dict[str, int]:
|
|
"""Replace ``DoclingRawClient.download_into`` with a recorder that
|
|
writes a synthetic raw bundle and a valid manifest."""
|
|
import lightrag.parser.external.docling.client as client_mod
|
|
|
|
counters = {"calls": 0}
|
|
|
|
async def _fake_download(self, raw_dir: Path, source_file_path: Path, **_kwargs):
|
|
counters["calls"] += 1
|
|
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
main_json = raw_dir / "demo.json"
|
|
main_json.write_text(json.dumps(_FAKE_DOCLING_JSON), encoding="utf-8")
|
|
(raw_dir / "demo.md").write_text("# fake md", encoding="utf-8")
|
|
art = raw_dir / "artifacts"
|
|
art.mkdir(exist_ok=True)
|
|
(art / "img_000000.png").write_bytes(b"\x89PNG fake")
|
|
|
|
src_size, src_hash = compute_size_and_hash(source_file_path)
|
|
crit_size, crit_hash = compute_size_and_hash(main_json)
|
|
others = [
|
|
ManifestFile(path="demo.md", size=(raw_dir / "demo.md").stat().st_size),
|
|
ManifestFile(
|
|
path="artifacts/img_000000.png",
|
|
size=(art / "img_000000.png").stat().st_size,
|
|
),
|
|
]
|
|
options_signature = compute_options_signature(
|
|
tunable_env=snapshot_tunable_env(),
|
|
fixed_constants=FIXED_CONSTANTS,
|
|
)
|
|
manifest = Manifest(
|
|
engine="docling",
|
|
source_content_hash=src_hash,
|
|
source_size_bytes=src_size,
|
|
source_filename_at_parse=source_file_path.name,
|
|
critical_file=ManifestFile(
|
|
path="demo.json", size=crit_size, sha256=crit_hash
|
|
),
|
|
files=others,
|
|
total_size_bytes=crit_size + sum(f.size for f in others),
|
|
task_id=f"fake-{counters['calls']}",
|
|
endpoint_signature="http://docling.test",
|
|
options_signature=options_signature,
|
|
extras={"fixed_constants": dict(FIXED_CONSTANTS)},
|
|
downloaded_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
)
|
|
write_manifest(raw_dir, manifest)
|
|
return manifest
|
|
|
|
monkeypatch.setattr(client_mod.DoclingRawClient, "download_into", _fake_download)
|
|
return counters
|
|
|
|
|
|
def _stub_pipeline(monkeypatch: pytest.MonkeyPatch, rag: LightRAG, src: Path) -> None:
|
|
"""Common pipeline-level stubs: avoid moving the source file and pin
|
|
the file resolver to the synthetic path."""
|
|
|
|
async def _noop_archive(_p: str) -> None:
|
|
return None
|
|
|
|
import lightrag.pipeline as pipeline_module
|
|
|
|
monkeypatch.setattr(
|
|
pipeline_module,
|
|
"archive_docx_source_after_full_docs_sync",
|
|
_noop_archive,
|
|
)
|
|
monkeypatch.setattr(rag, "_resolve_source_file_for_parser", lambda _p: str(src))
|
|
|
|
|
|
def _seed_doc_status(rag: LightRAG, doc_id: str) -> Any:
|
|
return rag.doc_status.upsert(
|
|
{
|
|
doc_id: {
|
|
"status": "PARSING",
|
|
"content_summary": "",
|
|
"content_length": 0,
|
|
"chunks_count": 0,
|
|
"chunks_list": [],
|
|
"created_at": "2026-05-18T00:00:00+00:00",
|
|
"updated_at": "2026-05-18T00:00:00+00:00",
|
|
"file_path": "demo.pdf",
|
|
"track_id": "trk",
|
|
"content_hash": "",
|
|
"metadata": {},
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_emits_compliant_sidecar(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
counters = _install_fake_download(monkeypatch)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
parsed = await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1
|
|
|
|
parsed_dir = Path(parsed["blocks_path"]).parent
|
|
assert parsed["parse_format"] == FULL_DOCS_FORMAT_LIGHTRAG
|
|
assert parsed_dir.name == "demo.pdf.parsed"
|
|
|
|
files = {p.name for p in parsed_dir.iterdir() if p.is_file()}
|
|
assert "demo.blocks.jsonl" in files
|
|
assert "demo.tables.json" in files
|
|
assert "demo.drawings.json" in files
|
|
assert "demo.equations.json" in files
|
|
assert (parsed_dir / "demo.blocks.assets").is_dir()
|
|
assert (parsed_dir / "demo.blocks.assets" / "img_000000.png").is_file()
|
|
|
|
blocks_raw = (parsed_dir / "demo.blocks.jsonl").read_text()
|
|
lines = blocks_raw.splitlines()
|
|
meta = json.loads(lines[0])
|
|
rows = [json.loads(line) for line in lines[1:]]
|
|
assert meta["parse_engine"] == "docling"
|
|
assert meta["bbox_attributes"] == {"origin": "LEFTBOTTOM"}
|
|
assert "max" not in meta["bbox_attributes"]
|
|
assert "page_sizes" not in meta["bbox_attributes"]
|
|
assert meta["table_file"] is True
|
|
assert meta["drawing_file"] is True
|
|
assert meta["equation_file"] is True
|
|
# No label="title" in the fixture (matches the typical PDF case
|
|
# where docling produces only section_headers) → doc_title falls
|
|
# back to the document stem.
|
|
assert meta["doc_title"] == "demo"
|
|
|
|
contents = " ".join(row.get("content", "") for row in rows)
|
|
assert '<table id="tb-' in contents
|
|
assert "<drawing" in contents
|
|
assert "<equation" in contents
|
|
|
|
# Raw bundle preserved next to sidecar
|
|
raw_dir = parsed_dir.parent / "demo.pdf.docling_raw"
|
|
assert (raw_dir / "_manifest.json").is_file()
|
|
assert (raw_dir / "demo.json").is_file()
|
|
assert (raw_dir / "demo.md").is_file()
|
|
assert (raw_dir / "artifacts" / "img_000000.png").is_file()
|
|
|
|
# Drawing path correctly resolved
|
|
drawings = json.loads((parsed_dir / "demo.drawings.json").read_text())[
|
|
"drawings"
|
|
]
|
|
(drawing_id, drawing_item) = next(iter(drawings.items()))
|
|
assert drawing_id.startswith("im-")
|
|
assert drawing_item["path"] == "demo.blocks.assets/img_000000.png"
|
|
|
|
# Table self_ref propagated
|
|
tables = json.loads((parsed_dir / "demo.tables.json").read_text())["tables"]
|
|
(_, table_item) = next(iter(tables.items()))
|
|
assert table_item.get("self_ref") == "#/tables/0"
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_cache_hit_skips_download(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
counters = _install_fake_download(monkeypatch)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1, "cache hit must not re-download"
|
|
|
|
monkeypatch.setenv("LIGHTRAG_FORCE_REPARSE_DOCLING", "true")
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 2
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_cache_invalidates_on_source_change(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
counters = _install_fake_download(monkeypatch)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1
|
|
|
|
data = src.read_bytes()
|
|
src.write_bytes(b"\x00" + data[1:])
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 2
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_options_signature_invalidates_cache(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
counters = _install_fake_download(monkeypatch)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1
|
|
|
|
# Flip an env var that participates in the options signature
|
|
monkeypatch.setenv("DOCLING_OCR_LANG", "en,zh")
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 2, (
|
|
"DOCLING_OCR_LANG change must invalidate the bundle cache"
|
|
)
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_endpoint_signature_invalidates_cache(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
counters = _install_fake_download(monkeypatch)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 1
|
|
|
|
# Pointing at a different docling-serve instance must not silently
|
|
# reuse a bundle that was produced by the previous one.
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling-other.test")
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
assert counters["calls"] == 2, (
|
|
"DOCLING_ENDPOINT change must invalidate the bundle cache"
|
|
)
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|
|
|
|
|
|
@pytest.mark.offline
|
|
def test_parse_docling_zero_blocks_raises(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""When the docling bundle yields no body blocks (e.g. everything was
|
|
classified as furniture/background) ``parse_docling`` must fail loudly
|
|
so the document is marked failed — never persist a half-baked sidecar.
|
|
"""
|
|
|
|
async def _run() -> None:
|
|
monkeypatch.setenv("DOCLING_ENDPOINT", "http://docling.test")
|
|
|
|
# Install a fake download that writes a valid bundle whose body has
|
|
# no children — the adapter then produces zero IR blocks.
|
|
import lightrag.parser.external.docling.client as client_mod
|
|
|
|
empty_json: dict[str, Any] = {
|
|
"schema_name": "DoclingDocument",
|
|
"version": "1.10.0",
|
|
"origin": {"filename": "demo.pdf", "mimetype": "application/pdf"},
|
|
"body": {
|
|
"self_ref": "#/body",
|
|
"children": [],
|
|
"content_layer": "body",
|
|
"label": "unspecified",
|
|
},
|
|
"groups": [],
|
|
"texts": [],
|
|
"tables": [],
|
|
"pictures": [],
|
|
"key_value_items": [],
|
|
"form_items": [],
|
|
"pages": {},
|
|
}
|
|
|
|
async def _fake_download(
|
|
self, raw_dir: Path, source_file_path: Path, **_kwargs
|
|
):
|
|
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
main_json = raw_dir / "demo.json"
|
|
main_json.write_text(json.dumps(empty_json), encoding="utf-8")
|
|
(raw_dir / "demo.md").write_text("# empty", encoding="utf-8")
|
|
|
|
src_size, src_hash = compute_size_and_hash(source_file_path)
|
|
crit_size, crit_hash = compute_size_and_hash(main_json)
|
|
others = [
|
|
ManifestFile(path="demo.md", size=(raw_dir / "demo.md").stat().st_size),
|
|
]
|
|
options_signature = compute_options_signature(
|
|
tunable_env=snapshot_tunable_env(),
|
|
fixed_constants=FIXED_CONSTANTS,
|
|
)
|
|
manifest = Manifest(
|
|
engine="docling",
|
|
source_content_hash=src_hash,
|
|
source_size_bytes=src_size,
|
|
source_filename_at_parse=source_file_path.name,
|
|
critical_file=ManifestFile(
|
|
path="demo.json", size=crit_size, sha256=crit_hash
|
|
),
|
|
files=others,
|
|
total_size_bytes=crit_size + sum(f.size for f in others),
|
|
task_id="fake-empty",
|
|
endpoint_signature="http://docling.test",
|
|
options_signature=options_signature,
|
|
extras={"fixed_constants": dict(FIXED_CONSTANTS)},
|
|
downloaded_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
)
|
|
write_manifest(raw_dir, manifest)
|
|
return manifest
|
|
|
|
monkeypatch.setattr(
|
|
client_mod.DoclingRawClient, "download_into", _fake_download
|
|
)
|
|
|
|
input_dir = tmp_path / "inputs" / "ws"
|
|
input_dir.mkdir(parents=True)
|
|
src = input_dir / "demo.pdf"
|
|
src.write_bytes(b"PDFPDF" * 256)
|
|
|
|
rag = _new_rag(tmp_path)
|
|
await rag.initialize_storages()
|
|
try:
|
|
_stub_pipeline(monkeypatch, rag, src)
|
|
doc_id = "doc-abcdef0123456789abcdef0123456789"
|
|
await _seed_doc_status(rag, doc_id)
|
|
|
|
with pytest.raises(ValueError, match="zero blocks"):
|
|
await _parse_via_registry(
|
|
rag,
|
|
"docling",
|
|
doc_id=doc_id,
|
|
file_path="demo.pdf",
|
|
content_data={},
|
|
)
|
|
|
|
# Sidecar must NOT have been emitted: ``write_sidecar`` is reached
|
|
# only after the zero-blocks check, so no ``*.blocks.jsonl`` may
|
|
# exist anywhere under the workspace.
|
|
blocks_files = list(tmp_path.rglob("*.blocks.jsonl"))
|
|
assert not blocks_files, (
|
|
f"sidecar emitted despite zero-blocks failure: {blocks_files}"
|
|
)
|
|
finally:
|
|
await rag.finalize_storages()
|
|
|
|
asyncio.new_event_loop().run_until_complete(_run())
|