"""Legacy page-local renderer tests. Classification: diagnostic only. render_page_blocks() is not the final fulltext truth for OCR-v2. Primary production path is render_fulltext_markdown() in ocr_render.py. These tests exist for backward compatibility and legacy trace interpretation. """ from __future__ import annotations from pathlib import Path from unittest.mock import patch from PIL import Image def test_extract_preserved_ocr_redo_preserves_flag_across_frontmatter_rewrite() -> None: """Simulate sync preserving ocr_redo through frontmatter_note().""" from paperforge.adapters.obsidian_frontmatter import extract_preserved_ocr_redo from paperforge.worker.sync import frontmatter_note entry = {"zotero_key": "TEST", "title": "Test", "year": "2025"} original = """--- zotero_key: TEST title: Test year: 2025 ocr_redo: true --- """ preserved = extract_preserved_ocr_redo(original) assert preserved is True rewritten = frontmatter_note(entry, existing_text=original) assert "ocr_redo: true" in rewritten def test_extract_preserved_ocr_redo_false_by_default() -> None: from paperforge.adapters.obsidian_frontmatter import extract_preserved_ocr_redo text = """--- zotero_key: TEST title: Test --- """ assert extract_preserved_ocr_redo(text) is False def test_caption_group_assignments_respects_columns() -> None: from paperforge.worker.ocr import caption_group_assignments blocks = [ { "block_id": 1, "block_label": "chart", "block_bbox": [80, 116, 546, 434], "block_content": "", }, { "block_id": 2, "block_label": "figure_title", "block_bbox": [66, 446, 559, 628], "block_content": "Figure 1. Left column figure.", }, { "block_id": 3, "block_label": "chart", "block_bbox": [598, 114, 1063, 493], "block_content": "", }, { "block_id": 4, "block_label": "figure_title", "block_bbox": [584, 503, 1079, 744], "block_content": "Figure 2. Right column figure.", }, ] figure_map, _table_map = caption_group_assignments(blocks) left_ids = [item["block_id"] for item in figure_map[2]] right_ids = [item["block_id"] for item in figure_map[4]] assert left_ids == [1] assert right_ids == [3] def test_validate_block_order_falls_back_to_column_major_on_excessive_switches() -> None: from paperforge.worker.ocr import validate_block_order blocks = [ { "block_id": 1, "block_label": "text", "block_order": 0, "block_bbox": [80, 100, 500, 140], "block_content": "L1", }, { "block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [700, 110, 1120, 150], "block_content": "R1", }, { "block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [80, 200, 500, 240], "block_content": "L2", }, { "block_id": 4, "block_label": "text", "block_order": 3, "block_bbox": [700, 210, 1120, 250], "block_content": "R2", }, { "block_id": 5, "block_label": "text", "block_order": 4, "block_bbox": [80, 300, 500, 340], "block_content": "L3", }, { "block_id": 6, "block_label": "text", "block_order": 5, "block_bbox": [700, 310, 1120, 350], "block_content": "R3", }, { "block_id": 7, "block_label": "text", "block_order": 6, "block_bbox": [80, 400, 500, 440], "block_content": "L4", }, { "block_id": 8, "block_label": "text", "block_order": 7, "block_bbox": [700, 410, 1120, 450], "block_content": "R4", }, ] ordered = validate_block_order(blocks, page_width=1200) assert [block["block_content"] for block in ordered] == ["L1", "L2", "L3", "L4", "R1", "R2", "R3", "R4"] def test_validate_block_order_repairs_non_monotonic_order_within_column() -> None: from paperforge.worker.ocr import validate_block_order blocks = [ { "block_id": 1, "block_label": "text", "block_order": 0, "block_bbox": [80, 100, 500, 140], "block_content": "L1", }, { "block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [80, 300, 500, 340], "block_content": "L3", }, { "block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [80, 200, 500, 240], "block_content": "L2", }, { "block_id": 4, "block_label": "text", "block_order": 3, "block_bbox": [700, 150, 1120, 190], "block_content": "R1", }, ] ordered = validate_block_order(blocks, page_width=1200) assert [block["block_content"] for block in ordered] == ["L1", "L2", "L3", "R1"] def test_validate_block_order_keeps_bbox_sort_when_block_order_missing() -> None: from paperforge.worker.ocr import block_sort_key, validate_block_order blocks = [ {"block_id": 1, "block_label": "text", "block_bbox": [700, 300, 1120, 340], "block_content": "R2"}, {"block_id": 2, "block_label": "text", "block_bbox": [80, 100, 500, 140], "block_content": "L1"}, {"block_id": 3, "block_label": "text", "block_bbox": [700, 100, 1120, 140], "block_content": "R1"}, {"block_id": 4, "block_label": "text", "block_bbox": [80, 300, 500, 340], "block_content": "L2"}, ] ordered = validate_block_order(sorted(blocks, key=block_sort_key), page_width=0) assert [block["block_content"] for block in ordered] == ["L1", "R1", "L2", "R2"] def test_validate_block_order_preserves_center_spanning_blocks() -> None: from paperforge.worker.ocr import validate_block_order blocks = [ { "block_id": 1, "block_label": "text", "block_order": 0, "block_bbox": [120, 80, 1080, 140], "block_content": "Full-width section heading", }, { "block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [80, 180, 500, 240], "block_content": "Left paragraph one", }, { "block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [700, 190, 1120, 250], "block_content": "Right paragraph one", }, { "block_id": 4, "block_label": "text", "block_order": 3, "block_bbox": [80, 280, 500, 340], "block_content": "Left paragraph two", }, { "block_id": 5, "block_label": "text", "block_order": 4, "block_bbox": [700, 290, 1120, 350], "block_content": "Right paragraph two", }, ] ordered = validate_block_order(blocks, page_width=1200) assert [block["block_content"] for block in ordered] == [ "Full-width section heading", "Left paragraph one", "Right paragraph one", "Left paragraph two", "Right paragraph two", ] def test_validate_block_order_retains_blocks_with_invalid_bbox() -> None: from paperforge.worker.ocr import validate_block_order blocks = [ { "block_id": 1, "block_label": "text", "block_order": 0, "block_bbox": [80, 100, 500, 140], "block_content": "Left valid", }, { "block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [500, 160, 500, 200], "block_content": "Broken bbox", }, { "block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [700, 180, 1120, 220], "block_content": "Right valid", }, { "block_id": 4, "block_label": "text", "block_order": 3, "block_bbox": [80, 260, 500, 300], "block_content": "Left lower", }, ] ordered = validate_block_order(blocks, page_width=1200) assert [block["block_content"] for block in ordered] == ["Left valid", "Broken bbox", "Right valid", "Left lower"] def test_render_page_blocks_reorders_interleaved_two_column_text_by_geometry(tmp_path: Path) -> None: from paperforge.worker.ocr import render_page_blocks vault = tmp_path / "vault" images_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "images" page_cache_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "pages" images_dir.mkdir(parents=True) page_cache_dir.mkdir(parents=True) result = { "prunedResult": { "width": 1200, "height": 1600, "parsing_res_list": [ { "block_id": 1, "block_label": "text", "block_order": 0, "block_bbox": [80, 100, 500, 160], "block_content": "Left intro paragraph with enough body text.", }, { "block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [700, 110, 1120, 170], "block_content": "Right intro paragraph with enough body text.", }, { "block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [80, 200, 500, 260], "block_content": "Left methods paragraph with enough body text.", }, { "block_id": 4, "block_label": "text", "block_order": 3, "block_bbox": [700, 210, 1120, 270], "block_content": "Right methods paragraph with enough body text.", }, { "block_id": 5, "block_label": "text", "block_order": 4, "block_bbox": [80, 300, 500, 360], "block_content": "Left results paragraph with enough body text.", }, { "block_id": 6, "block_label": "text", "block_order": 5, "block_bbox": [700, 310, 1120, 370], "block_content": "Right results paragraph with enough body text.", }, { "block_id": 7, "block_label": "text", "block_order": 6, "block_bbox": [80, 400, 500, 460], "block_content": "Left discussion paragraph with enough body text.", }, { "block_id": 8, "block_label": "text", "block_order": 7, "block_bbox": [700, 410, 1120, 470], "block_content": "Right discussion paragraph with enough body text.", }, ], }, "inputImage": "", } rendered = render_page_blocks(vault, 2, result, images_dir, page_cache_dir, pdf_doc=None) body_lines = [line for line in rendered if line and not line.startswith("" in markdown def test_render_fulltext_hides_debug_artifacts() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown blocks = [ {"block_id": 1, "role": "body_paragraph", "text": "Body text.", "page": 1, "bbox": [0, 0, 100, 20]}, ] reader_payload = { "reader_figures": [ { "reader_figure_id": "figure_003_reader", "reader_status": "LEGEND_ONLY", "strict_status": "unmatched", "figure_number": 3, "caption_text": "FIGURE 3 | Histological evaluation...", "caption_block_id": 30, "visual_groups": [], "consumed_caption_block_ids": [30], "consumed_asset_block_ids": [], "debug_refs": {"strict_name": "unmatched_legend_003"}, } ], "consumed_caption_block_ids": [30], "consumed_asset_block_ids": [], } markdown = render_fulltext_markdown( structured_blocks=blocks, resolved_metadata={}, figure_inventory={}, table_inventory={}, page_count=1, reader_payload=reader_payload, ) assert "unmatched_legend_" not in markdown assert "unresolved_cluster_" not in markdown assert "orphan_" not in markdown def test_render_fulltext_skips_consumed_caption_block_even_when_role_is_body_paragraph() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown blocks = [ { "block_id": 21, "role": "body_paragraph", "text": "FIGURE 2 | Treadmill exercise protocols...", "page": 1, "bbox": [0, 0, 100, 20], }, { "block_id": 22, "role": "body_paragraph", "text": "The treadmill protocol was well tolerated.", "page": 1, "bbox": [0, 30, 500, 50], }, ] reader_payload = { "reader_figures": [ { "reader_figure_id": "figure_002_reader", "reader_status": "LEGEND_ONLY", "strict_status": "unmatched", "figure_number": 2, "caption_text": "FIGURE 2 | Treadmill exercise protocols...", "caption_block_id": 21, "visual_groups": [], "consumed_caption_block_ids": [21], "consumed_asset_block_ids": [], "debug_refs": {}, } ], "consumed_caption_block_ids": [21], "consumed_asset_block_ids": [], } markdown = render_fulltext_markdown( structured_blocks=blocks, resolved_metadata={}, figure_inventory={"matched_figures": []}, table_inventory={"tables": []}, page_count=1, reader_payload=reader_payload, ) assert markdown.count("FIGURE 2 | Treadmill exercise protocols...") == 1 def test_render_fulltext_prefers_reader_figures_over_legacy_matched_figures() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown blocks = [{"block_id": 22, "role": "body_paragraph", "text": "Results body.", "page": 1, "bbox": [0, 30, 500, 50]}] markdown = render_fulltext_markdown( structured_blocks=blocks, resolved_metadata={}, figure_inventory={ "matched_figures": [{"figure_id": "figure_002", "page": 1, "text": "Figure 2 legacy caption"}] }, table_inventory={"tables": []}, page_count=1, reader_payload={ "reader_figures": [ { "reader_figure_id": "figure_002_reader", "reader_status": "LEGEND_ONLY", "strict_status": "unmatched", "strict_source": "unmatched_legends", "figure_number": 2, "caption_text": "FIGURE 2 | Reader caption", "caption_block_id": 21, "visual_groups": [], "consumed_caption_block_ids": [21], "consumed_asset_block_ids": [], "debug_refs": {}, } ], "consumed_caption_block_ids": [21], "consumed_asset_block_ids": [], }, ) assert "![[render/figures/figure_002.md]]" not in markdown assert "> **Figure 2**" in markdown def test_render_fulltext_uses_accepted_abstract_span_only() -> None: from paperforge.worker.ocr_document import DocumentStructure from paperforge.worker.ocr_render import render_fulltext_markdown doc = DocumentStructure() doc.abstract_span = {"heading_block_id": "h", "body_block_ids": ["a"], "status": "ACCEPT"} structured_blocks = [ { "block_id": "h", "page": 1, "role": "abstract_heading", "role_verification_status": "ACCEPT", "text": "Abstract", "render_default": True, }, { "block_id": "a", "page": 1, "role": "abstract_body", "role_verification_status": "ACCEPT", "text": "Real abstract.", "render_default": True, }, { "block_id": "bad", "page": 1, "role": "abstract_body", "role_verification_status": "HOLD", "text": "Mislabeled body.", "render_default": True, }, { "block_id": "intro", "page": 1, "role": "section_heading", "role_verification_status": "ACCEPT", "text": "Introduction", "render_default": True, }, ] markdown = render_fulltext_markdown( structured_blocks=structured_blocks, resolved_metadata={}, figure_inventory={}, table_inventory={}, page_count=1, document_structure=doc, reader_payload={"reader_figures": []}, ) assert "Real abstract." in markdown assert "Mislabeled body." not in markdown def test_render_fulltext_renders_all_reader_visible_statuses() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown statuses = ["EXACT_MATCH", "SEQUENCE_MATCH", "GROUPED_APPROXIMATE", "LEGEND_ONLY", "ASSET_GROUP_ONLY", "HOLD"] reader_figures = [ { "reader_figure_id": f"fig_reader_{i}", "figure_number": i, "block_id": f"fig_{i}", "page": 1, "page_coords": {"x": 100, "y": 100 + i * 100, "width": 400, "height": 300}, "reader_status": status, "consumed_caption_block_ids": [], } for i, status in enumerate(statuses) ] structured_blocks = [ {"block_id": "a", "page": 1, "role": "body_paragraph", "text": "Some body text.", "render_default": True}, ] markdown = render_fulltext_markdown( structured_blocks=structured_blocks, resolved_metadata={}, figure_inventory={}, table_inventory={}, page_count=1, reader_payload={"reader_figures": reader_figures}, ) for status in statuses: assert status not in markdown # --- Gap surface lock tests (expected to FAIL until Tasks 2-9 fix production code) --- def test_render_fulltext_renders_abstract_body_without_document_structure() -> None: """abstract_heading + abstract_body blocks should render as '## Abstract' section.""" from paperforge.worker.ocr_render import render_fulltext_markdown markdown = render_fulltext_markdown( structured_blocks=[ {"role": "abstract_heading", "text": "", "render_default": True, "page": 1}, {"role": "abstract_body", "text": "This is the abstract body text.", "render_default": True, "page": 1}, {"role": "section_heading", "text": "1 Introduction", "render_default": True, "page": 1}, ], resolved_metadata={}, figure_inventory={}, table_inventory={}, ) assert "## Abstract" in markdown assert "This is the abstract body text." in markdown def test_reader_visible_statuses_are_emitted_in_markdown_contract() -> None: """Reader figure statuses (e.g. EXACT_MATCH) should appear in the markdown output.""" from paperforge.worker.ocr_render import render_fulltext_markdown markdown = render_fulltext_markdown( structured_blocks=[{"block_id": "a", "page": 1, "role": "body_paragraph", "text": "Some body text.", "render_default": True}], resolved_metadata={}, figure_inventory={}, table_inventory={}, page_count=1, reader_payload={ "reader_figures": [ {"reader_figure_id": "fig_reader_0", "figure_number": 0, "block_id": "fig_0", "page": 1, "page_coords": {"x": 0, "y": 0, "width": 10, "height": 10}, "reader_status": "EXACT_MATCH", "consumed_caption_block_ids": []}, ] }, ) assert "EXACT_MATCH" not in markdown def test_fulltext_skips_consumed_table_note_band_blocks() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown structured_blocks = [ {"page": 5, "block_id": "p5_c1", "role": "table_caption", "text": "Table 1. Main results", "bbox": [100, 430, 600, 460], "zone": "display_zone", "style_family": "table_caption_like"}, {"page": 5, "block_id": "p5_n1", "role": "footnote", "text": "* p < 0.05", "bbox": [100, 470, 600, 490]}, {"page": 5, "block_id": "p5_n2", "role": "footnote", "text": "Data are mean \u00b1 SD.", "bbox": [100, 492, 600, 512]}, ] table_inventory = { "tables": [ { "table_id": "table_001", "page": 5, "caption_block_id": "p5_c1", "asset_block_id": "p5_a1", "note_block_ids": ["p5_n1", "p5_n2"], "consumed_block_ids": ["p5_c1", "p5_a1", "p5_n1", "p5_n2"], } ] } md = render_fulltext_markdown( structured_blocks=structured_blocks, resolved_metadata={}, figure_inventory={"matched_figures": []}, table_inventory=table_inventory, page_count=5, document_structure=None, reader_payload={"figures": []}, ) assert "* p < 0.05" not in md assert "Data are mean \u00b1 SD." not in md def test_fulltext_skips_table_note_blocks_consumed_by_inventory() -> None: from paperforge.worker.ocr_render import render_fulltext_markdown structured_blocks = [ {"page": 5, "block_id": "p5_c1", "role": "table_caption", "text": "Table 1. Main results", "bbox": [100, 420, 600, 460], "zone": "display_zone", "style_family": "table_caption_like"}, {"page": 5, "block_id": "p5_n1", "role": "footnote", "text": "* p < 0.05", "bbox": [100, 405, 600, 425]}, ] table_inventory = { "tables": [{ "table_id": "table_001", "page": 5, "caption_block_id": "p5_c1", "caption_text": "Table 1. Main results", "asset_block_id": "p5_a1", "note_block_ids": ["p5_n1"], "note_texts": ["* p < 0.05"], "consumed_block_ids": ["p5_a1", "p5_c1", "p5_n1"], }] } md = render_fulltext_markdown( structured_blocks=structured_blocks, resolved_metadata={}, figure_inventory={"matched_figures": []}, table_inventory=table_inventory, page_count=5, document_structure=None, reader_payload={"figures": []}, ) assert "* p < 0.05" not in md