M8-R03B: keep meter swap recompute in caller transaction
This commit is contained in:
@@ -36,10 +36,10 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy import create_engine, event, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.energy import Meter
|
||||
from app.models.energy import EnergyCostPeriod, Meter
|
||||
from app.models.meter_source import MeterSource, MeterSourceBinding, MeterSourceChannel
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -443,6 +443,54 @@ def test_declare_meter_recompute_failure_rolls_back_handoff(meters_client):
|
||||
assert session.execute(select(MeterSourceBinding)).scalar_one().ended_at is None
|
||||
|
||||
|
||||
def test_declare_meter_final_commit_failure_rolls_back_handoff_and_recompute(meters_client):
|
||||
"""A real recompute remains uncommitted until the route's final commit succeeds."""
|
||||
client, engine = meters_client
|
||||
_login(client)
|
||||
boundary = datetime.now(UTC) - timedelta(minutes=45)
|
||||
boundary = boundary.replace(minute=boundary.minute - boundary.minute % 15, second=0, microsecond=0)
|
||||
old_start = boundary - timedelta(days=1)
|
||||
|
||||
with Session(engine) as session:
|
||||
old_meter = Meter(
|
||||
label="Old meter",
|
||||
commodity="electricity",
|
||||
started_at=old_start,
|
||||
reason="initial",
|
||||
created_at=old_start,
|
||||
)
|
||||
session.add(old_meter)
|
||||
session.commit()
|
||||
old_id = old_meter.id
|
||||
channel_uuid = _add_bound_channel(engine, meter_id=old_id, started_at=old_start)
|
||||
|
||||
def fail_final_commit(_session: Session) -> None:
|
||||
raise RuntimeError("final commit failed")
|
||||
|
||||
event.listen(Session, "before_commit", fail_final_commit)
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="final commit failed"):
|
||||
client.post(
|
||||
"/api/energy/meters",
|
||||
json=_declare_payload(
|
||||
label="New meter",
|
||||
started_at=boundary.isoformat(),
|
||||
reason="meter_swap",
|
||||
source_channel_uuid=channel_uuid,
|
||||
),
|
||||
headers={"X-CSRF-Token": _CSRF},
|
||||
)
|
||||
finally:
|
||||
event.remove(Session, "before_commit", fail_final_commit)
|
||||
|
||||
with Session(engine) as session:
|
||||
assert session.execute(select(Meter).where(Meter.label == "New meter")).scalar_one_or_none() is None
|
||||
assert session.get(Meter, old_id).ended_at is None
|
||||
binding = session.execute(select(MeterSourceBinding)).scalar_one()
|
||||
assert binding.ended_at is None
|
||||
assert session.execute(select(EnergyCostPeriod)).scalars().all() == []
|
||||
|
||||
|
||||
def test_declare_meter_overlap_returns_422(meters_client):
|
||||
"""Declaring a meter with started_at before active meter's started_at → 422."""
|
||||
client, _ = meters_client
|
||||
@@ -546,6 +594,7 @@ def test_declare_meter_retroactive_triggers_recompute(meters_client):
|
||||
assert resp.status_code == 201
|
||||
# recompute_range should have been called with start == t_past
|
||||
assert mock_recompute.called
|
||||
assert mock_recompute.call_args.kwargs["commit"] is False
|
||||
call_args = mock_recompute.call_args
|
||||
recompute_start = call_args[0][1] # positional arg index 1 (session is 0)
|
||||
# Normalise for comparison
|
||||
@@ -685,6 +734,7 @@ def test_patch_meter_started_at_retroactive_triggers_recompute(meters_client):
|
||||
assert resp.status_code == 200
|
||||
# recompute should be triggered
|
||||
assert mock_recompute.called
|
||||
assert mock_recompute.call_args.kwargs["commit"] is False
|
||||
call_args = mock_recompute.call_args
|
||||
recompute_start = call_args[0][1]
|
||||
if recompute_start.tzinfo is None:
|
||||
|
||||
Reference in New Issue
Block a user