netx/packages/netx-topology-mcp/tests/test_ortho_metro.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

113 lines
4.3 KiB
Python

"""Tests for ortho_metro (multi-layer H/V metro recipe)."""
from __future__ import annotations
import math
from netx_topology_mcp.layout_ops import LayoutParams, build_state_from_nodes_edges, run_recipe
from netx_topology_mcp.layout_ops.ortho_metro import (
_axis_ok,
build_ortho_metro_skeleton,
)
from netx_topology_mcp.layout_tool import RECIPE_ALIASES, run_layout_on_graph
def _core_bar_graph():
"""Two CN cores + AN stubs + one deg-2 corridor + one triangle."""
nodes = [
{"fabric_node_id": "c0", "name": "X-CN0-Y", "x": 0, "y": 0},
{"fabric_node_id": "c1", "name": "X-CN1-Y", "x": 0, "y": 0},
{"fabric_node_id": "a0", "name": "X-AN0-Y", "x": 0, "y": 0},
{"fabric_node_id": "a1", "name": "X-AN1-Y", "x": 0, "y": 0},
{"fabric_node_id": "e0", "name": "X-EN0-Y", "x": 0, "y": 0},
{"fabric_node_id": "e1", "name": "X-EN1-Y", "x": 0, "y": 0},
{"fabric_node_id": "t0", "name": "X-AN2-Y", "x": 0, "y": 0},
{"fabric_node_id": "t1", "name": "X-AN3-Y", "x": 0, "y": 0},
]
edges = [
{"a_node_id": "c0", "b_node_id": "c1"},
{"a_node_id": "c0", "b_node_id": "a0"},
{"a_node_id": "c1", "b_node_id": "a1"},
{"a_node_id": "a0", "b_node_id": "e0"},
{"a_node_id": "e0", "b_node_id": "e1"},
# triangle on a0
{"a_node_id": "a0", "b_node_id": "t0"},
{"a_node_id": "t0", "b_node_id": "t1"},
{"a_node_id": "t1", "b_node_id": "a0"},
]
return nodes, edges
def test_recipe_alias() -> None:
# ortho_metro module kept; unpublished from public recipe aliases.
assert "ortho_metro" not in RECIPE_ALIASES
def test_build_ortho_metro_places_all() -> None:
nodes, edges = _core_bar_graph()
st = build_state_from_nodes_edges(nodes, edges)
op = build_ortho_metro_skeleton(st, LayoutParams())
assert op.op == "build_ortho_metro_skeleton"
assert set(op.state.positions) == {n["fabric_node_id"] for n in nodes}
assert op.state.meta.get("rings_mode") == "ortho_metro"
meta = op.state.meta.get("ortho_metro") or {}
assert int(meta.get("axis_edges") or 0) + int(meta.get("diag_edges") or 0) == len(
op.state.links
)
def test_ortho_metro_mostly_axis_aligned() -> None:
from netx_topology_mcp.layout_metrics import compute_edge_clearance
nodes, edges = _core_bar_graph()
st = build_state_from_nodes_edges(nodes, edges)
op = build_ortho_metro_skeleton(st, LayoutParams(), skip_triangles=True)
pos = op.state.positions
axis = 0
diag = 0
for a, b in op.state.links:
if _axis_ok(pos[a], pos[b]):
axis += 1
else:
diag += 1
# Ortho first; clearance may break one chord to kill edge-through-node.
assert axis >= len(op.state.links) - 2
assert diag <= 2
clr = compute_edge_clearance(pos, op.state.links, names=op.state.names, thr=40.0)
assert int(clr.get("edge_clearance_hits") or 0) == 0
def test_ortho_metro_no_footprint_stack() -> None:
nodes, edges = _core_bar_graph()
st = build_state_from_nodes_edges(nodes, edges)
op = build_ortho_metro_skeleton(st, LayoutParams(pitch=200, side=170))
ids = list(op.state.positions)
for i, a in enumerate(ids):
for b in ids[i + 1 :]:
x1, y1 = op.state.positions[a]
x2, y2 = op.state.positions[b]
assert math.hypot(x2 - x1, y2 - y1) >= 140.0
def test_run_recipe_and_layout_tool_unpublished() -> None:
nodes, edges = _core_bar_graph()
st = build_state_from_nodes_edges(nodes, edges)
try:
run_recipe(st, "ortho_metro_v1", LayoutParams())
raise AssertionError("ortho_metro_v1 should be unpublished")
except Exception as e:
assert "ortho" in str(e).lower() or "unknown" in str(e).lower() or "recipe" in str(e).lower()
try:
run_layout_on_graph(nodes, edges, action="layout", recipe="ortho_metro")
raise AssertionError("ortho_metro recipe should be unpublished")
except ValueError as e:
assert "unknown_recipe" in str(e)
def test_skip_triangles_false_tries_all() -> None:
nodes, edges = _core_bar_graph()
st = build_state_from_nodes_edges(nodes, edges)
op = build_ortho_metro_skeleton(st, LayoutParams(), skip_triangles=False)
# Still places everyone; may leave 0 diags if coplanar H on triangle.
assert len(op.state.positions) == len(nodes)