diff --git a/tests/harness/test_console.py b/tests/harness/test_console.py index a252b06a..8b95e455 100644 --- a/tests/harness/test_console.py +++ b/tests/harness/test_console.py @@ -10,6 +10,8 @@ from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.contingencies import contingency_rate +from domain.modelling.generators.heating_recommendation import recommend_heating +from domain.modelling.generators.solid_wall_recommendation import recommend_solid_wall from harness.console import DEFAULT_CATALOGUE, run_modelling, run_one from repositories.product.product_json_repository import ProductJsonRepository from tests.domain.modelling._elmhurst_recommendation import ( @@ -86,13 +88,21 @@ def test_run_modelling_recommends_solid_wall_insulation_for_solid_brick() -> Non "solid_brick_ewi_001431_before.pdf" ) - # Act — Modelling only, no database. - plan = run_modelling(epc, goal_band="C", print_table=False) + # Assert — the solid-wall generator is wired in and OFFERS a solid-wall + # Option for this dwelling. Whether the Optimiser then selects it depends on + # the package: the efficient ASHP bundle (ADR-0025) now often reaches the + # band first, so we assert the Option is OFFERED (the wiring/eligibility + # intent) rather than selected. + recommendation = recommend_solid_wall(epc, ProductJsonRepository(DEFAULT_CATALOGUE)) + assert recommendation is not None + assert {o.measure_type for o in recommendation.options} & { + "external_wall_insulation", + "internal_wall_insulation", + } - # Assert — the solid-wall generator is wired into the candidate pool, so a - # solid-wall Option reaches the optimised package. - measure_types = {measure.measure_type for measure in plan.measures} - assert measure_types & {"external_wall_insulation", "internal_wall_insulation"} + # And Modelling still produces a plan end to end (now ASHP-led). + plan = run_modelling(epc, goal_band="C", print_table=False) + assert len(plan.measures) >= 1 def test_run_modelling_listed_building_yields_no_wall_insulation() -> None: @@ -195,12 +205,19 @@ def test_run_modelling_recommends_hhr_storage_for_an_electric_dwelling() -> None # heating generator is wired into the candidate pool (ADR-0024). epc: EpcPropertyData = _electric_storage_lit_epc() - # Act — Modelling only, no database. - plan = run_modelling(epc, goal_band="C", print_table=False) + # Assert — the heating generator is wired in and OFFERS the HHR storage + # bundle for an electric dwelling. The Optimiser now selects the ASHP bundle + # instead — the efficient Vaillant (ADR-0025) beats resistance storage on SAP + # — so HHR is offered-but-not-selected; assert it is OFFERED to preserve the + # wiring check, and that the optimised package leads with ASHP. + recommendation = recommend_heating(epc, ProductJsonRepository(DEFAULT_CATALOGUE)) + assert recommendation is not None + assert "high_heat_retention_storage_heaters" in { + o.measure_type for o in recommendation.options + } - # Assert — the HHR storage bundle reaches the optimised package. - measure_types = {measure.measure_type for measure in plan.measures} - assert "high_heat_retention_storage_heaters" in measure_types + plan = run_modelling(epc, goal_band="C", print_table=False) + assert "air_source_heat_pump" in {m.measure_type for m in plan.measures} def test_sample_catalogue_prices_every_generator_measure_type() -> None: diff --git a/tests/harness/test_report.py b/tests/harness/test_report.py index 5b69bd62..4357b4f7 100644 --- a/tests/harness/test_report.py +++ b/tests/harness/test_report.py @@ -77,33 +77,26 @@ def test_each_fired_measure_carries_the_attributes_that_triggered_it() -> None: # Assert — the Plan ran and every fired measure names its trigger fields. assert report.plan is not None assert report.plan_error is None + # The efficient representative heat pump (Vaillant aroTHERM plus 5 kW, + # ADR-0025) raises SAP enough on this gas dwelling that ASHP + solid-floor + # insulation reach the target band on their own, displacing the fabric stack + # the Optimiser used to assemble (ADR-0024 — ASHP is now a strong candidate). triggers: dict[str, MeasureTrigger] = _triggers_by_measure(report) assert set(triggers) == { - "cavity_wall_insulation", - "mechanical_ventilation", + "air_source_heat_pump", "solid_floor_insulation", - "low_energy_lighting", } - # Cavity fill fired because the main wall is an uninsulated cavity. - assert triggers["cavity_wall_insulation"].triggers == { - "wall_construction": 4, - "wall_insulation_type": 4, - } - # Ventilation fired because the dwelling lodges no mechanical kind. - assert triggers["mechanical_ventilation"].triggers == { - "mechanical_ventilation_kind": None, + # ASHP fired because the dwelling is a non-flat house not already a heat pump + # (eligibility is physical/planning only — ADR-0024). + assert triggers["air_source_heat_pump"].triggers == { + "property_type": "0", + "main_heating_category": 2, } # Solid-floor insulation fired off an uninsulated solid ground floor. assert triggers["solid_floor_insulation"].triggers == { "floor_insulation_thickness": None, "floor_construction_type": "Solid", } - # The LED upgrade fired because the dwelling lodges 10 low-energy-unknown bulbs. - assert triggers["low_energy_lighting"].triggers == { - "incandescent_fixed_lighting_bulbs_count": 0, - "cfl_fixed_lighting_bulbs_count": None, - "low_energy_fixed_lighting_bulbs_count": 10, - } def test_few_measure_cert_surfaces_only_its_fired_measures_triggers() -> None: diff --git a/tests/orchestration/test_ara_first_run_pipeline_integration.py b/tests/orchestration/test_ara_first_run_pipeline_integration.py index 49c77033..8ef68627 100644 --- a/tests/orchestration/test_ara_first_run_pipeline_integration.py +++ b/tests/orchestration/test_ara_first_run_pipeline_integration.py @@ -351,6 +351,10 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan( # The sample EPC lodges 8 low-energy-unknown bulbs, so the LED upgrade is # a cheap positive-SAP candidate the Optimiser also keeps (ADR-0023). "low_energy_lighting", + # The efficient representative heat pump (Vaillant aroTHERM plus 5 kW, + # ADR-0025) now raises SAP even on this gas dwelling, so the Optimiser + # also keeps the ASHP bundle in the least-cost-to-band package (ADR-0024). + "air_source_heat_pump", } # Each persisted measure carries the catalogue id of the Product it installs # (the MaterialRow ids seeded above), replacing the retired @@ -641,16 +645,24 @@ def test_listed_uprn_ingested_blocks_solid_wall_insulation_in_modelling( fuel_rates=FuelRatesStaticFileRepository(), ).run(property_ids=[40, 41], scenario_ids=[7], portfolio_id=1) - # Assert — the listed dwelling gets no wall insulation (both Options blocked); - # the unrestricted dwelling gets one. The only difference between them is the - # planning status Ingestion cached, proving the loop end to end (ADR-0019/0020). - _WALL_TYPES = {"external_wall_insulation", "internal_wall_insulation"} + # Assert — a listed building blocks the fabric-protected measures: both + # solid-wall Options AND the ASHP bundle (all gated on `blocks_internal`, + # ADR-0024). So the listed dwelling gets neither, while the unrestricted one + # gets the ASHP bundle (which the efficient Vaillant now makes the Optimiser + # select — ADR-0025, so walls are no longer needed to reach the band). The + # only difference between them is the planning status Ingestion cached, + # proving the gate end to end (ADR-0019/0020/0024). + _PROTECTED_TYPES = { + "external_wall_insulation", + "internal_wall_insulation", + "air_source_heat_pump", + } with Session(db_engine) as session: listed_types = _plan_measure_types(session, property_id=40) unrestricted_types = _plan_measure_types(session, property_id=41) - assert _WALL_TYPES.isdisjoint(listed_types) - assert _WALL_TYPES & unrestricted_types + assert _PROTECTED_TYPES.isdisjoint(listed_types) + assert "air_source_heat_pump" in unrestricted_types def _plan_measure_types(session: Session, *, property_id: int) -> set[str]: