From e880ae868c61fb40f53d67f50acf8a37658b1d62 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 11:36:43 +0000 Subject: [PATCH] =?UTF-8?q?Fabric=20short=20of=20the=20target=20is=20toppe?= =?UTF-8?q?d=20up=20with=20non-fabric=20measures=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../modelling/test_optimiser_fabric_first.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/domain/modelling/test_optimiser_fabric_first.py b/tests/domain/modelling/test_optimiser_fabric_first.py index 483112e69..d1011ed73 100644 --- a/tests/domain/modelling/test_optimiser_fabric_first.py +++ b/tests/domain/modelling/test_optimiser_fabric_first.py @@ -117,3 +117,30 @@ def test_fabric_reaching_the_target_excludes_non_fabric_measures() -> None: assert abs(package.score.sap_continuous - 70.0) <= 1e-9 +def test_fabric_short_of_target_is_topped_up_with_non_fabric_measures() -> None: + # Arrange — all the fabric there is (the wall, +5) cannot reach the target; + # phase 2 must add the heat pump on top of the retained fabric. + groups: list[list[ScoredOption]] = [ + [_scored("cavity_wall_insulation", gain=5.0, cost=1000.0, overlay=_WALL_OVERLAY)], + [_scored("air_source_heat_pump", gain=20.0, cost=8000.0, overlay=_HEATING_OVERLAY)], + ] + scorer = _StubScorer(base=60.0, wall=5.0, roof=0.0, heating=20.0) + + # Act — target 75 (gain 15); fabric alone tops out at 65. + package: OptimisedPackage = optimise_package_fabric_first( + groups=groups, + scorer=scorer, + baseline_epc=build_epc(), + budget=20000.0, + target_sap=75.0, + ) + + # Assert — the fabric is kept and the heat pump lands on top of it; the + # score is the truthful whole-package figure (60 + 5 + 20). + assert _selected_types(package) == { + "cavity_wall_insulation", + "air_source_heat_pump", + } + assert abs(package.score.sap_continuous - 85.0) <= 1e-9 + +