netx/tests/mcp_remote/topology/test_untangle.py
oliver 52679a7cfc MCP: serve tools from the API process over Streamable HTTP at /mcp
- netx_api/mcp_remote: ops / topology / bizMonitor tools, Bearer auth,
  per-tool RBAC filtering, ?groups= filter, readOnlyHint and group meta.
- Mount the official MCP Streamable HTTP app at /mcp (stateless);
  AuthAuditMiddleware is pure ASGI and skips /mcp.
- Let uvicorn chunk MCP responses; some Windows LSPs rewrite
  Content-Length replies on */mcp* paths into broken chunked framing.
- NETX_MCP_ALLOWED_HOSTS opts into DNS-rebinding checks (off by default
  so LAN and reverse-proxy hosts work; Bearer is the gate).
- Remove the three stdio Python MCP packages; tests moved to /mcp,
  including real-TCP framing regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:52:08 +08:00

54 lines
1.9 KiB
Python

"""Untangle must not fling nodes across the canvas."""
from __future__ import annotations
import math
from netx_api.mcp_remote.topology.layout_ops import build_state_from_nodes_edges
from netx_api.mcp_remote.topology.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