From d51801089b142316541dd5908a811db0af8b333b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 11:42:54 +0000 Subject: [PATCH] =?UTF-8?q?Fabric=20gets=20first=20claim=20on=20the=20budg?= =?UTF-8?q?et=20for=20every=20goal,=20not=20just=20Increasing=20EPC=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents behaviour already delivered: with no SAP target both phases run max-gain, so the envelope still consumes the budget first. Test passed on arrival. Co-Authored-By: Claude Fable 5 --- .../modelling/test_optimiser_fabric_first.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/domain/modelling/test_optimiser_fabric_first.py b/tests/domain/modelling/test_optimiser_fabric_first.py index 1eb19b696..09b01e646 100644 --- a/tests/domain/modelling/test_optimiser_fabric_first.py +++ b/tests/domain/modelling/test_optimiser_fabric_first.py @@ -298,6 +298,30 @@ def test_no_fabric_candidates_proceeds_straight_to_the_full_pool() -> None: assert abs(package.score.sap_continuous - 80.0) <= 1e-9 +def test_without_a_target_fabric_still_gets_first_claim_on_the_budget() -> None: + # Arrange — a max-gain goal (no SAP target). Plain max-gain would spend the + # whole £8000 on the heat pump (+20); fabric first commits the wall (+5) + # before the remainder is considered, pricing the heat pump out. + 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 — no target: the flag applies to every goal, not just Increasing EPC. + package: OptimisedPackage = optimise_package_fabric_first( + groups=groups, + scorer=scorer, + baseline_epc=build_epc(), + budget=8000.0, + target_sap=None, + ) + + # Assert — wall first; the heat pump no longer fits the leftover £7000. + assert _selected_types(package) == {"cavity_wall_insulation"} + assert abs(package.score.sap_continuous - 65.0) <= 1e-9 + + class _InteractionScorer: """A stub whose boiler gain collapses once the wall is insulated (+10 raw, +3 post-fabric) while the heat pump's holds (+8 either way) — so a phase 2