"""Modelling and the Baseline stage must agree on what the Property scores now. Per ADR-0002 the persisted Effective Performance is what the app reports as the Property's current state, and a Plan's `post_*` figures are read against it. The Modelling stage re-scores the Effective EPC itself rather than reading that row, so the two can drift apart silently — and when they do, the app subtracts one baseline from the other and reports a gain nobody modelled (#1652 / #1655: property 749719 showed "+9.65 SAP" on a plan whose own CO2, bill and consumption savings were all exactly 0.0). Modelling keeps its own re-score — anchoring the SAP to the persisted integer would leave the Plan's SAP disagreeing with its own CO2 / primary energy / bill, which all come off the same SapResult. Instead the divergence is made *loud*, so a future drift surfaces as an error instead of a phantom gain in a board pack. """ from __future__ import annotations import logging from typing import Optional import pytest from datatypes.epc.domain.epc import Epc from domain.property_baseline.performance import Performance from domain.property_baseline.property_baseline_performance import ( PropertyBaselinePerformance, ) from orchestration.modelling_orchestrator import ( _check_baseline_coherence, # pyright: ignore[reportPrivateUsage] ) def _persisted(effective_sap: int) -> PropertyBaselinePerformance: """A Baseline Performance whose Effective half scores `effective_sap`.""" effective = Performance( sap_score=effective_sap, epc_band=Epc.from_sap_score(effective_sap), co2_emissions=2.0, primary_energy_intensity=200, ) return PropertyBaselinePerformance( lodged=None, effective=effective, rebaseline_reason="physical_state_changed", space_heating_kwh=0.0, water_heating_kwh=0.0, ) def test_divergence_beyond_rounding_is_logged_as_an_error( caplog: pytest.LogCaptureFixture, ) -> None: # Arrange — the 749719 case: the Baseline stage persisted an Effective SAP of # 63, but Modelling scored the same Property at 72.65. persisted: Optional[PropertyBaselinePerformance] = _persisted(63) # Act with caplog.at_level(logging.ERROR): _check_baseline_coherence(749719, 72.65, persisted) # Assert — loud, and it names the Property and both figures. assert "749719" in caplog.text assert "72.65" in caplog.text assert "63" in caplog.text assert any(r.levelno == logging.ERROR for r in caplog.records) def test_a_baseline_agreeing_within_rounding_is_silent( caplog: pytest.LogCaptureFixture, ) -> None: # Arrange — the persisted Effective SAP is the *rounded* score, so a # continuous 63.22 against a stored 63 is agreement, not divergence. persisted: Optional[PropertyBaselinePerformance] = _persisted(63) # Act with caplog.at_level(logging.WARNING): _check_baseline_coherence(749719, 63.22, persisted) # Assert assert caplog.records == [] def test_a_property_with_no_persisted_baseline_is_reported_not_logged( caplog: pytest.LogCaptureFixture, ) -> None: # Arrange — the Property never went through the Baseline stage. A data gap # should degrade one Property, not abort the batch (ADR-0012 reserves the # abort for a load-bearing calculator raise) — but it must not log per # Property either: whole batches legitimately have no Baseline stage (the # database-free harness the modelling_e2e handler runs on), and one line per # Property buries the real errors in tens of thousands of duplicates. # Act with caplog.at_level(logging.WARNING): missing = _check_baseline_coherence(749719, 63.22, None) # Assert — reported back to the caller to aggregate, silent in itself. assert missing is True assert caplog.records == [] def test_a_property_with_a_baseline_is_not_reported_as_missing() -> None: # Arrange persisted: Optional[PropertyBaselinePerformance] = _persisted(63) # Act missing = _check_baseline_coherence(749719, 63.22, persisted) # Assert assert missing is False