"""Persistence contracts for reliability monitoring data.""" import importlib from pathlib import Path import pytest from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine import app.models as models from app.config import Settings from app.models.device import Base @pytest.fixture async def db_session() -> AsyncSession: """Provide an isolated database containing the current model metadata.""" engine = create_async_engine("sqlite+aiosqlite:///:memory:") try: async with engine.begin() as connection: await connection.run_sync(Base.metadata.create_all) async with AsyncSession(engine, expire_on_commit=False) as session: yield session finally: await engine.dispose() async def test_probe_record_stores_packet_summary(db_session: AsyncSession): """A probe aggregate retains loss and latency needed by the state machine.""" record = models.PingRecord( device_id=1, is_alive=True, response_time_ms=12.5, round_num=1, sent_count=3, received_count=2, packet_loss_percent=33.33, average_rtt_ms=12.5, is_valid=True, ) db_session.add(record) await db_session.commit() assert record.packet_loss_percent == 33.33 assert record.received_count == 2 async def test_outbox_defaults_to_pending(db_session: AsyncSession): """New notifications stay eligible for delivery until the dispatcher claims them.""" assert hasattr(models, "NotificationOutbox") assert hasattr(models, "NotificationStatus") item = models.NotificationOutbox( alert_event_id=1, message_content="masked summary", ) db_session.add(item) await db_session.commit() assert item.status == models.NotificationStatus.pending def test_device_policy_prefers_explicit_device_overrides(): """A device-specific threshold supersedes only the configured global default.""" device = models.Device( probe_packets_per_round=5, degraded_loss_percent=None, ) policy = models.DeviceMonitoringPolicy.from_device(device, Settings()) assert policy.probe_packets_per_round == 5 assert policy.degraded_loss_percent == 20.0 async def test_migration_is_idempotent_and_records_revision(tmp_path: Path): """Rerunning the migration preserves the schema and writes one revision marker.""" migration = importlib.import_module( "migrations.versions.20260803_reliability_monitoring" ) assert hasattr(migration, "run_reliability_migration") database_url = f"sqlite+aiosqlite:///{tmp_path / 'monitoring.db'}" engine = create_async_engine(database_url) try: async with engine.begin() as connection: await connection.run_sync(migration.run_reliability_migration) await connection.run_sync(Base.metadata.create_all) async with engine.begin() as connection: await connection.run_sync(migration.run_reliability_migration) revision_count = await connection.scalar( text( "SELECT count(*) FROM schema_migrations " "WHERE revision = '20260803_reliability_monitoring'" ) ) assert revision_count == 1 finally: await engine.dispose()