mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 1 of the DB-less inspection harness. Complete the in-memory FakeUnitOfWork so the ModellingOrchestrator runs with no Postgres: add FakeScenarioRepository + FakePlanRepository (idempotent, keyed by (property_id, scenario_id)), expose scenario/product/plan on the fake unit, and grow FakePropertyRepo to compose the effective EPC from the EPC repo at read time — mirroring PropertyPostgresRepository, so the EPC Ingestion persists is visible to Baseline + Modelling (the through-repos hand-off, in memory). The new integration test drives the full AraFirstRunPipeline (Ingestion -> Baseline -> Modelling) against the FakeUnitOfWork — no Session ever opened — on the uninsulated 000490 fixture with its lodged recorded-performance filled in (it already carries the RHI block, so Baseline can run) and asserts a multi-measure Plan is produced. The committed product catalogue prices the wall/floor/ventilation measures it fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
5.4 KiB
Python
151 lines
5.4 KiB
Python
"""First Run end-to-end with NO database — in-memory fakes only.
|
|
|
|
The same `AraFirstRunPipeline` the Postgres integration test drives, but wired
|
|
against a `FakeUnitOfWork` instead of a `PostgresUnitOfWork`: Ingestion ->
|
|
Baseline -> Modelling run start-to-finish, hand off through in-memory repos, and
|
|
produce an inspectable multi-measure Plan without a `Session` ever being opened.
|
|
This is the harness the owner runs to sense-check recommendations interactively.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from datatypes.epc.domain.epc import Epc
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
|
from domain.modelling.scenario import Scenario
|
|
from domain.property.property import Property, PropertyIdentity
|
|
from domain.property_baseline.rebaseliner import StubRebaseliner
|
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
|
from orchestration.ara_first_run_pipeline import AraFirstRunPipeline
|
|
from orchestration.ingestion_orchestrator import IngestionOrchestrator
|
|
from orchestration.modelling_orchestrator import ModellingOrchestrator
|
|
from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator
|
|
from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
|
FuelRatesStaticFileRepository,
|
|
)
|
|
from repositories.geospatial.geospatial_repository import GeospatialRepository
|
|
from repositories.product.product_json_repository import ProductJsonRepository
|
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
|
build_epc as _build_uninsulated_cavity_and_floor_epc,
|
|
)
|
|
from tests.orchestration.fakes import (
|
|
FakeEpcRepo,
|
|
FakePlanRepository,
|
|
FakePropertyRepo,
|
|
FakeScenarioRepository,
|
|
FakeUnitOfWork,
|
|
)
|
|
|
|
_CATALOGUE = Path(__file__).resolve().parent / "fixtures/product_catalogue.json"
|
|
|
|
|
|
@dataclass
|
|
class _Command:
|
|
portfolio_id: int
|
|
property_ids: list[int]
|
|
scenario_ids: list[int]
|
|
|
|
|
|
class _FetcherReturning:
|
|
def __init__(self, epc: EpcPropertyData) -> None:
|
|
self._epc = epc
|
|
|
|
def get_by_uprn(self, uprn: int) -> Optional[EpcPropertyData]:
|
|
return self._epc
|
|
|
|
|
|
class _NoCoordinates(GeospatialRepository):
|
|
def coordinates_for(self, uprn: int): # type: ignore[no-untyped-def]
|
|
return None # skip the solar leg
|
|
|
|
|
|
class _UnusedSolarFetcher:
|
|
def get_building_insights(
|
|
self, longitude: float, latitude: float
|
|
) -> dict[str, Any]: # pragma: no cover
|
|
return {}
|
|
|
|
|
|
def _uninsulated_lodged_epc() -> EpcPropertyData:
|
|
# 000490: an uninsulated cavity wall + suspended floor (loft already 300mm),
|
|
# so the wall + floor Generators fire and the ventilation Dependency follows.
|
|
# The calculator fixture carries no lodged recorded-performance, so we fill it
|
|
# in (as a real lodged EPC would) — it already carries the RHI block — so the
|
|
# Baseline stage can run inside the full pipeline.
|
|
epc = _build_uninsulated_cavity_and_floor_epc()
|
|
return dataclasses.replace(
|
|
epc,
|
|
energy_rating_current=57,
|
|
current_energy_efficiency_band=Epc.D,
|
|
co2_emissions_current=3.0,
|
|
energy_consumption_current=300,
|
|
)
|
|
|
|
|
|
def test_first_run_produces_a_multi_measure_plan_without_a_database() -> None:
|
|
# Arrange — an in-memory Property (no EPC yet; Ingestion supplies it), a
|
|
# default Increasing-EPC Scenario, and a file-backed product catalogue.
|
|
epc_repo = FakeEpcRepo()
|
|
plan_repo = FakePlanRepository()
|
|
property_repo = FakePropertyRepo(
|
|
{
|
|
10: Property(
|
|
identity=PropertyIdentity(
|
|
portfolio_id=1,
|
|
postcode="A0 0AA",
|
|
address="1 Some Street",
|
|
uprn=12345,
|
|
)
|
|
)
|
|
},
|
|
epc_repo=epc_repo,
|
|
)
|
|
unit: FakeUnitOfWork = FakeUnitOfWork(
|
|
property=property_repo,
|
|
epc=epc_repo,
|
|
scenario=FakeScenarioRepository(
|
|
{
|
|
7: Scenario(
|
|
id=7,
|
|
goal="Increasing EPC",
|
|
goal_value="C",
|
|
budget=None,
|
|
is_default=True,
|
|
)
|
|
}
|
|
),
|
|
product=ProductJsonRepository(_CATALOGUE),
|
|
plan=plan_repo,
|
|
)
|
|
|
|
pipeline = AraFirstRunPipeline(
|
|
ingestion=IngestionOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
epc_fetcher=_FetcherReturning(_uninsulated_lodged_epc()),
|
|
geospatial_repo=_NoCoordinates(),
|
|
solar_fetcher=_UnusedSolarFetcher(),
|
|
),
|
|
baseline=PropertyBaselineOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
rebaseliner=StubRebaseliner(),
|
|
fuel_rates=FuelRatesStaticFileRepository(),
|
|
),
|
|
modelling=ModellingOrchestrator(
|
|
unit_of_work=lambda: unit,
|
|
calculator=Sap10Calculator(),
|
|
fuel_rates=FuelRatesStaticFileRepository(),
|
|
),
|
|
)
|
|
|
|
# Act — the whole First Run, no Session ever opened.
|
|
pipeline.run(_Command(portfolio_id=1, property_ids=[10], scenario_ids=[7]))
|
|
|
|
# Assert — a Plan was persisted in memory for (property 10, scenario 7),
|
|
# with at least one Plan Measure and a post-retrofit SAP no worse than baseline.
|
|
plan = plan_repo.saved[(10, 7)]
|
|
assert len(plan.measures) >= 1
|
|
assert plan.post_sap_continuous >= plan.baseline.sap_continuous
|