mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Add cutover migration monitor and day-based batch retention.
Protect baselines and compare/migration-referenced batches from purge or delete, and let operators mark baselines and clean high-freq data manually. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c7539e379d
commit
8631f58113
23 changed files with 3837 additions and 91 deletions
136
tests/test_biz_migration_evaluate.py
Normal file
136
tests/test_biz_migration_evaluate.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""Unit tests for cutover migration evaluate helpers (port status focus)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from netx_api.biz_migration.evaluate import (
|
||||
dual_verdict,
|
||||
evaluate_metric_dual,
|
||||
parse_expect_set,
|
||||
port_sheet_def,
|
||||
port_status_label,
|
||||
side_verdict,
|
||||
)
|
||||
|
||||
|
||||
class ParseExpectSetTests(unittest.TestCase):
|
||||
def test_ports_go_to_interface_brief(self):
|
||||
got = parse_expect_set({"ports": ["gei-1", "gei-2", ""]})
|
||||
self.assertEqual(got["interface_brief"], {"gei-1", "gei-2"})
|
||||
self.assertEqual(got["_ports"], {"gei-1", "gei-2"})
|
||||
|
||||
|
||||
class PortSheetTests(unittest.TestCase):
|
||||
def test_port_sheet_fields(self):
|
||||
s = port_sheet_def()
|
||||
self.assertEqual(s["metric_id"], "interface_brief")
|
||||
self.assertEqual(s["key_fields"], ["interface"])
|
||||
self.assertEqual(s["compare_fields"], ["admin", "phy", "prot"])
|
||||
|
||||
def test_status_label(self):
|
||||
self.assertEqual(
|
||||
port_status_label({"admin": "up", "phy": "up", "prot": "up"}),
|
||||
"up/up/up",
|
||||
)
|
||||
self.assertEqual(port_status_label({}), "—")
|
||||
|
||||
|
||||
class VerdictTests(unittest.TestCase):
|
||||
def test_side_anomaly_gone(self):
|
||||
self.assertEqual(
|
||||
side_verdict(kind="removed", in_expect=False, window_active=True),
|
||||
("anomaly_gone", "red"),
|
||||
)
|
||||
self.assertEqual(
|
||||
side_verdict(kind="removed", in_expect=True, window_active=True),
|
||||
("expected_gone", "yellow"),
|
||||
)
|
||||
|
||||
def test_dual_migrated(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(old_kind="removed", new_kind="added", in_expect=True, window_active=True),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_dual_lost(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(old_kind="removed", new_kind="", in_expect=True, window_active=False),
|
||||
("lost", "red"),
|
||||
)
|
||||
|
||||
def test_acceptance_unfinished_is_red(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="",
|
||||
in_expect=True,
|
||||
window_active=False,
|
||||
acceptance=True,
|
||||
),
|
||||
("unfinished", "red"),
|
||||
)
|
||||
# window mode still yellow
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
acceptance=False,
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
|
||||
|
||||
class EvaluateMetricDualTests(unittest.TestCase):
|
||||
def test_port_migration_happy_path(self):
|
||||
old_base = [{"interface": "gei-old", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
old_cur: list[dict] = []
|
||||
new_cur = [{"interface": "gei-new", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
expect = parse_expect_set({"ports": ["gei-old"]})
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="interface_brief",
|
||||
key_fields=["interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["admin", "phy", "prot"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=old_cur,
|
||||
new_baseline_rows=None,
|
||||
new_current_rows=new_cur,
|
||||
port_map={"gei-old": "gei-new"},
|
||||
expect=expect,
|
||||
window_active=True,
|
||||
)
|
||||
self.assertEqual(out["progress_total"], 1)
|
||||
migrated = [r for r in out["rows"] if r["verdict"] == "migrated"]
|
||||
self.assertTrue(migrated, out["rows"])
|
||||
self.assertEqual(migrated[0]["color"], "green")
|
||||
self.assertEqual(migrated[0]["old_status"], "gone")
|
||||
self.assertEqual(migrated[0]["new_status"], "up/up/up")
|
||||
self.assertEqual(out["progress_ok"], 1)
|
||||
|
||||
def test_unexpected_loss_is_anomaly(self):
|
||||
old_base = [{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
old_cur: list[dict] = []
|
||||
new_cur = [{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
expect = parse_expect_set({"ports": []})
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="interface_brief",
|
||||
key_fields=["interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["admin", "phy", "prot"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=old_cur,
|
||||
new_baseline_rows=None,
|
||||
new_current_rows=new_cur,
|
||||
port_map={},
|
||||
expect=expect,
|
||||
window_active=False,
|
||||
)
|
||||
reds = [r for r in out["rows"] if r["color"] == "red"]
|
||||
self.assertTrue(reds, out["rows"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
97
tests/test_biz_state_retention.py
Normal file
97
tests/test_biz_state_retention.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Retention purge: age days + daily keep + baseline protection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from netx_api.db import Base
|
||||
from netx_api.models import BizStateBatch, BizStateTask
|
||||
from netx_api.biz_state.retention import purge_task_batches, protected_batch_ids
|
||||
|
||||
|
||||
class RetentionPurgeTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
TestingSession = sessionmaker(
|
||||
bind=engine, autoflush=False, autocommit=False, expire_on_commit=False
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
self.db = TestingSession()
|
||||
self.task = BizStateTask(
|
||||
id="t1",
|
||||
source="managed",
|
||||
ne_id="ne1",
|
||||
status="running",
|
||||
interval_sec=60,
|
||||
retention_days=7,
|
||||
daily_keep_enabled=False,
|
||||
daily_keep_count=10,
|
||||
)
|
||||
self.db.add(self.task)
|
||||
self.db.commit()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.db.close()
|
||||
|
||||
def _add_batch(self, bid: str, *, days_ago: float, baseline: bool = False) -> BizStateBatch:
|
||||
started = datetime.utcnow() - timedelta(days=days_ago)
|
||||
b = BizStateBatch(
|
||||
id=bid,
|
||||
task_id=self.task.id,
|
||||
status="success",
|
||||
started_at=started,
|
||||
ended_at=started,
|
||||
is_baseline=baseline,
|
||||
baseline_marked_at=started if baseline else None,
|
||||
)
|
||||
self.db.add(b)
|
||||
self.db.commit()
|
||||
return b
|
||||
|
||||
def test_age_purge_keeps_baseline(self) -> None:
|
||||
self._add_batch("old", days_ago=10)
|
||||
self._add_batch("pin", days_ago=20, baseline=True)
|
||||
self._add_batch("fresh", days_ago=1)
|
||||
info = purge_task_batches(self.db, self.task)
|
||||
self.assertEqual(info["dropped"], 1)
|
||||
ids = {b.id for b in self.db.query(BizStateBatch).all()}
|
||||
self.assertEqual(ids, {"pin", "fresh"})
|
||||
self.assertIn("pin", protected_batch_ids(self.db, task_id=self.task.id))
|
||||
|
||||
def test_daily_keep_skips_today(self) -> None:
|
||||
self.task.daily_keep_enabled = True
|
||||
self.task.daily_keep_count = 1
|
||||
self.db.commit()
|
||||
# same past calendar day: 3 batches → keep 1 newest
|
||||
yesterday_noon = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0) - timedelta(
|
||||
days=1
|
||||
)
|
||||
for i, bid in enumerate(("y1", "y2", "y3")):
|
||||
b = BizStateBatch(
|
||||
id=bid,
|
||||
task_id=self.task.id,
|
||||
status="success",
|
||||
started_at=yesterday_noon - timedelta(hours=i),
|
||||
ended_at=yesterday_noon - timedelta(hours=i),
|
||||
)
|
||||
self.db.add(b)
|
||||
# today: keep all even if > N
|
||||
self._add_batch("tod1", days_ago=0.01)
|
||||
self._add_batch("tod2", days_ago=0.02)
|
||||
self.db.commit()
|
||||
info = purge_task_batches(self.db, self.task)
|
||||
self.assertEqual(info["dropped"], 2)
|
||||
ids = {b.id for b in self.db.query(BizStateBatch).all()}
|
||||
self.assertIn("y1", ids) # newest yesterday
|
||||
self.assertIn("tod1", ids)
|
||||
self.assertIn("tod2", ids)
|
||||
self.assertNotIn("y2", ids)
|
||||
self.assertNotIn("y3", ids)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue