"""Unit tests for the per-sheet DrawingIntegrityAgent and scope builder.""" import backend.agents.integrity_agent as integ from backend.agents.base import AgentScope, AgentUsage from backend.agents.integrity_agent import ( DrawingIntegrityAgent, build_integrity_scopes, ) from backend import config def _sheet(page, sheet_number, n_assertions): return { "sheet_number": sheet_number, "page_number": page, "sheet_title": f"Sheet {sheet_number}", "discipline": "Architectural", "assertions": [ {"object_type": "note", "source_text": f"note {i}"} for i in range(n_assertions) ], } def test_build_scopes_skips_sparse_sheets(monkeypatch): monkeypatch.setattr(config, "INTEGRITY_MIN_ASSERTIONS", 3) sheets = [ _sheet(1, "A101", 5), # kept _sheet(2, "A102", 2), # skipped (too sparse) _sheet(3, "A103", 3), # kept (== floor) ] page_to_b64 = {1: "IMG1", 2: "IMG2", 3: "IMG3"} page_to_text = {1: "text one", 2: "text two", 3: "text three"} scopes = build_integrity_scopes(sheets, page_to_b64, page_to_text) ids = sorted(s.scope_id for s in scopes) assert ids == ["integrity:1", "integrity:3"] # Each scope carries its own page image + text layer. by_id = {s.scope_id: s for s in scopes} assert by_id["integrity:1"].payload["image_b64"] == "IMG1" assert by_id["integrity:1"].payload["text_layer"] == "text one" def test_agent_parses_and_anchors_sheet(monkeypatch): """Findings with blank sheets get anchored to the scope's sheet number.""" captured = {} def fake_call_json(**kwargs): captured.update(kwargs) return {"issues": [ {"issue_id": "DI-1", "severity": "high", "confidence": "high", "category": "dangling_reference", "sheets": [], "description": "Detail callout 5/A101 has no detail 5 on this sheet", "evidence": [{"sheet": "A101", "source_text": "5/A101"}]}, ]} monkeypatch.setattr(integ, "call_json", fake_call_json) sheet = _sheet(1, "A101", 5) scope = AgentScope("integrity:1", { "sheet": sheet, "page_number": 1, "image_b64": "IMG1", "text_layer": "the deterministic text layer", }) result = DrawingIntegrityAgent(AgentUsage()).run(scope) assert result.error == "" assert len(result.artifacts) == 1 finding = result.artifacts[0] assert finding["source_stage"] == "drawing_integrity" assert finding["sheets"] == ["A101"] # anchored assert finding["agent"] == "drawing_integrity" assert finding["scope_id"] == "integrity:1" # The image + text layer reached the model. assert captured["images_b64"] == ["IMG1"] assert "the deterministic text layer" in captured["user_text"] def test_agent_empty_issues_is_clean(monkeypatch): monkeypatch.setattr(integ, "call_json", lambda **k: {"issues": []}) scope = AgentScope("integrity:1", { "sheet": _sheet(1, "A101", 5), "page_number": 1, "image_b64": "IMG1", "text_layer": "t", }) result = DrawingIntegrityAgent(AgentUsage()).run(scope) assert result.error == "" assert result.artifacts == [] def test_agent_survives_call_failure(monkeypatch): def boom(**kwargs): raise RuntimeError("model exploded") monkeypatch.setattr(integ, "call_json", boom) scope = AgentScope("integrity:1", { "sheet": _sheet(1, "A101", 5), "page_number": 1, "image_b64": "IMG1", "text_layer": "t", }) result = DrawingIntegrityAgent(AgentUsage()).run(scope) assert "model exploded" in result.error assert result.artifacts == []