mirror of
https://github.com/hansjone/netx.git
synced 2026-10-19 06:53:19 +08:00
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>
This commit is contained in:
parent
9bcb8ee0e3
commit
79f929de0c
86 changed files with 31578 additions and 240 deletions
113
packages/netx-topology-mcp/tests/test_ortho_metro.py
Normal file
113
packages/netx-topology-mcp/tests/test_ortho_metro.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue