mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
The measures a run considers should come from the Scenario, not a CLI flag.
The live scenario table persists exclusions only (no inclusions column), as a
Postgres text-array of exact MeasureType values.
- Scenario gains `exclusions: frozenset[MeasureType]` + `considered_measures()`
(all measures minus the excluded ones, or None when none are excluded).
- ScenarioModel.to_domain parses the `{a,b,c}` exclusions array into
MeasureTypes, raising on a token that is not an exact MeasureType value
(no high-level category expansion), per the strict-enum convention.
- ModellingOrchestrator._plan_for derives the allowlist from the Scenario's
exclusions, combined (intersection) with any explicit considered_measures
override via the new `combine_considered_measures`.
- run_modelling_e2e sources the allowlist from the Scenario; --measures /
--exclude-measures become optional overlays (e.g. the technical
secondary_heating_removal exclusion the catalogue cannot yet stock).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
"""Behaviour of the Postgres-backed ScenarioRepository: reading the Scenarios
|
|
the Modelling stage scores a Property against, off the live ``scenario`` table.
|
|
|
|
The FE creates a Scenario in the scenario-builder and passes its id to the
|
|
pipeline (#1130); the orchestrator reads it back here at modelling time. Only
|
|
the fields modelling uses are mapped — goal / goal_value / budget / is_default;
|
|
the legacy file-path columns are ignored. See CONTEXT.md (Scenario) and
|
|
ADR-0017.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import Engine
|
|
from sqlmodel import Session
|
|
|
|
from domain.modelling.measure_type import MeasureType
|
|
from domain.modelling.portfolio_goal import PortfolioGoal
|
|
from domain.modelling.scenario import Scenario
|
|
from infrastructure.postgres.modelling import ScenarioModel
|
|
from repositories.scenario.scenario_postgres_repository import (
|
|
ScenarioPostgresRepository,
|
|
)
|
|
|
|
|
|
def test_get_many_maps_live_scenario_rows_to_domain_in_input_order(
|
|
db_engine: Engine,
|
|
) -> None:
|
|
# Arrange
|
|
with Session(db_engine) as session:
|
|
session.add(
|
|
ScenarioModel(
|
|
id=7,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="C",
|
|
budget=15000.0,
|
|
is_default=True,
|
|
)
|
|
)
|
|
session.add(
|
|
ScenarioModel(
|
|
id=9,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="B",
|
|
budget=None,
|
|
is_default=False,
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
# Act
|
|
with Session(db_engine) as session:
|
|
scenarios: list[Scenario] = ScenarioPostgresRepository(session).get_many(
|
|
[9, 7]
|
|
)
|
|
|
|
# Assert — to_domain maps the PortfolioGoal enum to its value string
|
|
assert [s.id for s in scenarios] == [9, 7] # input order preserved
|
|
assert scenarios[0] == Scenario(
|
|
id=9, goal="Increasing EPC", goal_value="B", budget=None, is_default=False
|
|
)
|
|
assert scenarios[1] == Scenario(
|
|
id=7,
|
|
goal="Increasing EPC",
|
|
goal_value="C",
|
|
budget=15000.0,
|
|
is_default=True,
|
|
)
|
|
|
|
|
|
def test_get_many_parses_the_exclusions_array_into_measure_types(
|
|
db_engine: Engine,
|
|
) -> None:
|
|
# Arrange — the live `exclusions` column is a Postgres text-array literal of
|
|
# exact MeasureType values.
|
|
with Session(db_engine) as session:
|
|
session.add(
|
|
ScenarioModel(
|
|
id=7,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="C",
|
|
is_default=True,
|
|
exclusions="{solar_pv,internal_wall_insulation}",
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
# Act
|
|
with Session(db_engine) as session:
|
|
scenario: Scenario = ScenarioPostgresRepository(session).get_many([7])[0]
|
|
|
|
# Assert
|
|
assert scenario.exclusions == frozenset(
|
|
{MeasureType.SOLAR_PV, MeasureType.INTERNAL_WALL_INSULATION}
|
|
)
|
|
|
|
|
|
def test_get_many_treats_a_null_exclusions_column_as_no_exclusions(
|
|
db_engine: Engine,
|
|
) -> None:
|
|
# Arrange
|
|
with Session(db_engine) as session:
|
|
session.add(
|
|
ScenarioModel(
|
|
id=7,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="C",
|
|
is_default=True,
|
|
exclusions=None,
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
# Act
|
|
with Session(db_engine) as session:
|
|
scenario: Scenario = ScenarioPostgresRepository(session).get_many([7])[0]
|
|
|
|
# Assert
|
|
assert scenario.exclusions == frozenset()
|
|
|
|
|
|
def test_get_many_raises_on_an_exclusion_that_is_not_a_measure_type(
|
|
db_engine: Engine,
|
|
) -> None:
|
|
# Arrange — a legacy high-level category (`heating`) is not an exact
|
|
# MeasureType value; exact-only resolution must reject it loudly.
|
|
with Session(db_engine) as session:
|
|
session.add(
|
|
ScenarioModel(
|
|
id=7,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="C",
|
|
is_default=True,
|
|
exclusions="{heating}",
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
# Act / Assert
|
|
with Session(db_engine) as session:
|
|
with pytest.raises(ValueError):
|
|
ScenarioPostgresRepository(session).get_many([7])
|
|
|
|
|
|
def test_get_many_raises_when_a_scenario_id_is_missing(db_engine: Engine) -> None:
|
|
# Arrange
|
|
with Session(db_engine) as session:
|
|
session.add(
|
|
ScenarioModel(
|
|
id=7,
|
|
goal=PortfolioGoal.INCREASING_EPC,
|
|
goal_value="C",
|
|
is_default=True,
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
# Act / Assert
|
|
with Session(db_engine) as session:
|
|
with pytest.raises(ValueError):
|
|
ScenarioPostgresRepository(session).get_many([7, 404])
|