"""Tests that messy notes retrieve and map to the correct MASTER paragraph.""" from __future__ import annotations from docx import Document from backend.config import settings from backend.core import ingest, section_mapper, template_discoverer from backend.core.paragraph_retriever import build_retrieval_query, retrieve_master_paragraphs from backend.core.rag_store import TIER_MASTER, Chunk, get_rag_store def _write_cross_id_bundle(tmp_path): """Canonical-aligned template + standard paragraphs (D1/D2).""" tpl = tmp_path / "template.docx" doc = Document() doc.add_paragraph("D1 Chimney stacks") doc.add_paragraph("D2 Roof coverings") doc.save(str(tpl)) paras = tmp_path / "paragraphs.docx" doc2 = Document() doc2.add_paragraph("D1\nChimney Stacks") doc2.add_paragraph( "The chimney stacks were inspected from ground level using binoculars." ) doc2.add_paragraph("D2\nRoof Coverings") doc2.add_paragraph( "The roof covering comprises slate laid to pitched timber rafters." ) doc2.save(str(paras)) return tpl, paras def test_build_retrieval_query_uses_observations(): q = build_retrieval_query( "Chimney Stacks", ["stack cracked", "mortar open joints at crown"], section_id="D1", ) assert "D1 Chimney Stacks:" in q assert "mortar" in q def test_chimney_notes_retrieve_d1_not_d2(tmp_path, monkeypatch): monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data")) tpl, paras = _write_cross_id_bundle(tmp_path) tenant = "t-match" ingest.ingest_report_template(tenant, tpl) ingest.ingest_standard_paragraphs(tenant, paras) schema = template_discoverer.load_schema(tenant) assert schema.section_alias_map.get("D1") == "D1" hits = retrieve_master_paragraphs( tenant, section_label="Chimney stacks", paragraph_section_id=schema.paragraph_section_id("D1"), observations=["stack cracked mortar missing"], ) assert hits assert hits[0].section_id == "D1" assert "chimney" in hits[0].text.lower() assert "slate" not in hits[0].text.lower() def test_roof_notes_retrieve_d2_not_d1(tmp_path, monkeypatch): monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data")) tpl, paras = _write_cross_id_bundle(tmp_path) tenant = "t-roof" ingest.ingest_report_template(tenant, tpl) ingest.ingest_standard_paragraphs(tenant, paras) schema = template_discoverer.load_schema(tenant) hits = retrieve_master_paragraphs( tenant, section_label="Roof coverings", paragraph_section_id=schema.paragraph_section_id("D2"), observations=["slate tile slipped on south slope"], ) assert hits assert hits[0].section_id == "D2" assert "slate" in hits[0].text.lower() assert "chimney" not in hits[0].text.lower() def test_section_strict_excludes_other_sections(): store = get_rag_store() store.clear_tier("t-strict", TIER_MASTER) store.ingest_document( "t-strict", "p", [ Chunk(text="Chimney stack mortar decay noted.", section_id="D1", tier=TIER_MASTER), Chunk(text="Roof slate covering slipped tiles.", section_id="D2", tier=TIER_MASTER), ], tier=TIER_MASTER, ) hits = store.search_for_generation( "t-strict", "slate roof tile slipped", section_id="D2", top_k=3, section_strict=True, ) assert hits assert all(h.section_id == "D2" for h in hits) def test_orphan_note_routed_by_rag_topic(tmp_path, monkeypatch): """Notes with no section header still match a MASTER paragraph by topic.""" monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data")) tpl, paras = _write_cross_id_bundle(tmp_path) tenant = "t-orphan" ingest.ingest_report_template(tenant, tpl) ingest.ingest_standard_paragraphs(tenant, paras) ingest.ingest_reference(tenant, paras) result = section_mapper.generate_report( tenant, "slate tile slipped on the south slope of the roof", interference_level="maximum", ) d3 = next(s for s in result.sections if s.section_id == "D2") assert d3.status == "OK" assert "slate" in d3.text.lower() assert "south" in d3.text.lower() def test_end_to_end_d1_notes_use_d1_rag_source(tmp_path, monkeypatch): monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data")) tpl, paras = _write_cross_id_bundle(tmp_path) tenant = "t-e2e" ingest.ingest_report_template(tenant, tpl) ingest.ingest_standard_paragraphs(tenant, paras) ingest.ingest_reference(tenant, paras) result = section_mapper.generate_report( tenant, "D1: stack cracked mortar open\n\nD2: slate slipped south slope", interference_level="maximum", ) d1 = next(s for s in result.sections if s.section_id == "D1") d3 = next(s for s in result.sections if s.section_id == "D2") assert d1.status != "NO_RAG_MATCH" assert d3.status != "NO_RAG_MATCH" assert d1.reference_sources or d1.rag_sources assert d3.reference_sources or d3.rag_sources assert any( "paragraphs.docx" in src.report_filename for src in d1.reference_sources ) or any("paragraphs.docx" in s for s in d1.rag_sources) assert "chimney" in d1.text.lower() or "stack" in d1.text.lower() assert "slate" in d3.text.lower() or "roof" in d3.text.lower() assert "slate" not in d1.text.lower() or "chimney" in d1.text.lower()