RICS / backend /tests /test_pipeline_accuracy_fixes.py
StormShadow308's picture
Add demo documentation and Docker setup for v2 report generation system
aad7814
Raw
History Blame Contribute Delete
7.1 kB
"""Tests for the surgical accuracy fixes: property guard, note extraction,
and 'specified' token resolution."""
from __future__ import annotations
from types import SimpleNamespace
from backend.core import property_blocklist
from backend.core.property_blocklist import (
categories_for,
filter_hits_by_property,
is_paragraph_compatible,
)
from backend.core.specified_resolver import resolve_specified_tokens
from backend.core.survey_notes import build_property_context, parse_notes
_NOTES = (
"Mid-terraced Victorian townhouse, freehold, built 1887, four storeys.\n"
"Within a conservation area. Flood Zone 1. EPC band D, score 58.\n"
"Welsh slate pitched roof; north slope ~15% slates damaged; reroof Β£12k-Β£18k.\n"
"Front chimney stack render spalling on the south face.\n"
"Viessmann Vitodens 200-W condensing boiler in the basement, 22 radiators.\n"
"180L unvented cylinder.\n"
"Southeast corner, first floor, diagonal stepped crack 4mm max.\n"
"Rear garage corrugated roof panels likely contain asbestos.\n"
"London Plane tree 4.5m from the rear elevation.\n"
"Total essential repairs Β£53k-Β£134k."
)
# ── Fix 3: structured note extraction ────────────────────────────────────────
def test_parse_notes_identity_fields():
n = parse_notes(_NOTES)
assert n.construction_year == 1887
assert n.storeys == 4
assert n.tenure == "freehold"
assert n.is_conservation_area is True
assert n.flood_zone == "Zone 1"
assert n.epc_band == "D"
assert n.epc_score == 58
assert "terraced" in n.property_type.lower()
def test_parse_notes_flags_and_costs():
n = parse_notes(_NOTES)
assert n.has_asbestos is True
assert "asbestos" in n.asbestos_location.lower()
assert n.has_structural_crack is True
assert "4mm" in n.crack_description.lower().replace(" ", "")
assert n.tree_species == "london plane"
assert n.tree_distance_m == 4.5
assert (12000, 18000) in n.cost_estimates.values()
assert n.cost_total_essential == (53000, 134000)
def test_parse_notes_section_findings_populated():
n = parse_notes(_NOTES)
assert isinstance(n.section_findings, dict)
# Keyworded lines should route somewhere rather than vanish entirely.
assert sum(len(v) for v in n.section_findings.values()) >= 1
def test_build_property_context_session_overrides_notes():
n = parse_notes(_NOTES)
ctx = build_property_context(n, property_type="detached house", tenure="leasehold")
assert ctx["property_type"] == "detached house"
assert ctx["tenure"] == "leasehold"
assert ctx["construction_year"] == 1887
def test_build_property_context_falls_back_to_notes():
n = parse_notes(_NOTES)
ctx = build_property_context(n)
assert "terraced" in ctx["property_type"].lower()
assert ctx["tenure"] == "freehold"
# ── Fix 1: property-type guard ───────────────────────────────────────────────
def test_categories_for_house_and_terraced():
assert categories_for({"property_type": "mid-terraced townhouse"}) >= {"house", "terraced"}
assert categories_for({"property_type": "purpose-built flat"}) == {"flat"}
assert categories_for({"property_type": ""}) == set()
assert categories_for(None) == set()
def test_house_blocks_flat_terminology():
ctx = {"property_type": "mid-terraced house"}
flat_para = (
"The managing agent should provide the PEEP and confirm whether a stay put "
"policy applies to the communal areas and flat entrance doors."
)
assert is_paragraph_compatible(flat_para, ctx) is False
def test_terraced_blocks_front_driveway_and_integral_garage():
ctx = {"property_type": "mid-terraced townhouse"}
assert is_paragraph_compatible("There is a front driveway laid with block paving.", ctx) is False
assert is_paragraph_compatible("The integral garage is accessed from the hallway.", ctx) is False
def test_flat_report_keeps_communal_content():
ctx = {"property_type": "second-floor flat"}
flat_para = "The communal areas are maintained by the managing agent."
assert is_paragraph_compatible(flat_para, ctx) is True
def test_unknown_property_type_never_filters():
para = "The communal areas are maintained by the managing agent."
assert is_paragraph_compatible(para, {"property_type": ""}) is True
assert is_paragraph_compatible(para, None) is True
def test_filter_hits_drops_incompatible():
ctx = {"property_type": "mid-terraced house"}
hits = [
SimpleNamespace(text="The chimney stack mortar is eroded and needs repointing."),
SimpleNamespace(text="Refer to the managing agent regarding the communal areas."),
]
kept = filter_hits_by_property(hits, ctx)
assert len(kept) == 1
assert "chimney" in kept[0].text
def test_guard_disabled_passthrough(monkeypatch):
monkeypatch.setattr(property_blocklist.settings, "property_guard_enabled", False)
ctx = {"property_type": "mid-terraced house"}
assert is_paragraph_compatible("communal areas managing agent", ctx) is True
# ── Fix 2: 'specified' / redaction token resolution ──────────────────────────
def test_redaction_tokens_become_visible_placeholder():
para = "Where partly visible, there are soil pipes to the [REDACTED_NAME_1] side of the property."
out = resolve_specified_tokens(para, section_code="F6")
assert "REDACTED" not in out
assert "specified" not in out.lower()
assert "[SURVEYOR TO CONFIRM]" in out
def test_sentence_initial_token_dropped_and_capitalised():
para = "[REDACTED_NAME_1] repairs tend to be expensive due to scaffolding."
out = resolve_specified_tokens(para, section_code="D1")
assert out.startswith("Repairs tend to be expensive")
assert "REDACTED" not in out
def test_double_bracket_token_resolved():
para = "The roof is of hipped pitch and [[REDACTED_NAME_2]] construction."
out = resolve_specified_tokens(para, section_code="D2")
assert "[[" not in out
assert "REDACTED" not in out
assert "[SURVEYOR TO CONFIRM] construction" in out
def test_standalone_token_line_removed():
para = "Roof inspected.\n[REDACTED_ADDRESS_1]\nNo defects noted."
out = resolve_specified_tokens(para, section_code="D2")
assert "REDACTED" not in out
assert "Roof inspected." in out
assert "No defects noted." in out
def test_legitimate_specified_word_is_preserved():
# 'specified' is a normal English word; the resolver must not corrupt it.
para = "The works were completed using the specified materials and methods."
out = resolve_specified_tokens(para, section_code="E3")
assert out == para
def test_clean_paragraph_unchanged():
para = "The chimney stack mortar is eroded and requires repointing. Condition Rating 2."
out = resolve_specified_tokens(para, section_code="D1")
assert out == para