netx/packages/netx-topology-mcp/tests/test_untangle.py
oliver 79f929de0c Converge netx-topology MCP to dual-unit drain + orbit/polish mainline.
Trim public tools to 14, expose only kept layout actions/recipes, merge Fabric queries, drop createTopologyView/listTopologyViews, and align skill/docs with the live catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 23:18:53 +08:00

54 lines
1.9 KiB
Python

"""Untangle must not fling nodes across the canvas."""
from __future__ import annotations
import math
from netx_topology_mcp.layout_ops import build_state_from_nodes_edges
from netx_topology_mcp.layout_ops.untangle import _MAX_JUMP, _candidates, untangle_crossings
def test_candidates_stay_near_node() -> None:
pos = {
"a": (0.0, 0.0),
"b": (5000.0, 0.0), # far neighbor — old code used dx*3.5 → 17k jump
"c": (2500.0, 800.0),
}
adj = {"c": {"a", "b"}, "a": {"c"}, "b": {"c"}}
import random
cands = _candidates(pos, "c", adj, random.Random(1))
assert cands
x0, y0 = pos["c"]
for x, y in cands:
assert math.hypot(x - x0, y - y0) <= _MAX_JUMP + 1e-6
def test_untangle_preserves_bbox_order() -> None:
# Long chain with one intentional crossing-ish layout; moves stay local.
nodes = []
edges = []
for i in range(8):
nodes.append(
{
"fabric_node_id": f"n{i}",
"name": f"N{i}-EN-1",
"x": float(i * 200),
"y": 0.0 if i % 2 == 0 else 40.0,
}
)
if i:
edges.append({"a_node_id": f"n{i-1}", "b_node_id": f"n{i}"})
# add a long chord that crosses nothing badly but gives degree
edges.append({"a_node_id": "n0", "b_node_id": "n7"})
st = build_state_from_nodes_edges(nodes, edges)
before = dict(st.positions)
xs0 = [p[0] for p in before.values()]
ys0 = [p[1] for p in before.values()]
span0 = max(max(xs0) - min(xs0), max(ys0) - min(ys0))
out = untangle_crossings(st, max_rounds=40, max_degree=7, target_crossings=0)
xs1 = [p[0] for p in out.state.positions.values()]
ys1 = [p[1] for p in out.state.positions.values()]
span1 = max(max(xs1) - min(xs1), max(ys1) - min(ys1))
# Must not explode bbox by >2x
assert span1 <= span0 * 2.0 + 800.0