From 39f028f03a6b962682d2c298dd59808055a3f264 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 24 Jun 2026 11:04:29 +0000 Subject: [PATCH] =?UTF-8?q?per-property=20failure=20fails=20child=20SubTas?= =?UTF-8?q?k=20without=20raising=20from=20handler=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../modelling_e2e/test_handler.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/applications/modelling_e2e/test_handler.py b/tests/applications/modelling_e2e/test_handler.py index 2ac4520d5..a07f7346c 100644 --- a/tests/applications/modelling_e2e/test_handler.py +++ b/tests/applications/modelling_e2e/test_handler.py @@ -814,6 +814,77 @@ def test_partial_batch_failure_raises_runtime_error_listing_failed_ids() -> None mock_uow.commit.assert_called_once() +# --------------------------------------------------------------------------- +# Partial batch failure — per-property isolation +# --------------------------------------------------------------------------- + + +def test_per_property_failure_fails_child_subtask_and_siblings_continue() -> None: + """Two properties: property 1 succeeds, property 2 fails during modelling. + The handler does not raise; property 1's UoW was committed; run_subtask was + called for both properties.""" + # Arrange + pid1, pid2 = 111, 222 + mock_engine = _engine_mock([pid1, pid2], [1001, 1002], [POSTCODE, POSTCODE]) + mock_plan = _plan_mock() + mock_uow = MagicMock() + call_count = {"n": 0} + + def _run_modelling_side_effect(*args: Any, **kwargs: Any) -> Any: + call_count["n"] += 1 + if call_count["n"] == 2: + raise ValueError("modelling exploded") + return mock_plan + + mock_orch = _mock_orchestrator() + + with ExitStack() as stack: + stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV)) + stack.enter_context( + patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcClientService") + ).return_value.get_by_uprn.return_value = MagicMock() + stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository")) + stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient")) + stack.enter_context( + patch("applications.modelling_e2e.handler._spatial_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.overlays_from", return_value=[]) + ) + stack.enter_context(patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader")) + stack.enter_context( + patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") + ).return_value.get_many.return_value = [MagicMock()] + stack.enter_context(patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides")) + stack.enter_context(patch("applications.modelling_e2e.handler.Session")) + stack.enter_context( + patch( + "applications.modelling_e2e.handler.run_modelling", + side_effect=_run_modelling_side_effect, + ) + ) + MockUoW = stack.enter_context(patch("applications.modelling_e2e.handler.PostgresUnitOfWork")) + MockUoW.return_value.__enter__.return_value = mock_uow + MockUoW.return_value.__exit__.return_value = False + + # Act — must not raise even though pid2 fails + _call_handler( + {"property_ids": [pid1, pid2], "portfolio_id": PORTFOLIO_ID, + "scenario_id": SCENARIO_ID, "no_solar": True, "dry_run": False}, + orchestrator=mock_orch, + ) + + # run_subtask called for both properties; pid1 committed + assert mock_orch.run_subtask.call_count == 2 + mock_uow.commit.assert_called_once() + + # --------------------------------------------------------------------------- # Cohort cache hit # ---------------------------------------------------------------------------