Model/tests/applications/ara_first_run/test_handler.py
Khalim Conn-Kowlessar 75fbba60fc feat(ara): AraFirstRunTriggerBody + ara_first_run lambda skeleton (#1130)
Stage-2 entry point for the First Run use case. Adds the
`ara_first_run` Lambda package mirroring the `postcode_splitter`
template, its typed trigger contract, and a stub `FirstRunPipeline`.

- `AraFirstRunTriggerBody`: thin command of five fields — `task_id`,
  `sub_task_id` (UUID, lifecycle), `portfolio_id`, `property_ids`,
  `scenario_ids` (int business IDs). No `model_config` override, so
  Pydantic's default `extra="ignore"` lets the FastAPI backend add
  fields without breaking deployed lambdas. UPRNs / Scenario defs are
  deliberately off the event — read from source-of-truth tables.
- Thin `handler.py`: validate-and-delegate only, via a named
  `dispatch_first_run` seam (testable without the Lambda runtime).
  Subtask status (in-progress/complete/failed) + CloudWatch log URL
  come for free from the existing `@subtask_handler()` decorator.
- `FirstRunPipeline` (orchestration/) stub: `run(command)` receives the
  validated command. Declares a structural `FirstRunCommand` Protocol
  (the three business fields) that `AraFirstRunTriggerBody` satisfies,
  so orchestration needs no application-layer import — rhymes with the
  `EpcFetcher`/`SolarFetcher` Protocols on IngestionOrchestrator
  (ADR-0011). Full Ingestion→Baseline→Modelling composition lands in
  #1136.
- Dockerfile / requirements.txt / local_handler/ mirror postcode_splitter.

TDD: 7 new tests (trigger-body validation incl. forward-compat +
id-types, pipeline seam, handler delegation). pyright strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:38:15 +00:00

44 lines
1.4 KiB
Python

from __future__ import annotations
from typing import Optional
from uuid import UUID
from applications.ara_first_run.ara_first_run_trigger_body import (
AraFirstRunTriggerBody,
)
from applications.ara_first_run.handler import dispatch_first_run
from orchestration.first_run_pipeline import FirstRunCommand
class _SpyPipeline:
"""Records the command it is asked to run, instead of composing stages."""
def __init__(self) -> None:
self.received: Optional[FirstRunCommand] = None
def run(self, command: FirstRunCommand) -> None:
self.received = command
def test_validates_the_event_body_and_delegates_the_command_to_the_pipeline() -> None:
# Arrange — a raw SQS body, as the decorator hands it to the handler.
body = {
"task_id": "e295d89b-a7c5-4a9a-8b4e-b405fab1f298",
"sub_task_id": "f4a9944f-41f0-4a33-8669-5016ec574068",
"portfolio_id": 42,
"property_ids": [101, 102],
"scenario_ids": [7],
}
pipeline = _SpyPipeline()
# Act
dispatch_first_run(body, pipeline=pipeline)
# Assert — the raw body was validated into the typed trigger and handed
# straight on, untouched.
received = pipeline.received
assert isinstance(received, AraFirstRunTriggerBody)
assert received.task_id == UUID("e295d89b-a7c5-4a9a-8b4e-b405fab1f298")
assert received.portfolio_id == 42
assert received.property_ids == [101, 102]
assert received.scenario_ids == [7]