"""Runner-level text-layer flow: excerpt into verify scopes, hi-DPI crop replacement with full-page fallback, and coverage-gap findings.""" import pytest fitz = pytest.importorskip("pymupdf") import backend.agents.runner as runner_mod from backend.agents.base import AgentResult from backend.agents.runner import run_agent_pipeline PAGE_TEXT = "(5) 2X6 STUD PACK AT BEARING" def _make_pdf(path): doc = fitz.open() page = doc.new_page(width=612, height=792) page.insert_text((72, 72), PAGE_TEXT, fontsize=11) doc.save(str(path)) doc.close() return str(path) def _finding(sheets, evidence_text): return { "issue_id": "C1", "severity": "critical", "confidence": "high", "source_stage": "constructability", "sheets": sheets, "description": "stud pack conflict", "evidence": [{"sheet": sheets[0], "source_text": evidence_text}], } def _stub_agent(artifacts): return lambda usage: type("S", (), { "name": "stub", "run": lambda self, scope: AgentResult( scope_id=scope.scope_id, artifacts=list(artifacts)), })() def _patch_pipeline(monkeypatch, finding, verify_sink): monkeypatch.setattr( runner_mod, "convert_pdf_to_images", lambda path: [{"page_number": 1, "base64": "QUJD"}]) monkeypatch.setattr(runner_mod, "SheetExtractorAgent", _stub_agent([ {"sheet_number": "S401", "page_number": 1, "level": "roof", "discipline": "S", "assertions": [ {"text": "(5) 2X6 STUD PACK", "object_type": "framing"}, {"text": "HSS16X4 beam", "object_type": "framing"}, ]}, ])) monkeypatch.setattr(runner_mod, "SheetIndexAgent", _stub_agent([{}])) monkeypatch.setattr(runner_mod, "JurisdictionAgent", _stub_agent([{}])) monkeypatch.setattr(runner_mod, "LinkerAgent", _stub_agent([ {"key": "c1", "location": "roof beam pocket", "assertions": []}, ])) monkeypatch.setattr(runner_mod, "ConflictCriticAgent", _stub_agent([])) monkeypatch.setattr(runner_mod, "CodeAgent", _stub_agent([])) monkeypatch.setattr(runner_mod, "ConstructabilityAgent", _stub_agent([finding])) monkeypatch.setattr(runner_mod, "CompletenessAgent", _stub_agent([])) monkeypatch.setattr( runner_mod, "BrainAgent", lambda usage: type("B", (), { "run": lambda self, findings, sheet_index, jurisdiction: (list(findings), [])})()) class _RecordingVerifier: name = "verify" def __init__(self, usage): pass def run(self, scope): verify_sink.append(scope.payload) return AgentResult(scope_id=scope.scope_id, artifacts=[{ "finding_index": scope.payload["finding_index"], "status": "confirmed", "verdicts": [], }]) monkeypatch.setattr(runner_mod, "EvidenceVerifierAgent", lambda usage: _RecordingVerifier(usage)) def test_verify_scope_carries_text_excerpt_and_crop(monkeypatch, tmp_path): """Evidence text matches the page text layer -> excerpt present and the full-page image is replaced by a hi-DPI crop.""" sink = [] _patch_pipeline(monkeypatch, _finding(["S401"], "(5) 2X6 STUD PACK AT BEARING"), sink) pdf = _make_pdf(tmp_path / "set.pdf") run_agent_pipeline(pdf, out_dir=str(tmp_path), require_review=False) assert len(sink) == 1 payload = sink[0] assert "2X6 STUD PACK" in payload["text_layer_excerpt"] assert payload["images_b64"], "crop must never drop all images" assert payload["images_b64"][0] != "QUJD", "expected crop, not full page" def test_verify_scope_falls_back_to_full_page(monkeypatch, tmp_path): """Evidence text not in the text layer -> keep the full-page image.""" sink = [] _patch_pipeline(monkeypatch, _finding(["S401"], "PENTHOUSE EXHAUST FAN EF-9"), sink) pdf = _make_pdf(tmp_path / "set.pdf") run_agent_pipeline(pdf, out_dir=str(tmp_path), require_review=False) assert len(sink) == 1 assert sink[0]["images_b64"] == ["QUJD"] def test_coverage_gap_becomes_gap_finding(monkeypatch, tmp_path): """Text layer present but zero objects extracted -> failed-scope gap finding survives into the report.""" sink = [] _patch_pipeline(monkeypatch, _finding(["S401"], PAGE_TEXT), sink) # Extractor returns a sheet with NO objects despite a real text layer. monkeypatch.setattr(runner_mod, "SheetExtractorAgent", _stub_agent([ {"sheet_number": "S401", "page_number": 1, "level": "roof", "discipline": "S", "assertions": []}, ])) pdf = _make_pdf(tmp_path / "set.pdf") report = run_agent_pipeline(pdf, out_dir=str(tmp_path), require_review=False) gaps = [f for f in (report.get("validated_issues") or []) if f.get("category") == "analysis_gap"] assert any("extraction gap" in (g.get("description") or "") for g in gaps)