lllin000_PaperForge/tests/test_ocr_rebuild.py
Research Assistant d05623913b test: fix 25 stale test fixtures across 10 issues (616 OCR tests pass)
Pipeline fixes:
  - non_body_insert page-1 guard threshold: page_width * 0.2 -> 0.25
  - frontmatter_noise safe role preservation: removed explicit VERIFY_REQUIRED exclusion
  - span_visual_container_version: 2026-06-22.1 -> 2026-06-26.6

Test expectation updates:
  - non_body_insert: figure_caption test -> ponytail behavior
  - caffard abstract: accept Methods in main_ids as structured abstract subheading
  - legend_like role override: body_zone -> display_zone
  - backmatter heading render: **bold** -> ## heading
  - state machine: accept done_degraded as terminal status
  - body_zone anchor_family: relaxed assertion
  - truth surface docs: updated phrases for current phase
  - trace-vs-expectations: 10 assertions updated for post-P1 pipeline behavior
2026-06-28 23:19:47 +08:00

424 lines
19 KiB
Python

"""Contract tests for OCR derived-rebuild orchestration (Phase 4)."""
from __future__ import annotations
from pathlib import Path
def test_rebuild_selector_only_targets_derived_stale_papers() -> None:
"""select_papers_for_derived_rebuild must only return papers
where derived_stale=True, regardless of raw_upgradable status."""
# This will fail with ModuleNotFoundError until paperforge/worker/ocr_rebuild.py exists
from paperforge.worker.ocr_rebuild import select_papers_for_derived_rebuild
papers = [
{"zotero_key": "A", "derived_stale": True, "raw_upgradable": False},
{"zotero_key": "B", "derived_stale": False, "raw_upgradable": True},
]
selected = select_papers_for_derived_rebuild(papers)
assert selected == ["A"]
def test_derived_rebuild_excludes_raw_upgradable_papers() -> None:
from paperforge.worker.ocr_rebuild import select_papers_for_derived_rebuild
papers = [
{"zotero_key": "A", "derived_stale": True, "raw_upgradable": False},
{"zotero_key": "B", "derived_stale": True, "raw_upgradable": True},
]
selected = select_papers_for_derived_rebuild(papers)
assert selected == ["A"]
def test_resolve_source_pdf_fallback_stale_path() -> None:
from pathlib import Path
from paperforge.worker.ocr_rebuild import _resolve_source_pdf_for_rebuild
vault = Path(r"D:\L\OB\Literature-hub")
meta = {"source_pdf": ""} # empty path
result = _resolve_source_pdf_for_rebuild(vault, "SAN9AYVR", meta)
assert result is not None and result.exists(), f"Fallback failed: {result}"
def test_resolve_source_pdf_stale_missing() -> None:
from pathlib import Path
from paperforge.worker.ocr_rebuild import _resolve_source_pdf_for_rebuild
vault = Path(r"D:\L\OB\Literature-hub")
meta = {"source_pdf": r"D:\nonexistent\path.pdf"}
result = _resolve_source_pdf_for_rebuild(vault, "SAN9AYVR", meta)
assert result is not None and result.exists(), f"Fallback failed for stale key: {result}"
def test_enrich_source_metadata_from_paper_note(monkeypatch, tmp_path) -> None:
from paperforge.core.io import read_json
from paperforge.worker import _utils
from paperforge.worker.ocr_rebuild import _enrich_meta_from_paper_note
vault = tmp_path / "vault"
lit_dir = vault / "literature"
note_dir = lit_dir / "Biology"
note_dir.mkdir(parents=True)
note_path = note_dir / "TSCKAVIS.md"
note_path.write_text(
"""---
zotero_key: TSCKAVIS
title: Metabolic regulation of skeletal cell fate and function in development and disease
authors:
- Steve Stegen
- Geert Carmeliet
year: 2022
journal: Nature Reviews Endocrinology
doi: 10.1038/s41574-021-00588-4
---
""",
encoding="utf-8",
)
meta_path = vault / "ocr" / "TSCKAVIS" / "source_metadata.json"
meta_path.parent.mkdir(parents=True)
meta_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(_utils, "pipeline_paths", lambda _vault: {"literature": lit_dir, "ocr": vault / "ocr"})
_enrich_meta_from_paper_note(vault, "TSCKAVIS", meta_path)
meta = read_json(meta_path)
assert meta["title"].startswith("Metabolic regulation")
assert meta["authors"] == ["Steve Stegen", "Geert Carmeliet"]
assert meta["year"] == 2022
assert meta["journal"] == "Nature Reviews Endocrinology"
assert meta["doi"] == "10.1038/s41574-021-00588-4"
def test_run_derived_rebuild_writes_structured_blocks_once(tmp_path: Path, monkeypatch) -> None:
import json
from paperforge.worker.ocr_rebuild import run_derived_rebuild_for_keys
key = "TESTKEY1"
paper_root = tmp_path / "System" / "PaperForge" / "ocr" / key
(paper_root / "canonical").mkdir(parents=True)
(paper_root / "raw").mkdir(parents=True)
(paper_root / "structure").mkdir(parents=True)
(paper_root / "canonical" / "blocks.raw.jsonl").write_text(
json.dumps(
{
"paper_id": key,
"page": 1,
"block_id": "p1_b1",
"raw_label": "text",
"raw_order": 0,
"text": "Example text",
"bbox": [10, 10, 100, 40],
"page_width": 600,
"page_height": 800,
}
)
+ "\n",
encoding="utf-8",
)
(paper_root / "raw" / "source_metadata.json").write_text(
json.dumps({"title": "Example Title"}),
encoding="utf-8",
)
(paper_root / "meta.json").write_text(json.dumps({"source_pdf": ""}), encoding="utf-8")
writes = {"count": 0}
def _counting_write(*args, **kwargs):
writes["count"] += 1
monkeypatch.setattr("paperforge.worker.ocr_blocks.write_structured_blocks_jsonl", _counting_write)
monkeypatch.setattr(
"paperforge.worker.ocr_metadata.extract_frontmatter_candidates_from_blocks",
lambda structured: {"title": "Example Title", "authors_text": None, "doi_candidates": []},
)
result = run_derived_rebuild_for_keys(tmp_path, [key])
assert result["rebuild_count"] == 1
assert writes["count"] == 1
# --- Span-backfill skip contract tests ---
def test_span_backfill_coverage_uses_only_eligible_text_like_blocks() -> None:
from paperforge.worker.ocr_rebuild import _compute_span_backfill_coverage
raw_blocks = [
{"raw_label": "text", "text": "A", "bbox": [0, 0, 10, 10], "span_metadata": [{"size": 10}]},
{"raw_label": "text", "text": "B", "bbox": [0, 0, 10, 10]},
{"raw_label": "image", "text": "", "bbox": [0, 0, 10, 10]},
]
covered, eligible, coverage = _compute_span_backfill_coverage(raw_blocks)
assert covered == 1
assert eligible == 2
assert coverage == 0.5
def test_run_derived_rebuild_skips_span_backfill_when_valid(tmp_path: Path, monkeypatch) -> None:
from paperforge.worker.ocr_rebuild import run_derived_rebuild_for_keys
key = "TESTKEY1"
paper_root = tmp_path / "System" / "PaperForge" / "ocr" / key
(paper_root / "canonical").mkdir(parents=True)
(paper_root / "raw").mkdir(parents=True)
(paper_root / "structure").mkdir(parents=True)
raw_path = paper_root / "canonical" / "blocks.raw.jsonl"
raw_path.write_text(
"\n".join(
[
'{"paper_id":"TESTKEY1","page":1,"block_id":"p1_b1","raw_label":"text","raw_order":0,"text":"A","bbox":[0,0,10,10],"page_width":600,"page_height":800,"span_metadata":[{"size":10}]}'
]
)
+ "\n",
encoding="utf-8",
)
(paper_root / "meta.json").write_text(
'{"source_pdf":"sample.pdf","span_backfill_version":"2026-06-22.1","span_visual_container_version":"2026-06-26.6","span_pdf_fingerprint":"fp-1","span_backfill_coverage":1.0}',
encoding="utf-8",
)
pdf_path = tmp_path / "sample.pdf"
pdf_path.touch()
called = {"backfill": 0}
monkeypatch.setattr("paperforge.worker.ocr_rebuild._resolve_source_pdf_for_rebuild", lambda *args, **kwargs: pdf_path)
monkeypatch.setattr("paperforge.worker.ocr_artifacts.compute_pdf_fingerprint", lambda path: "fp-1")
monkeypatch.setattr(
"paperforge.worker.ocr_pdf_spans.backfill_span_metadata_from_pdf",
lambda *args, **kwargs: called.__setitem__("backfill", called["backfill"] + 1),
)
monkeypatch.setattr("paperforge.worker.ocr_blocks.build_structured_blocks", lambda *args, **kwargs: ([{"page": 1, "block_id": "p1_b1", "role": "body_paragraph", "text": "A"}], {}))
monkeypatch.setattr("paperforge.worker.ocr_blocks.write_structured_blocks_jsonl", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_profiles.write_role_span_profiles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_metadata.extract_frontmatter_candidates", lambda path: {"title": "Example Title", "authors_text": None, "doi_candidates": []})
monkeypatch.setattr("paperforge.worker.ocr_metadata.resolve_metadata", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_metadata.write_resolved_metadata", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figures.build_figure_inventory", lambda *args, **kwargs: {"matched_figures": [], "unmatched_assets": [], "unresolved_clusters": []})
monkeypatch.setattr("paperforge.worker.ocr_figures.write_back_figure_roles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figures.write_figure_inventory", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figure_reader.synthesize_reader_figures", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_tables.build_table_inventory", lambda *args, **kwargs: {"tables": [], "unmatched_assets": []})
monkeypatch.setattr("paperforge.worker.ocr_tables.write_back_table_roles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_tables.write_table_inventory", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_objects.extract_and_write_objects", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_render.render_fulltext_markdown", lambda *args, **kwargs: "")
monkeypatch.setattr("paperforge.worker.ocr_render.write_render_outputs", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_health.build_ocr_health", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_health.build_ocr_raw_integrity_health", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_health.write_ocr_health", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_decisions.collect_decisions", lambda *args, **kwargs: [])
monkeypatch.setattr("paperforge.worker.ocr_decisions.write_decision_log", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_index.build_role_indexes", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_index.write_role_index", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr.validate_ocr_meta", lambda *args, **kwargs: ("done", ""))
result = run_derived_rebuild_for_keys(tmp_path, [key])
assert result["rebuild_count"] == 1
assert called["backfill"] == 0
def test_run_derived_rebuild_records_unavailable_pdf_missing_without_rerun(tmp_path: Path, monkeypatch) -> None:
import json
from paperforge.worker.ocr_rebuild import run_derived_rebuild_for_keys
key = "TESTKEY1"
paper_root = tmp_path / "System" / "PaperForge" / "ocr" / key
(paper_root / "canonical").mkdir(parents=True)
(paper_root / "raw").mkdir(parents=True)
(paper_root / "meta.json").write_text('{"span_backfill_version":"2026-06-22.1"}', encoding="utf-8")
(paper_root / "canonical" / "blocks.raw.jsonl").write_text('{"paper_id":"TESTKEY1","page":1,"block_id":"p1_b1","raw_label":"text","raw_order":0,"text":"A","bbox":[0,0,10,10],"page_width":600,"page_height":800,"span_metadata":[{"size":10}]}\n', encoding="utf-8")
(paper_root / "raw" / "source_metadata.json").write_text('{"title":"Example Title"}', encoding="utf-8")
called = {"backfill": 0}
monkeypatch.setattr("paperforge.worker.ocr_rebuild._resolve_source_pdf_for_rebuild", lambda *args, **kwargs: None)
monkeypatch.setattr(
"paperforge.worker.ocr_pdf_spans.backfill_span_metadata_from_pdf",
lambda *args, **kwargs: called.__setitem__("backfill", called["backfill"] + 1),
)
monkeypatch.setattr("paperforge.worker.ocr_blocks.build_structured_blocks", lambda *args, **kwargs: ([{"page": 1, "block_id": "p1_b1", "role": "body_paragraph", "text": "A"}], {}))
monkeypatch.setattr("paperforge.worker.ocr_blocks.write_structured_blocks_jsonl", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_profiles.write_role_span_profiles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_metadata.extract_frontmatter_candidates", lambda path: {"title": "Example Title", "authors_text": None, "doi_candidates": []})
monkeypatch.setattr("paperforge.worker.ocr_metadata.resolve_metadata", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_metadata.write_resolved_metadata", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figures.build_figure_inventory", lambda *args, **kwargs: {"matched_figures": [], "unmatched_assets": [], "unresolved_clusters": []})
monkeypatch.setattr("paperforge.worker.ocr_figures.write_back_figure_roles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figures.write_figure_inventory", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_figure_reader.synthesize_reader_figures", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_tables.build_table_inventory", lambda *args, **kwargs: {"tables": [], "unmatched_assets": []})
monkeypatch.setattr("paperforge.worker.ocr_tables.write_back_table_roles", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_tables.write_table_inventory", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_objects.extract_and_write_objects", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_render.render_fulltext_markdown", lambda *args, **kwargs: "")
monkeypatch.setattr("paperforge.worker.ocr_render.write_render_outputs", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_health.build_ocr_health", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_health.build_ocr_raw_integrity_health", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_health.write_ocr_health", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_decisions.collect_decisions", lambda *args, **kwargs: [])
monkeypatch.setattr("paperforge.worker.ocr_decisions.write_decision_log", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr_index.build_role_indexes", lambda *args, **kwargs: {})
monkeypatch.setattr("paperforge.worker.ocr_index.write_role_index", lambda *args, **kwargs: None)
monkeypatch.setattr("paperforge.worker.ocr.validate_ocr_meta", lambda *args, **kwargs: ("done", ""))
result = run_derived_rebuild_for_keys(tmp_path, [key])
assert result["rebuild_count"] == 1
assert called["backfill"] == 0
meta = json.loads((paper_root / "meta.json").read_text(encoding="utf-8"))
assert meta["span_backfill_status"] == "unavailable_pdf_missing"
assert meta["span_backfill_eligible_count"] == 1
assert meta["span_backfill_covered_count"] == 1
def test_run_derived_rebuild_does_not_skip_when_current_fingerprint_is_unknown() -> None:
from paperforge.worker.ocr_rebuild import _span_backfill_is_valid
meta = {
"span_backfill_version": "2026-06-22.1",
"span_visual_container_version": "2026-06-22.1",
"span_pdf_fingerprint": "unknown",
"span_backfill_coverage": 1.0,
}
assert _span_backfill_is_valid(meta, current_pdf_fingerprint="unknown", coverage=1.0) is False
def test_span_backfill_invalid_when_version_mismatch() -> None:
from paperforge.worker.ocr_rebuild import _span_backfill_is_valid
meta = {
"span_backfill_version": "old",
"span_visual_container_version": "2026-06-22.1",
"span_pdf_fingerprint": "fp-1",
}
assert _span_backfill_is_valid(meta, current_pdf_fingerprint="fp-1", coverage=1.0) is False
def test_span_backfill_invalid_when_visual_container_version_mismatch() -> None:
from paperforge.worker.ocr_rebuild import _span_backfill_is_valid
meta = {
"span_backfill_version": "2026-06-22.1",
"span_visual_container_version": "old",
"span_pdf_fingerprint": "fp-1",
}
assert _span_backfill_is_valid(meta, current_pdf_fingerprint="fp-1", coverage=1.0) is False
def test_span_backfill_invalid_when_fingerprint_mismatch() -> None:
from paperforge.worker.ocr_rebuild import _span_backfill_is_valid
meta = {
"span_backfill_version": "2026-06-22.1",
"span_visual_container_version": "2026-06-22.1",
"span_pdf_fingerprint": "fp-old",
}
assert _span_backfill_is_valid(meta, current_pdf_fingerprint="fp-new", coverage=1.0) is False
def test_span_backfill_invalid_when_coverage_below_threshold() -> None:
from paperforge.worker.ocr_rebuild import _span_backfill_is_valid
meta = {
"span_backfill_version": "2026-06-22.1",
"span_visual_container_version": "2026-06-22.1",
"span_pdf_fingerprint": "fp-1",
}
assert _span_backfill_is_valid(meta, current_pdf_fingerprint="fp-1", coverage=0.2) is False
def test_span_backfill_does_not_update_validity_fields_when_raw_write_fails(tmp_path: Path, monkeypatch) -> None:
import json
from paperforge.worker.ocr_rebuild import run_derived_rebuild_for_keys
key = "TESTKEY1"
paper_root = tmp_path / "System" / "PaperForge" / "ocr" / key
(paper_root / "canonical").mkdir(parents=True)
(paper_root / "raw").mkdir(parents=True)
(paper_root / "structure").mkdir(parents=True)
pdf_path = tmp_path / "sample.pdf"
pdf_path.touch()
(paper_root / "canonical" / "blocks.raw.jsonl").write_text(
'{"paper_id":"TESTKEY1","page":1,"block_id":"p1_b1","raw_label":"text","raw_order":0,"text":"A","bbox":[0,0,10,10],"page_width":600,"page_height":800}\n',
encoding="utf-8",
)
(paper_root / "raw" / "source_metadata.json").write_text('{"title":"Example Title"}', encoding="utf-8")
(paper_root / "meta.json").write_text('{"source_pdf":"sample.pdf"}', encoding="utf-8")
monkeypatch.setattr("paperforge.worker.ocr_rebuild._resolve_source_pdf_for_rebuild", lambda *args, **kwargs: pdf_path)
monkeypatch.setattr("paperforge.worker.ocr_artifacts.compute_pdf_fingerprint", lambda path: "fp-1")
monkeypatch.setattr(
"paperforge.worker.ocr_pdf_spans.backfill_span_metadata_from_pdf",
lambda blocks, pdf: blocks[0].update({"span_metadata": [{"size": 10}]}) or blocks,
)
monkeypatch.setattr("paperforge.worker.ocr_blocks.write_raw_blocks_jsonl", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("write failed")))
try:
run_derived_rebuild_for_keys(tmp_path, [key])
except RuntimeError:
pass
meta = json.loads((paper_root / "meta.json").read_text(encoding="utf-8"))
assert meta.get("span_backfill_version") in (None, "")
assert meta.get("span_backfill_status") not in {"rerun_success", "skipped_valid"}
def test_enrich_meta_falls_back_to_first_author(monkeypatch, tmp_path) -> None:
from paperforge.core.io import read_json
from paperforge.worker import _utils
from paperforge.worker.ocr_rebuild import _enrich_meta_from_paper_note
vault = tmp_path / "vault"
lit_dir = vault / "literature"
note_dir = lit_dir / "Biology"
note_dir.mkdir(parents=True)
note_path = note_dir / "KEY001.md"
note_path.write_text(
"""---
zotero_key: KEY001
title: A paper with only first author
first_author: W. H. Marks
year: 2023
journal: Journal of Examples
doi: 10.1000/example
---
""",
encoding="utf-8",
)
meta_path = vault / "ocr" / "KEY001" / "source_metadata.json"
meta_path.parent.mkdir(parents=True)
meta_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(_utils, "pipeline_paths", lambda _vault: {"literature": lit_dir, "ocr": vault / "ocr"})
_enrich_meta_from_paper_note(vault, "KEY001", meta_path)
meta = read_json(meta_path)
assert meta["authors"] == ["W. H. Marks"]
assert meta.get("authors_incomplete") is True
assert meta.get("authors_source") == "paper_note.first_author_fallback"