Model/tests/orchestration/test_modelling_baseline_coherence.py
Khalim Conn-Kowlessar 223ef7c196 Report missing baselines once per batch, and seed the harness's own 🟩
The modelling_e2e handler models one property per run_modelling call, so a
per-property "no persisted baseline" line would be one per property across a
whole re-model. The harness has no Baseline stage by design and cannot drift
from one, so it now establishes its own the way the Baseline orchestrator
would; the Postgres-backed lane is where the assertion has something to catch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:46:25 +00:00

111 lines
4.1 KiB
Python

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