Files
Conflict_Checker/tests/agents/test_text_layer_flow.py
T
woogi d37ac8c1c7
Docker Release / build-and-push (push) Successful in 1m8s
Docker Release / release (push) Skipped
feat: refocus on drawings — code/ADA gated off, drawing-integrity wave, Brain-directed clarification
- ENABLE_CODE_REVIEW flag (default off): skips code/ADA/jurisdiction review
  path in both pipelines; nothing deleted, one env flag to restore.
- Per-sheet Drawing Integrity QA wave (agent + classic, default on):
  dangling refs, on-sheet contradictions, dimension sanity, missing sheet
  essentials, tag hygiene. New DrawingIntegrityAgent + classic stage.
- Broadened conflict critic: intra-sheet + same-discipline contradictions,
  not just cross-discipline.
- Wave 6.5 Brain-directed clarification (bounded hub-and-spoke): Brain names
  uncertain findings, verify_evidence requests route through the wave-5b
  verifier; refuted findings suppressed. One planning call + capped verifies,
  single iteration. Shared _build_verify_scopes across 5b and 6.5.
- Config knobs, .env.example, frontend copy, tests (182 passing).
2026-08-20 15:10:32 -05:00

130 lines
5.0 KiB
Python

"""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), []),
"plan_clarifications": lambda self, prioritized: []})())
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)