Lodge explicit zero roof insulation for a surveyed "Insulation At: None" 🟥

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-14 21:26:42 +00:00
parent 178af2f044
commit 66e0adf619

View file

@ -74,6 +74,75 @@ class TestRoofConstructionTypeCoding:
assert extension_part.roof_construction_type == "Flat"
class TestRoofInsulationNoneCoding:
"""A surveyed roof-space "Insulation At: None" is the assessor explicitly
recording ZERO loft insulation `from_site_notes` must lodge an explicit
`roof_insulation_thickness=0`, not leave it `None` ("unknown"). With `None`
the calculator's `u_roof` falls through to the age-band "assumed insulated"
default (e.g. 0.16 W/m²K at band J/K) instead of the Table 16 uninsulated
row (2.30) a ~14x understatement of roof heat loss (issue #1590 bug 2;
fixtures 507665533138 / 507670893756 over-rate by ~7 SAP each).
"""
def test_insulation_at_none_lodges_explicit_zero_thickness(self) -> None:
# Arrange — the surveyor recorded "Insulation At: None" (no thickness
# line follows; there is no insulation to measure).
data = load("pashub_rdsap_site_notes_example1.json")
data["roof_space"]["main_building"]["insulation_at"] = "None"
data["roof_space"]["main_building"]["insulation_thickness_mm"] = None
survey = from_dict(PasHubRdSapSiteNotes, data)
# Act
result = EpcPropertyDataMapper.from_site_notes(survey)
# Assert — explicit zero, so u_roof takes the uninsulated branch.
assert result.sap_building_parts[0].roof_insulation_thickness == 0
def test_surveyed_thickness_still_passes_through(self) -> None:
# Arrange — the fixture's as-lodged "Joists" + 100mm.
survey = from_dict(
PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json")
)
# Act
result = EpcPropertyDataMapper.from_site_notes(survey)
# Assert
assert result.sap_building_parts[0].roof_insulation_thickness == 100
def test_blank_insulation_at_stays_unknown(self) -> None:
# Arrange — a blank lodging is "not surveyed", NOT "no insulation";
# it must stay None so the age-band default still applies.
data = load("pashub_rdsap_site_notes_example1.json")
data["roof_space"]["main_building"]["insulation_at"] = ""
data["roof_space"]["main_building"]["insulation_thickness_mm"] = None
survey = from_dict(PasHubRdSapSiteNotes, data)
# Act
result = EpcPropertyDataMapper.from_site_notes(survey)
# Assert
assert result.sap_building_parts[0].roof_insulation_thickness is None
def test_extension_insulation_at_none_lodges_explicit_zero(self) -> None:
# Arrange — same behaviour on the extension building part.
data = load("pashub_rdsap_site_notes_example2.json")
data["roof_space"]["extensions"][0]["insulation_at"] = "None"
data["roof_space"]["extensions"][0]["insulation_thickness_mm"] = None
survey = from_dict(PasHubRdSapSiteNotes, data)
# Act
result = EpcPropertyDataMapper.from_site_notes(survey)
# Assert
extension_part = next(
bp
for bp in result.sap_building_parts
if bp.identifier != BuildingPartIdentifier.MAIN
)
assert extension_part.roof_insulation_thickness == 0
class TestCountryCode:
"""`from_site_notes` must set `country_code` so the cascade's England /
Wales / Scotland / NI U-value tables (`u_floor`, `u_basement_floor`,