RICS / backend /tests /conftest.py
StormShadow308's picture
Add demo documentation and Docker setup for v2 report generation system
aad7814
Raw
History Blame Contribute Delete
7.53 kB
"""Shared test fixtures.
Tests run fully offline: the FAISS store is backed by a deterministic fake
embedder and no OpenAI key is configured, so mapping/grounding take their
deterministic fallback paths.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from docx import Document
from backend.config import settings
from backend.core import embeddings, notes_parser, pii_scrubber, rag_store
from backend.core.rics_canonical_l3 import build_section_anchor_text, iter_canonical_leaf_sections
_ANCHOR_OVERLAP_STOP = frozenset({
"about", "elements", "other", "property", "section", "inside", "outside",
"survey", "rics", "level", "typical", "matters", "engagement", "terms",
"looked", "overall", "fine", "noted", "specific", "defects", "general",
"inspection", "opinion", "declaration", "diagram", "recommendations",
"the", "and", "not", "with", "for", "are", "was", "from", "this", "that",
"have", "has", "were", "its", "any", "all", "may", "can", "your", "our",
})
class AnchorSemanticFakeEmbedder(embeddings.FakeEmbedder):
"""Deterministic embedder that approximates anchor similarity for offline tests."""
def __init__(self, dim: int = 384) -> None:
super().__init__(dim)
self._anchor_texts: dict[str, str] = {}
self._anchor_vecs: dict[str, list[float]] = {}
def register_anchors(self, anchors: dict[str, str]) -> None:
self._anchor_texts = dict(anchors)
self._anchor_vecs = {sid: super()._vec(text) for sid, text in anchors.items()}
@staticmethod
def _token_set(text: str) -> set[str]:
return {
w
for w in re.findall(r"[a-z0-9]{3,}", (text or "").lower())
if w not in _ANCHOR_OVERLAP_STOP
}
def _vec(self, text: str) -> list[float]:
base = super()._vec(text)
if not self._anchor_texts:
return base
words = self._token_set(text)
best_sid: str | None = None
best_overlap = 0
for sid, anchor in self._anchor_texts.items():
overlap = len(words & self._token_set(anchor))
if overlap > best_overlap:
best_overlap = overlap
best_sid = sid
if best_sid and best_overlap > 0:
anchor_vec = self._anchor_vecs[best_sid]
alpha = min(0.95, 0.55 + 0.12 * best_overlap)
blended = [alpha * a + (1.0 - alpha) * b for a, b in zip(anchor_vec, base)]
norm = sum(x * x for x in blended) ** 0.5 or 1.0
return [x / norm for x in blended]
return base
@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
monkeypatch.setattr(settings, "data_dir", str(tmp_path / "data"))
monkeypatch.setattr(settings, "openai_api_key", "")
monkeypatch.setattr(settings, "pii_use_spacy", False)
monkeypatch.setattr(settings, "reference_auto_ingest_enabled", False)
fake = AnchorSemanticFakeEmbedder()
anchors = {
sid: build_section_anchor_text(sid)
for sid, _, _, _ in iter_canonical_leaf_sections()
}
fake.register_anchors(anchors)
monkeypatch.setattr(embeddings, "get_embedder", lambda: fake)
monkeypatch.setattr(rag_store, "get_embedder", lambda: fake)
embeddings.reset_embedder()
rag_store.reset_rag_store()
pii_scrubber.reset_nlp_cache()
notes_parser.reset_section_anchors()
notes_parser.initialize_section_anchors()
yield
notes_parser.reset_section_anchors()
rag_store.reset_rag_store()
def _write_report_template(path: Path) -> Path:
"""Minimal report template: section structure only."""
doc = Document()
doc.add_paragraph("A About the inspection")
doc.add_paragraph("E1 Chimney Stacks")
doc.add_paragraph("E2 Roof Coverings")
doc.add_paragraph("G4 Central Heating")
doc.save(str(path))
return path
def _write_standard_paragraphs(path: Path, *, with_rating: bool = False) -> Path:
"""Standard paragraphs Word master: headings + boilerplate body."""
doc = Document()
doc.add_paragraph("Section E1 - Chimney Stacks")
body = "The chimney stacks were inspected from ground level using binoculars."
if with_rating:
body += " Condition Rating 2 where defects require attention."
doc.add_paragraph(body)
doc.add_paragraph("Section E2 - Roof Coverings")
doc.add_paragraph("The roof covering comprises slate laid to pitched timber rafters.")
doc.add_paragraph("Section G4 - Central Heating")
doc.add_paragraph("The property is served by a gas-fired central heating system.")
doc.save(str(path))
return path
@pytest.fixture
def report_template_docx(tmp_path) -> Path:
return _write_report_template(tmp_path / "report_template.docx")
@pytest.fixture
def standard_paragraphs_docx(tmp_path) -> Path:
return _write_standard_paragraphs(tmp_path / "standard_paragraphs.docx")
@pytest.fixture
def standard_paragraphs_rated_docx(tmp_path) -> Path:
return _write_standard_paragraphs(tmp_path / "standard_paragraphs_rated.docx", with_rating=True)
# Legacy alias used by template_discoverer unit tests.
@pytest.fixture
def master_docx(standard_paragraphs_docx) -> Path:
return standard_paragraphs_docx
@pytest.fixture
def master_docx_rated(standard_paragraphs_rated_docx) -> Path:
return standard_paragraphs_rated_docx
@pytest.fixture
def client(configured_master):
from backend.main import app
from fastapi.testclient import TestClient
with TestClient(app) as c:
yield c
def _write_past_report(path: Path) -> Path:
"""Simulated uploaded past report for REFERENCE-tier generation."""
doc = Document()
doc.add_paragraph("Section E1 - Chimney Stacks")
doc.add_paragraph(
"The chimney stacks were inspected from ground level using binoculars. "
"Inspection was limited to visible external surfaces at the time of survey."
)
doc.add_paragraph("Section E2 - Roof Coverings")
doc.add_paragraph(
"The roof coverings were inspected from ground level using binoculars and, "
"where accessible, from within the roof void. The main roof covering "
"comprises natural slate tiles laid to pitched timber rafters with underfelt "
"and counter battens."
)
doc.add_paragraph("Section G4 - Central Heating")
doc.add_paragraph(
"The property was served by a gas-fired central heating system with "
"radiators to most rooms."
)
doc.save(str(path))
return path
@pytest.fixture
def configured_master(monkeypatch, report_template_docx, standard_paragraphs_docx) -> Path:
"""Point config at a split report template + standard paragraphs bundle.
This fixture models an operator that explicitly configures AND enables a shared
master, so it opts into ``master_template_auto_ingest`` (default-off in prod).
"""
folder = report_template_docx.parent
_write_past_report(folder / "past_report_sample.docx")
monkeypatch.setattr(settings, "master_template_dir", str(folder))
monkeypatch.setattr(settings, "report_template_filename", report_template_docx.name)
monkeypatch.setattr(settings, "standard_paragraphs_filename", standard_paragraphs_docx.name)
monkeypatch.setattr(settings, "master_template_filename", standard_paragraphs_docx.name)
monkeypatch.setattr(settings, "master_template_auto_ingest", True)
monkeypatch.setattr(settings, "reference_auto_ingest_enabled", True)
return standard_paragraphs_docx