feat: persist probe quality and notification outbox
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user