netx/tests/test_biz_migration_retention.py
oliver 315539e0fd Optimize cutover compare/monitor per the 2026-10-12 plan.
Correctness (A): normalize port-map names with the template rules (snapshot v2,
v1 replays unchanged); protect mappings/templates that are referenced; stop
dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated;
validate baseline ownership on project create; one canonical key form; fix
built-in template tolerances and upgrade old packaged values in place.

Cross-vendor identity (B): single identity pipeline for every path, per-side and
per-vendor interface rules, value maps, filters after normalization, mapping
pre-flight for compare and cutover projects, SQL eligibility fixes.

Data quality (V): no_data / suspect_parse / not_collected sheet states, command
slice coverage, directional compare modes, value-sorted duplicate pairing,
before-side raw names in results and CSV.

Robustness (H): bounded auto-run history, background evaluate/finish with
polling, manual route tables out of the acceptance gate, baseline row cache,
scoped duplicate-key failures, merged HF tasks with a per-NE session cap,
same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red
tickets.

Frontend: pre-flight report, mapping table view, template editor support for the
new rules, data-state banners. Docs: canonical field contract.

Not included: Huawei status parsers (no real samples), SQL-path route compare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 11:51:55 +08:00

96 lines
4.1 KiB
Python

"""H1: bounded auto-run history and run-aware batch protection."""
from datetime import timedelta
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from netx_api.biz_state.retention import protected_batch_map, purge_auto_runs
from netx_api.db import Base
from netx_api.models import (
BizCompareDiff, BizCompareJob, BizCompareRun, BizMigrationBatch, BizMigrationDiff, BizMigrationRedTicket,
BizMigrationRun, BizStateBatch, BizStateTask,
)
from netx_api.timeutil import utcnow_naive
@pytest.fixture()
def db():
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine, expire_on_commit=False)()
yield session
session.close()
engine.dispose()
def _runs(db, n, *, purpose="auto", batch="wave", prefix="r"):
base = utcnow_naive() - timedelta(hours=10)
for i in range(n):
db.add(BizMigrationRun(id=f"{prefix}{i}", project_id="p", batch_id=batch, purpose=purpose,
old_batch_id=f"o-{prefix}{i}", new_batch_id=f"n-{prefix}{i}",
created_at=base + timedelta(minutes=i), summary_json={}))
db.add(BizMigrationDiff(id=f"d-{prefix}{i}", run_id=f"{prefix}{i}", metric_id="m"))
db.commit()
def test_only_the_newest_auto_runs_survive_and_acceptance_runs_stay(db):
_runs(db, 400)
_runs(db, 1, purpose="acceptance", prefix="acc")
out = purge_auto_runs(db, migration_keep=300)
assert out["migration_runs"] == 100
assert db.query(BizMigrationRun).count() == 301
assert db.get(BizMigrationRun, "acc0") is not None
assert db.get(BizMigrationRun, "r399") is not None and db.get(BizMigrationRun, "r99") is None
assert db.query(BizMigrationDiff).count() == 301 # diffs follow their run
def test_runs_pinned_by_red_tickets_or_acceptance_are_never_dropped(db):
_runs(db, 12)
db.add(BizMigrationRedTicket(id="t", project_id="p", batch_id="wave", run_id="r0"))
db.add(BizMigrationBatch(id="wave", project_id="p", accept_run_id="r1"))
db.commit()
purge_auto_runs(db, migration_keep=5)
assert db.get(BizMigrationRun, "r0") is not None and db.get(BizMigrationRun, "r1") is not None
assert db.get(BizMigrationRun, "r2") is None and db.query(BizMigrationRun).count() == 7
def test_keep_is_per_wave(db):
_runs(db, 6, batch="w1", prefix="a")
_runs(db, 6, batch="w2", prefix="b")
assert purge_auto_runs(db, migration_keep=4)["migration_runs"] == 4
assert db.query(BizMigrationRun).filter_by(batch_id="w1").count() == 4
def test_compare_keeps_manual_runs_and_trims_auto_runs_of_auto_jobs(db):
db.add(BizCompareJob(id="job", name="j", mode="auto"))
base = utcnow_naive() - timedelta(hours=5)
for i in range(8):
db.add(BizCompareRun(id=f"c{i}", job_id="job", status="success", created_at=base + timedelta(minutes=i),
summary_json={"purpose": "auto" if i != 0 else "manual"}))
db.add(BizCompareDiff(id=f"cd{i}", run_id=f"c{i}", metric_id="m"))
db.commit()
out = purge_auto_runs(db, compare_keep=3)
assert out["compare_runs"] == 4 # c1..c4; c0 is manual and survives, c5..c7 are the newest
assert {r.id for r in db.query(BizCompareRun)} == {"c0", "c5", "c6", "c7"}
assert db.query(BizCompareDiff).count() == 4
def _batch(db, bid, task="t"):
db.add(BizStateBatch(id=bid, task_id=task, status="success"))
def test_protection_follows_surviving_runs_and_all_metric_batches(db):
db.add(BizStateTask(id="t", source="managed", ne_id="n"))
for bid in ("o-r0", "n-r0", "hf-a", "hf-b", "o-r1", "n-r1", "o-acc0", "n-acc0"):
_batch(db, bid)
db.commit()
_runs(db, 2)
_runs(db, 1, purpose="acceptance", prefix="acc")
acc = db.get(BizMigrationRun, "acc0")
acc.summary_json = {"metric_batches": {"arp": {"old_batch_id": "hf-a", "new_batch_id": "hf-b"}}}
db.commit()
reasons = protected_batch_map(db)
assert "hf-a" in reasons and "hf-b" in reasons # beyond the first binding
purge_auto_runs(db, migration_keep=1) # r0 goes away
assert "o-r0" not in protected_batch_map(db) and "o-r1" in protected_batch_map(db)