Model/tests/harness/test_plan_table.py
Khalim Conn-Kowlessar 9ef97be958 refactor(modelling): type measure_type fields as MeasureType
Tighten the recommendation/plan vocabulary off generic str:
MeasureOption.measure_type and PlanMeasure.measure_type are now MeasureType
(also _GlazingTarget.measure_type, MeasureDependency.triggers ->
frozenset[MeasureType], and the optimiser's chosen/required-type locals).
Because MeasureType is a StrEnum the change is transparent to persistence
(the `recommendation` varchar column), the optimiser group-by key, and every
`== "solar_pv"`-style comparison — so pyright now enforces the enum at every
construction site with no runtime behaviour change.

The catalogue boundary stays str: ProductRepository.get(measure_type: str)
and Product.measure_type are unchanged (they map arbitrary DB/JSON rows), so
the fake product repos in tests need no edit. Test construction helpers coerce
their str arg via MeasureType(...); direct constructions use members.

Suite green: tests/domain/modelling + orchestration + harness 253 pass + 3
xfail; pyright clean on production + tests (pre-existing moto + property-
override-rowcount baselines untouched).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:13:31 +00:00

91 lines
3 KiB
Python

"""The sense-check table the DB-less harness prints for a Plan."""
from __future__ import annotations
from domain.modelling.measure_type import MeasureType
from domain.modelling.plan import Plan, PlanMeasure
from domain.modelling.recommendation import Cost
from domain.modelling.scoring.package_scorer import Score
from domain.modelling.scoring.scoring import MeasureImpact
from harness.plan_table import format_plan_table
def _plan() -> Plan:
baseline = Score(
sap_continuous=57.4, co2_kg_per_yr=3000.0, primary_energy_kwh_per_yr=300.0
)
post = Score(
sap_continuous=61.2, co2_kg_per_yr=2100.0, primary_energy_kwh_per_yr=240.0
)
measures = (
PlanMeasure(
measure_type=MeasureType.CAVITY_WALL_INSULATION,
description="Cavity wall insulation",
cost=Cost(total=500.0, contingency_rate=0.1),
impact=MeasureImpact(
sap_points=3.1,
co2_savings_kg_per_yr=600.0,
energy_savings_kwh_per_yr=1200.0,
),
kwh_savings=900.0,
energy_cost_savings=120.0,
),
PlanMeasure(
measure_type=MeasureType.MECHANICAL_VENTILATION,
description="Mechanical extract ventilation",
cost=Cost(total=900.0, contingency_rate=0.26),
impact=MeasureImpact(
sap_points=-1.3,
co2_savings_kg_per_yr=-50.0,
energy_savings_kwh_per_yr=-200.0,
),
kwh_savings=-150.0,
energy_cost_savings=-30.0,
),
)
return Plan(measures=measures, baseline=baseline, post_retrofit=post)
def test_table_shows_valuation_uplift_with_pounds() -> None:
# Arrange — a £200k property modelled D (57.4) -> C (72.0).
baseline = Score(
sap_continuous=57.4, co2_kg_per_yr=3000.0, primary_energy_kwh_per_yr=300.0
)
post = Score(
sap_continuous=72.0, co2_kg_per_yr=2100.0, primary_energy_kwh_per_yr=240.0
)
plan = Plan(
measures=(),
baseline=baseline,
post_retrofit=post,
current_market_value=200_000.0,
)
# Act
table: str = format_plan_table(plan)
# Assert — the valuation line shows the average % uplift and its £ forms.
assert "valuation uplift" in table
assert "+2.5%" in table
assert "£5,000" in table
assert "£205,000" in table
def test_table_shows_package_transition_and_each_measure() -> None:
# Arrange
plan: Plan = _plan()
# Act
table: str = format_plan_table(plan)
# Assert — the package SAP transition (both bands resolve to D), and each
# measure's signed SAP contribution against its type.
assert "57.4" in table
assert "61.2" in table
assert "(D)" in table
assert "cavity_wall_insulation" in table
assert "+3.1" in table
assert "mechanical_ventilation" in table
assert "-1.3" in table
# The package cost of works (500 + 900) appears.
assert "1,400" in table