Model/tests/orchestration/test_ara_first_run_pipeline.py
Khalim Conn-Kowlessar c7e2aa3755 feat(modelling): ModellingOrchestrator persists a Plan end-to-end (#1157)
Slice 4b — closes the #1157 tracer. ModellingOrchestrator.run(property_ids,
scenario_ids, portfolio_id) now does real work in one Unit of Work,
committed once (ADR-0011/0012/0016/0017):

  read Property (effective EPC) + Scenario via repos → recommend_cavity_wall
  → select its Option → PackageScorer.score (role-2 package total) +
  marginal_impacts (role-3 attribution) → build Plan/PlanMeasure →
  uow.plan.save → commit.

- AraFirstRunPipeline / ModellingStage thread portfolio_id from the trigger
  body (one source of truth); handler builds the real orchestrator
  (unit_of_work + Sap10Calculator), dropping the Scenario/Materials stubs.
- ScenarioRepository.get_many promoted to @abstractmethod now the bare-stub
  instantiations are gone.
- New ara_first_run-style integration test: a property with an uninsulated
  cavity wall yields a persisted Plan + one cavity_wall_insulation Plan
  Measure (priced from the Product, figures present, linked by plan_id).
  Numeric SAP correctness is pinned separately in test_elmhurst_cascade_pins.
- Existing pipeline integration test updated: seeds scenario 7 and runs the
  real Modelling stage (its already-insulated sample wall yields an empty
  package — no crash).

121 pass across repositories/modelling/orchestration/app; pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:08:32 +00:00

66 lines
1.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from orchestration.ara_first_run_pipeline import AraFirstRunCommand, AraFirstRunPipeline
@dataclass
class _FakeCommand:
"""A stand-in for AraFirstRunTriggerBody — structurally a AraFirstRunCommand."""
portfolio_id: int
property_ids: list[int]
scenario_ids: list[int]
class _SpyIngestion:
def __init__(self, log: list[tuple[object, ...]]) -> None:
self._log = log
def run(self, property_ids: list[int]) -> None:
self._log.append(("ingestion", property_ids))
class _SpyBaseline:
def __init__(self, log: list[tuple[object, ...]]) -> None:
self._log = log
def run(self, property_ids: list[int]) -> None:
self._log.append(("baseline", property_ids))
class _SpyModelling:
def __init__(self, log: list[tuple[object, ...]]) -> None:
self._log = log
def run(
self, property_ids: list[int], scenario_ids: list[int], portfolio_id: int
) -> None:
self._log.append(("modelling", property_ids, scenario_ids, portfolio_id))
def test_run_sequences_the_three_stages_threading_only_property_ids() -> None:
# Arrange
log: list[tuple[object, ...]] = []
command: AraFirstRunCommand = _FakeCommand(
portfolio_id=1, property_ids=[10, 11], scenario_ids=[7]
)
pipeline = AraFirstRunPipeline(
ingestion=_SpyIngestion(log),
baseline=_SpyBaseline(log),
modelling=_SpyModelling(log),
)
# Act
pipeline.run(command)
# Assert — Ingestion -> Baseline -> Modelling, in order. Ingestion and
# Baseline receive only property_ids; Modelling additionally gets the
# scenario_ids (off the command, not a prior stage). Nothing else is
# threaded between stages — they communicate through repos (ADR-0011).
assert log == [
("ingestion", [10, 11]),
("baseline", [10, 11]),
("modelling", [10, 11], [7], 1),
]