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>
This commit is contained in:
oliver 2026-10-12 11:51:55 +08:00
parent fff7d63b9b
commit 315539e0fd
59 changed files with 6483 additions and 487 deletions

View file

@ -9,6 +9,7 @@ from netx_api.biz_state.iface_normalize import (
default_zte_iface_normalize_rules,
normalize_iface_name,
normalize_iface_rules,
normalize_port_map,
resolve_mapped_iface,
)
@ -71,6 +72,32 @@ class PortMapSubifTests(unittest.TestCase):
self.assertEqual(row["interface"], "xgei-0/1/0/1.55")
class NormalizePortMapTests(unittest.TestCase):
def test_keys_and_values_use_template_rules(self) -> None:
out, conflicts = normalize_port_map(
{"GE0/1/0/1": "GE0/2/0/1"},
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
)
self.assertEqual(out, {"gei0/1/0/1": "gei0/2/0/1"})
self.assertEqual(conflicts, [])
def test_conflicting_keys_are_reported_first_wins(self) -> None:
out, conflicts = normalize_port_map(
{"GE0/1": "gei0/2", "gei0/1": "gei0/3"},
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
)
self.assertEqual(out, {"gei0/1": "gei0/2"})
self.assertEqual(len(conflicts), 1)
self.assertEqual(conflicts[0]["dropped_to"], "gei0/3")
def test_same_target_duplicate_is_not_a_conflict(self) -> None:
_, conflicts = normalize_port_map(
{"GE0/1": "gei0/2", "gei0/1": "gei0/2"},
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
)
self.assertEqual(conflicts, [])
class ComparePipelineTests(unittest.TestCase):
def test_normalize_then_map_matches(self) -> None:
before = [{"interface": "GE-0/0/0/1.100", "admin": "up"}]