fix: preserve legacy monitoring migration data
This commit is contained in:
@@ -4,7 +4,7 @@ import importlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import Column, Integer, Table, inspect, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
|
||||
import app.models as models
|
||||
@@ -72,6 +72,19 @@ def test_device_policy_prefers_explicit_device_overrides():
|
||||
assert policy.degraded_loss_percent == 20.0
|
||||
|
||||
|
||||
def test_device_policy_uses_global_default_for_invalid_persisted_override():
|
||||
"""An invalid stored override cannot weaken a device's safe probe policy."""
|
||||
device = models.Device(
|
||||
probe_packets_per_round=0,
|
||||
degraded_loss_percent=101.0,
|
||||
)
|
||||
|
||||
policy = models.DeviceMonitoringPolicy.from_device(device, Settings())
|
||||
|
||||
assert policy.probe_packets_per_round == 3
|
||||
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(
|
||||
@@ -96,3 +109,143 @@ async def test_migration_is_idempotent_and_records_revision(tmp_path: Path):
|
||||
assert revision_count == 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_migration_preserves_legacy_rows_and_owns_only_its_schema(
|
||||
tmp_path: Path,
|
||||
):
|
||||
"""The revision upgrades legacy data twice without creating unrelated models."""
|
||||
migration = importlib.import_module(
|
||||
"migrations.versions.20260803_reliability_monitoring"
|
||||
)
|
||||
database_url = f"sqlite+aiosqlite:///{tmp_path / 'legacy-monitoring.db'}"
|
||||
engine = create_async_engine(database_url)
|
||||
future_table = Table(
|
||||
"future_unrelated_model",
|
||||
Base.metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
)
|
||||
try:
|
||||
async with engine.begin() as connection:
|
||||
await connection.execute(
|
||||
text(
|
||||
"CREATE TABLE devices ("
|
||||
"id INTEGER PRIMARY KEY, name VARCHAR(128) NOT NULL, "
|
||||
"ip VARCHAR(45) NOT NULL, device_type VARCHAR(16) NOT NULL, "
|
||||
"location VARCHAR(256), project_name VARCHAR(256), "
|
||||
"tags VARCHAR(512), ping_interval INTEGER, "
|
||||
"alert_threshold INTEGER, is_enabled BOOLEAN, "
|
||||
"current_status VARCHAR(16), consecutive_failures INTEGER, "
|
||||
"last_ping_time DATETIME, last_online_time DATETIME, "
|
||||
"last_offline_time DATETIME, created_at DATETIME, "
|
||||
"updated_at DATETIME)"
|
||||
)
|
||||
)
|
||||
await connection.execute(
|
||||
text(
|
||||
"CREATE TABLE ping_records ("
|
||||
"id INTEGER PRIMARY KEY, device_id INTEGER NOT NULL, "
|
||||
"is_alive BOOLEAN NOT NULL, response_time_ms FLOAT, "
|
||||
"round_num INTEGER NOT NULL, created_at DATETIME)"
|
||||
)
|
||||
)
|
||||
await connection.execute(
|
||||
text(
|
||||
"CREATE TABLE alert_events ("
|
||||
"id INTEGER PRIMARY KEY, device_id INTEGER NOT NULL, "
|
||||
"alert_type VARCHAR(16) NOT NULL, message VARCHAR(1024), "
|
||||
"start_at DATETIME NOT NULL, end_at DATETIME, "
|
||||
"duration_minutes INTEGER, is_resolved BOOLEAN, "
|
||||
"notification_sent BOOLEAN, acknowledged_at DATETIME, "
|
||||
"created_at DATETIME)"
|
||||
)
|
||||
)
|
||||
await connection.execute(
|
||||
text(
|
||||
"INSERT INTO devices (id, name, ip, device_type, current_status) "
|
||||
"VALUES (1, 'legacy-router', '192.0.2.10', 'router', 'online')"
|
||||
)
|
||||
)
|
||||
await connection.execute(
|
||||
text(
|
||||
"INSERT INTO ping_records "
|
||||
"(id, device_id, is_alive, response_time_ms, round_num) "
|
||||
"VALUES (2, 1, 1, 8.5, 7)"
|
||||
)
|
||||
)
|
||||
await connection.execute(
|
||||
text(
|
||||
"INSERT INTO alert_events "
|
||||
"(id, device_id, alert_type, message, start_at, notification_sent) "
|
||||
"VALUES (3, 1, 'offline', 'legacy event', CURRENT_TIMESTAMP, 1)"
|
||||
)
|
||||
)
|
||||
|
||||
await connection.run_sync(migration.run_reliability_migration)
|
||||
table_names = await connection.run_sync(
|
||||
lambda sync_connection: set(
|
||||
inspect(sync_connection).get_table_names()
|
||||
)
|
||||
)
|
||||
assert "future_unrelated_model" not in table_names
|
||||
assert "notification_outbox" in table_names
|
||||
Base.metadata.remove(future_table)
|
||||
await connection.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with engine.begin() as connection:
|
||||
await connection.run_sync(migration.run_reliability_migration)
|
||||
legacy_device = await connection.execute(
|
||||
text("SELECT name, ip, current_status FROM devices WHERE id = 1")
|
||||
)
|
||||
legacy_probe = await connection.execute(
|
||||
text(
|
||||
"SELECT device_id, is_alive, response_time_ms, round_num "
|
||||
"FROM ping_records WHERE id = 2"
|
||||
)
|
||||
)
|
||||
legacy_alert = await connection.execute(
|
||||
text(
|
||||
"SELECT device_id, alert_type, message, notification_sent "
|
||||
"FROM alert_events WHERE id = 3"
|
||||
)
|
||||
)
|
||||
probe_columns, alert_columns, probe_indexes = await connection.run_sync(
|
||||
lambda sync_connection: (
|
||||
{
|
||||
column["name"]: column
|
||||
for column in inspect(sync_connection).get_columns(
|
||||
"ping_records"
|
||||
)
|
||||
},
|
||||
{
|
||||
column["name"]
|
||||
for column in inspect(sync_connection).get_columns(
|
||||
"alert_events"
|
||||
)
|
||||
},
|
||||
{
|
||||
index["name"]
|
||||
for index in inspect(sync_connection).get_indexes(
|
||||
"ping_records"
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert legacy_device.one() == ("legacy-router", "192.0.2.10", "online")
|
||||
assert legacy_probe.one() == (1, 1, 8.5, 7)
|
||||
assert legacy_alert.one() == (1, "offline", "legacy event", 1)
|
||||
assert probe_columns["is_valid"]["nullable"] is False
|
||||
assert {
|
||||
"sent_count",
|
||||
"received_count",
|
||||
"packet_loss_percent",
|
||||
"average_rtt_ms",
|
||||
"failure_reason",
|
||||
}.issubset(probe_columns)
|
||||
assert "related_event_id" in alert_columns
|
||||
assert "idx_ping_records_device_created_at" in probe_indexes
|
||||
finally:
|
||||
if future_table in Base.metadata.tables.values():
|
||||
Base.metadata.remove(future_table)
|
||||
await engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user