diff --git a/scripts/audit/anomalies.py b/scripts/audit/anomalies.py index 3f05993e1..022f6fe27 100644 --- a/scripts/audit/anomalies.py +++ b/scripts/audit/anomalies.py @@ -187,6 +187,11 @@ def _already_meets_goal_with_works(a: PropertyAudit) -> Optional[str]: return f"already {a.effective_band} >= goal {a.scenario_goal_band} but cost_of_works £{a.cost_of_works:.0f}" +@check("costed-plan-without-real-gain", Severity.HIGH) +def _costed_plan_without_real_gain(a: PropertyAudit) -> Optional[str]: + raise NotImplementedError + + @check("plan-stops-short-of-goal", Severity.HIGH) def _plan_stops_short_of_goal(a: PropertyAudit) -> Optional[str]: """The default plan ends BELOW the scenario's goal band on a scenario that is diff --git a/tests/scripts/test_audit_anomalies.py b/tests/scripts/test_audit_anomalies.py index 86d7889b1..e34bf250d 100644 --- a/tests/scripts/test_audit_anomalies.py +++ b/tests/scripts/test_audit_anomalies.py @@ -10,6 +10,7 @@ from scripts.audit.anomalies import ( PropertyAudit, Severity, _already_meets_goal_with_works, # pyright: ignore[reportPrivateUsage] + _costed_plan_without_real_gain, # pyright: ignore[reportPrivateUsage] _effective_lodged_divergence, # pyright: ignore[reportPrivateUsage] _implausible_lodged_score, # pyright: ignore[reportPrivateUsage] _plan_stops_short_of_goal, # pyright: ignore[reportPrivateUsage] @@ -213,3 +214,70 @@ class TestAlreadyMeetsGoalWithWorks: # Assert assert registered["already-meets-goal-with-works"] is Severity.HIGH + + +class TestCostedPlanWithoutRealGain: + """Money spent for no meaningful SAP movement (#1652 / #1655). + + The coverage hole the #1652 diagnosis found: `plan-score-below-baseline` + only fires on a *drop* beyond 0.5 and `zero-works-post-differs` only on £0 + plans, so a costed plan landing flat against the effective baseline was + caught by neither — including the 112 homes whose modelled post-retrofit SAP + sat ~0.2 *below* their effective baseline. + """ + + def test_fires_when_works_are_bought_for_no_real_gain(self) -> None: + # Arrange — £6,240 of works moving the dwelling 0.2 SAP. + audit = _make_audit( + effective_sap=63.0, post_sap=63.2, cost_of_works=6240.0 + ) + + # Act + result = _costed_plan_without_real_gain(audit) + + # Assert + assert result is not None + assert "6240" in result or "6,240" in result + + def test_fires_when_the_post_score_lands_below_the_baseline(self) -> None: + # Arrange — the likely-downgrade case: paid works, post *below* baseline. + audit = _make_audit( + effective_sap=63.0, post_sap=62.8, cost_of_works=1029.0 + ) + + # Act + result = _costed_plan_without_real_gain(audit) + + # Assert + assert result is not None + + def test_silent_when_the_works_deliver_a_real_uplift(self) -> None: + # Arrange — £6,240 buying 9 SAP points: what a retrofit should look like. + audit = _make_audit( + effective_sap=63.0, post_sap=72.0, cost_of_works=6240.0 + ) + + # Act + result = _costed_plan_without_real_gain(audit) + + # Assert + assert result is None + + def test_silent_on_a_zero_cost_plan(self) -> None: + # Arrange — no money spent, so there is nothing to justify; a £0 plan + # that moves nothing is the correct outcome for an already-compliant + # dwelling, and is covered by its own check. + audit = _make_audit(effective_sap=63.0, post_sap=63.0, cost_of_works=0.0) + + # Act + result = _costed_plan_without_real_gain(audit) + + # Assert + assert result is None + + def test_is_registered_at_high_severity(self) -> None: + # Act + registered = {name: severity for name, severity, _ in _REGISTRY} + + # Assert + assert registered["costed-plan-without-real-gain"] is Severity.HIGH