"""Coverage-driven extraction retry ladder — classic path (pipeline/extractor.py).""" from backend import config from backend.pipeline import extractor PAGE_TEXT = ("1. \nALL SAWN LUMBER IN CONTACT WITH SOIL TO BE SOUTHERN PINE, " "PRESSURE TREATED.\n2. \nROOF SHEATHING: 5/8\" PLYWOOD, C-D GRADE, " "STRUCTURAL I.") def _page(n=8, text=PAGE_TEXT): return {"page_number": n, "base64": "AAAA", "text_layer": text} def test_classic_fallback_when_vision_returns_nothing(monkeypatch): """Vision pass returns an unusable bare list; text retry disabled -> deterministic fallback stubs make a dark sheet impossible.""" monkeypatch.setattr(extractor, "call_json", lambda **kw: [{"name": "general notes", "value": "notes"}]) monkeypatch.setattr(config, "EXTRACT_TEXT_RETRY_ENABLED", False) sheet = extractor._extract_one(_page()) assert sheet["assertions"], "dark sheet must be impossible with fallback enabled" assert all(a.get("grounding") == "text_layer_fallback" for a in sheet["assertions"]) assert sheet["coverage"]["ratio"] >= 0.6 def test_classic_merge_preserves_graphical_objects(monkeypatch): """Rung-2 merge must never drop vision-only graphical objects.""" def fake(**kw): if kw.get("images_b64"): return {"sheet": {}, "objects": [ {"object_id": "g1", "object_type": "lighting_fixture", "name": "pendant at grid C-4", "source_text": None, "graphical_basis": "16in pendant symbol at grid C-4"}]} return {"sheet": {}, "objects": [ {"object_id": "t1", "object_type": "general_note", "source_text": "ALL SAWN LUMBER IN CONTACT WITH SOIL TO BE " "SOUTHERN PINE, PRESSURE TREATED.", "name": "lumber note"}]} monkeypatch.setattr(extractor, "call_json", fake) sheet = extractor._extract_one(_page()) assert any(a.get("graphical_basis") for a in sheet["assertions"]) assert any("SAWN LUMBER" in (a.get("source_text") or "") for a in sheet["assertions"]) def test_classic_recovers_sheet_number(monkeypatch): text = ("REFLECTED CEILING PLAN\n" "GYP. BD. CEILING 8'-11 3/8\" A.F.F. TYP. FOR ALL STOREFRONT\n" "LED TAPE LIGHT. SEE ELEC. SCONCE 8'-0\" A.F.F., SEE ELEC.\n" "A102") def fake(**kw): if kw.get("images_b64"): return {"sheet": {}, "objects": [ {"object_id": "o1", "name": "RCP ceiling note", "source_text": "GYP. BD. CEILING 8'-11 3/8\" A.F.F. TYP. " "FOR ALL STOREFRONT", "attributes": {"height": "8'-11 3/8\""}}]} return {"sheet": {}, "objects": []} monkeypatch.setattr(extractor, "call_json", fake) sheet = extractor._extract_one(_page(18, text)) assert sheet["sheet_number"] == "A102" def test_classic_skips_retry_when_coverage_healthy(monkeypatch): calls = [] def fake(**kw): calls.append(kw) return {"sheet": {"sheet_number": "S202"}, "objects": [ {"object_id": "o1", "name": "lumber note", "source_text": "ALL SAWN LUMBER IN CONTACT WITH SOIL TO BE " "SOUTHERN PINE, PRESSURE TREATED.", "attributes": {"species": "southern pine"}}, {"object_id": "o2", "name": "sheathing note", "source_text": "ROOF SHEATHING: 5/8\" PLYWOOD, C-D GRADE, " "STRUCTURAL I.", "attributes": {"sheathing": "5/8 plywood"}}]} monkeypatch.setattr(extractor, "call_json", fake) sheet = extractor._extract_one(_page()) assert len(calls) == 1, "healthy coverage must not trigger the text-only rung" assert sheet["coverage"]["ratio"] == 1.0 assert sheet["sheet_number"] == "S202" def test_classic_scanned_page_keeps_failed_sheet_shape(monkeypatch): """No text layer (scanned page): total parse failure keeps the existing 'extraction failed' empty-sheet return — ladder is text-layer-only.""" monkeypatch.setattr(extractor, "call_json", lambda **kw: None) sheet = extractor._extract_one({"page_number": 4, "base64": "AAAA", "text_layer": None}) assert sheet["assertions"] == [] assert "extraction failed" in (sheet.get("sheet_title") or "")