From 641c1bd7f6f96041391cfad5fe28b8810faf39f7 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 16:20:45 +0000 Subject: [PATCH] test(modelling): pin least-cost-to-target end-to-end through the orchestrator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The orchestrator already threads budget/target_sap/dependencies into optimise_package, so no orchestrator change was needed. Add an integration test proving the new objective end-to-end on the real calculator: a band-D property (~57.4) with a goal of band D — already met — yields a Plan with NO measures and zero cost (the old max-gain objective would have recommended wall+floor+vent, improving within the band it is already in). Clarified that the existing multi-measure test now exercises the max-gain fallback (goal C unreachable from D, tops out ~61). Narrowed Optional sap_points/estimated_cost through locals to keep pyright strict-clean. Co-Authored-By: Claude Opus 4.8 --- ...test_ara_first_run_pipeline_integration.py | 106 +++++++++++++++++- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/tests/orchestration/test_ara_first_run_pipeline_integration.py b/tests/orchestration/test_ara_first_run_pipeline_integration.py index f4a0cf60..cca8473a 100644 --- a/tests/orchestration/test_ara_first_run_pipeline_integration.py +++ b/tests/orchestration/test_ara_first_run_pipeline_integration.py @@ -198,7 +198,10 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan( # fields, so the Baseline stage (not under test here) can't run on it. # SAP-numeric correctness is pinned in test_elmhurst_cascade_pins; here we # prove the multi-measure Plan is optimised, priced, attributed and - # persisted. + # persisted. The property is band D (~57.4) and tops out at ~61, so the + # goal-C target is unreachable — this exercises the least-cost-to-target + # objective's **max-gain fallback** (ADR-0016 amendment): best effort, all + # measures, below target. with Session(db_engine) as session: session.add( PropertyRow( @@ -293,8 +296,103 @@ def test_modelling_optimises_and_persists_a_multi_measure_plan( assert rec.estimated_cost is not None # The forced ventilation costs two £450 units and is priced even though it # was never a free choice in the pool. - assert abs(by_type["mechanical_ventilation"].estimated_cost - 900.0) <= 1e-6 + vent_cost: float | None = by_type["mechanical_ventilation"].estimated_cost + assert vent_cost is not None + assert abs(vent_cost - 900.0) <= 1e-6 # The insulation measures earn positive SAP; ventilation's contribution is # not positive (it only ever costs SAP — ADR-0016). - assert by_type["cavity_wall_insulation"].sap_points > 0.0 - assert by_type["mechanical_ventilation"].sap_points <= 0.0 + wall_sap: float | None = by_type["cavity_wall_insulation"].sap_points + vent_sap: float | None = by_type["mechanical_ventilation"].sap_points + assert wall_sap is not None and vent_sap is not None + assert wall_sap > 0.0 + assert vent_sap <= 0.0 + + +def test_modelling_recommends_nothing_when_already_at_the_target_band( + db_engine: Engine, +) -> None: + # Arrange — the same band-D property (~57.4), but a goal of band D, which it + # already meets. Least-cost-to-target recommends the cheapest package that + # *reaches* the target — and the target is already reached, so the cheapest + # package is the empty one. (The old max-gain objective would have + # recommended wall + floor + ventilation here, improving within the band the + # property is already in — exactly the over-recommendation this objective + # removes.) ADR-0016 amendment. + with Session(db_engine) as session: + session.add( + PropertyRow( + id=31, + portfolio_id=1, + postcode="A0 0AA", + address="4 Some Street", + uprn=44444, + ) + ) + session.add( + ScenarioRow( + id=8, goal="Increasing EPC", goal_value="D", is_default=True + ) + ) + # The fabric Generators + the ventilation dependency builder still run + # during candidate generation, so their Products must exist even though + # nothing is ultimately selected. + session.add_all( + [ + MaterialRow( + id=10, + type="cavity_wall_insulation", + total_cost=18.5, + cost_unit="gbp_per_m2", + is_active=True, + description="Cavity wall insulation", + ), + MaterialRow( + id=11, + type="suspended_floor_insulation", + total_cost=25.0, + cost_unit="gbp_per_m2", + is_active=True, + description="Suspended floor insulation", + ), + MaterialRow( + id=12, + type="mechanical_ventilation", + total_cost=450.0, + cost_unit="gbp_per_unit", + is_active=True, + description="Mechanical extract ventilation unit", + ), + ] + ) + session.commit() + EpcPostgresRepository(session).save( + _build_uninsulated_cavity_and_floor_epc(), + property_id=31, + portfolio_id=1, + ) + session.commit() + + def unit_of_work() -> PostgresUnitOfWork: + return PostgresUnitOfWork(lambda: Session(db_engine)) + + # Act + ModellingOrchestrator( + unit_of_work=unit_of_work, calculator=Sap10Calculator() + ).run(property_ids=[31], scenario_ids=[8], portfolio_id=1) + + # Assert — a Plan is persisted with no measures and zero cost; the + # post-retrofit figure is the unchanged baseline (still band D). + with Session(db_engine) as session: + plan = session.exec( + select(PlanRow).where(col(PlanRow.property_id) == 31) + ).first() + assert plan is not None + rec_rows = session.exec( + select(RecommendationRow).where( + col(RecommendationRow.plan_id) == plan.id + ) + ).all() + + assert rec_rows == [] + assert plan.cost_of_works == 0.0 + assert plan.post_epc_rating is Epc.D