"""Real project SQL, with all schema, observations, events and tasks rolled back.""" import os from contextlib import asynccontextmanager from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import AsyncMock from uuid import UUID, uuid4 import psycopg from psycopg.rows import dict_row import pytest from app.auth.project_dependencies import ProjectContext from app.domain.schemas.scada_replay import ScadaReplayCreate, ScadaReplayCommand from app.infra.db.metadb.database import SessionLocal, engine from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository from app.infra.db.postgresql.scada_replays import ScadaReplayRepository from app.infra.db.postgresql.scada import ScadaInfoRepository from app.services.scada_replay import ScadaReplayService pytestmark = pytest.mark.skipif(os.getenv("SCADA_REPLAY_STORAGE_TEST") != "1", reason="explicit rollback-only SQL test required") @pytest.fixture def anyio_backend(): return "asyncio" @pytest.mark.anyio async def test_real_manual_steps_clean_raw_and_persist_events_roll_back(monkeypatch): actual_project = UUID("871d7538-589c-47de-a6d5-3185ccb9f0e1") async with SessionLocal() as session: metadata = MetadataRepository(session) business = await metadata.get_project_db_routing(actual_project, "biz_data") timeseries = await metadata.get_project_db_routing(actual_project, "iot_data") await engine.dispose() pg = await psycopg.AsyncConnection.connect(business.dsn, row_factory=dict_row, connect_timeout=5) ts = await psycopg.AsyncConnection.connect(timeseries.dsn, row_factory=dict_row, connect_timeout=5) try: async with pg.transaction(force_rollback=True), ts.transaction(force_rollback=True): actual = await (await pg.execute("SELECT current_database() AS db")).fetchone() assert actual["db"] == "tjwater2_v2" migration = (Path(__file__).parents[2] / "resources/sql/031_scada_replays.sql").read_text() migration = migration.replace("BEGIN;\n", "", 1).removesuffix("COMMIT;\n") await pg.execute(migration) device = "SCADA_TEST_" + uuid4().hex[:12] start = datetime(2026, 10, 8, tzinfo=timezone.utc) for minute, value in ((0, 10), (15, 20), (30, 20), (45, 10), (60, 10)): await ts.execute("INSERT INTO scada.measurements(time,device_id,monitored_value) VALUES(%s,%s,%s)", (start + timedelta(minutes=minute), device, value)) monkeypatch.setattr(ScadaInfoRepository, "get_scadas", AsyncMock(return_value=[{ "device_id": device, "device_type": "pressure", "measurement_unit": "m", "transmission_frequency": "0:15:00", "transmission_mode": "non_realtime" }])) context = ProjectContext( project_id=uuid4(), project_code="tjwater2_v2", user_id=uuid4(), project_role="member", ) repo = ScadaReplayRepository(pg, context.project_id) wall = [start] service = ScadaReplayService(repo, context, ts, clock=lambda: wall[0]) config = ScadaReplayCreate(request_id=uuid4(), start_time=start, end_time=start + timedelta(minutes=75), device_ids=[device], rules=[{"rule_id": "pressure", "device_ids": [device], "kind": "bounds", "upper": 15, "unit": "m"}]) result = await service.create(config, monitoring_interval_seconds=900) while result.status != "completed": await service.command(result.replay_id, ScadaReplayCommand( request_id=uuid4(), expected_revision=result.revision, action="step" )) result = await service.process_pending_step(result.replay_id) assert result.status == "completed" assert result.processed_count == 5 assert result.anomaly_count == 1 events = await repo.events(result.replay_id) assert {event["change"] for event in events} == {"raised", "resolved"} assert all(event["task_id"] is None for event in events) raw = await (await ts.execute("SELECT monitored_value,cleaned_value FROM scada.measurements WHERE device_id=%s ORDER BY time", (device,))).fetchall() assert [row["monitored_value"] for row in raw] == [10, 20, 20, 10, 10] assert [row["cleaned_value"] for row in raw] == [10, 20, 20, 10, 10] finally: await pg.close(); await ts.close()