diff --git a/tests/applications/modelling_e2e/test_handler.py b/tests/applications/modelling_e2e/test_handler.py index a09fe266a..80c44aeeb 100644 --- a/tests/applications/modelling_e2e/test_handler.py +++ b/tests/applications/modelling_e2e/test_handler.py @@ -124,6 +124,18 @@ def _baseline_orchestrator() -> Iterator[MagicMock]: yield orchestrator +@pytest.fixture(autouse=True) +def _solar_repository() -> Iterator[MagicMock]: + """Patch the stored-Solar repository for every test — the handler reads + stored insights through it whatever refetch_solar says. Default: no stored + rows; solar tests override get_many with real payloads.""" + with patch( + "applications.modelling_e2e.handler.SolarPostgresRepository" + ) as repository: + repository.return_value.get_many.return_value = {} + yield repository + + # --------------------------------------------------------------------------- # Solar throttle: per-container call gap resolved from env (quota/N safety) # --------------------------------------------------------------------------- @@ -208,6 +220,96 @@ def test_trigger_body_new_flags_default_to_true() -> None: assert result.refetch_solar is True +# --------------------------------------------------------------------------- +# refetch_solar semantics — False skips the LIVE fetch only, never the measure +# --------------------------------------------------------------------------- + + +def test_refetch_solar_false_models_with_stored_solar_and_never_calls_google( + _solar_repository: MagicMock, +) -> None: + """refetch_solar=False means "don't pay for a Google call", NOT "model + without solar": the stored Solar insights are read from the DB and handed to + the Modelling stage, and the live Google client is never invoked. + + Provenance: portfolio 814's 2026-07-02 re-model ran with refetch_solar=False + and every plan lost its solar candidates (pids 742071/742077/742397/742399 — + houses stopped short of goal B with bill-negative ASHP-only plans) even + though the `solar` table held healthy payloads for their UPRNs.""" + # Arrange + stored_insights = {"solarPotential": {"maxArrayPanelsCount": 58}} + mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE]) + mock_uow = MagicMock() + + 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) + ) + mock_live_fetch = 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") + ).return_value.overrides_for_many.return_value = {} + 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_snapshot_with_off_catalogue_overrides" + ) + ) + stack.enter_context(patch("applications.modelling_e2e.handler.Session")) + mock_run_modelling = stack.enter_context( + patch( + "applications.modelling_e2e.handler.run_modelling", + return_value=_plan_mock(), + ) + ) + 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 + _solar_repository.return_value.get_many.return_value = { + UPRN: stored_insights + } + + # Act — _BODY carries refetch_solar=False + _call_handler(_BODY) + + # Assert — the stored insights reach the Modelling stage unchanged, and the + # paid Google leg is never exercised. + assert ( + mock_run_modelling.call_args.kwargs["solar_insights"] == stored_insights + ) + mock_live_fetch.assert_not_called() + + # --------------------------------------------------------------------------- # Child SubTask creation # ---------------------------------------------------------------------------