fix: preserve legacy monitoring migration data

This commit is contained in:
2026-08-04 11:22:21 +08:00
parent 80a31b8dd0
commit fee707f42d
4 changed files with 270 additions and 35 deletions
+154 -1
View File
@@ -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()