diff --git a/tests/domain/modelling/test_optimiser_fabric_first.py b/tests/domain/modelling/test_optimiser_fabric_first.py index 71ce7a4f6..1eb19b696 100644 --- a/tests/domain/modelling/test_optimiser_fabric_first.py +++ b/tests/domain/modelling/test_optimiser_fabric_first.py @@ -275,6 +275,29 @@ def test_ventilation_dependency_is_injected_once_across_both_phases() -> None: assert abs(package.score.sap_continuous - 68.0) <= 1e-9 +def test_no_fabric_candidates_proceeds_straight_to_the_full_pool() -> None: + # Arrange — the envelope work is already done (no fabric Recommendation + # survives generation); fabric first must not veto the run, it just means + # phase 1 has nothing to do. + groups: list[list[ScoredOption]] = [ + [_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. + package: OptimisedPackage = optimise_package_fabric_first( + groups=groups, + scorer=scorer, + baseline_epc=build_epc(), + budget=20000.0, + target_sap=75.0, + ) + + # Assert — the heat pump package, exactly as a plain run would produce. + assert _selected_types(package) == {"air_source_heat_pump"} + assert abs(package.score.sap_continuous - 80.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