"""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