mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 06:50:49 +08:00
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>
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""Tests for sinkTopologyDualUnits selection helpers + tool registration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from netx_topology_mcp.http_tools import HTTP_MCP_TOOLS
|
|
from netx_topology_mcp.layout_metrics import count_edge_crossings
|
|
from netx_topology_mcp.layout_ops import build_state_from_nodes_edges
|
|
from netx_topology_mcp.layout_ops.dual_units import DualUnit, find_dual_portal_units
|
|
from netx_topology_mcp.layout_ops.sink_dual_units import (
|
|
batch_node_ids,
|
|
layout_and_pack_batch,
|
|
leftover_batch_ids,
|
|
park_positions,
|
|
select_dual_unit_batch,
|
|
)
|
|
|
|
|
|
def _u(uid: int, a: str, b: str, *mids: str) -> DualUnit:
|
|
path = [a, *mids, b] if mids else [a, b]
|
|
return DualUnit(portal_a=a, portal_b=b, paths=[path], tails=[], unit_id=uid)
|
|
|
|
|
|
def test_select_prefers_less_shared_portals() -> None:
|
|
units = [
|
|
_u(1, "X", "A", "m1", "m2", "m3", "m4", "m5", "m6"),
|
|
_u(2, "X", "B", "n1", "n2", "n3", "n4", "n5", "n6"),
|
|
_u(3, "C", "D", "p1", "p2", "p3", "p4", "p5", "p6"),
|
|
]
|
|
picked = select_dual_unit_batch(units, max_units=1, min_nodes=8, max_nodes=80)
|
|
assert len(picked) == 1
|
|
assert picked[0].unit_id == 3
|
|
|
|
|
|
def test_select_respects_batch_cap() -> None:
|
|
units = [
|
|
_u(1, "A", "B", "m1", "m2", "m3", "m4", "m5", "m6"),
|
|
_u(2, "C", "D", "n1", "n2", "n3", "n4", "n5", "n6"),
|
|
_u(3, "E", "F", "p1", "p2", "p3", "p4", "p5", "p6"),
|
|
]
|
|
picked = select_dual_unit_batch(
|
|
units, max_units=3, min_nodes=8, max_nodes=80, max_batch_nodes=20
|
|
)
|
|
ids = batch_node_ids(picked)
|
|
assert len(ids) <= 20
|
|
assert len(picked) >= 1
|
|
|
|
|
|
def test_leftover_and_park() -> None:
|
|
left = leftover_batch_ids(
|
|
["a", "region:x", "b", "c"], max_batch_nodes=2, exclude_ids={"b"}
|
|
)
|
|
assert left == ["a", "c"]
|
|
pos = park_positions(
|
|
{"a": (10.0, 10.0), "c": (30.0, 10.0)},
|
|
["a", "c"],
|
|
sink_pos={"z": (100.0, 50.0)},
|
|
pad=40.0,
|
|
)
|
|
assert len(pos) == 2
|
|
# Orbit block-sweep: not fixed right — just leave sink tip with clearance.
|
|
assert all("x" in p and "y" in p for p in pos)
|
|
assert max(abs(p["x"] - 100.0) for p in pos) >= 40.0 or max(
|
|
abs(p["y"] - 50.0) for p in pos
|
|
) >= 40.0
|
|
|
|
|
|
def test_layout_and_pack_batch_zero_cross_units() -> None:
|
|
nodes = [
|
|
{"fabric_node_id": "p1", "name": "BTM-AN1-P", "role": "an", "x": 0, "y": 0},
|
|
{"fabric_node_id": "p2", "name": "BTM-AN2-P", "role": "an", "x": 100, "y": 0},
|
|
{"fabric_node_id": "a1", "name": "BTM-EN-A1", "role": "en", "x": 0, "y": 0},
|
|
{"fabric_node_id": "a2", "name": "BTM-EN-A2", "role": "en", "x": 0, "y": 0},
|
|
{"fabric_node_id": "b1", "name": "BTM-EN-B1", "role": "en", "x": 0, "y": 0},
|
|
{"fabric_node_id": "b2", "name": "BTM-EN-B2", "role": "en", "x": 0, "y": 0},
|
|
{"fabric_node_id": "c1", "name": "BTM-EN-C1", "role": "en", "x": 0, "y": 0},
|
|
{"fabric_node_id": "c2", "name": "BTM-EN-C2", "role": "en", "x": 0, "y": 0},
|
|
]
|
|
edges = [
|
|
{"a_node_id": "p1", "b_node_id": "a1"},
|
|
{"a_node_id": "a1", "b_node_id": "a2"},
|
|
{"a_node_id": "a2", "b_node_id": "p2"},
|
|
{"a_node_id": "p1", "b_node_id": "b1"},
|
|
{"a_node_id": "b1", "b_node_id": "b2"},
|
|
{"a_node_id": "b2", "b_node_id": "p2"},
|
|
{"a_node_id": "p1", "b_node_id": "c1"},
|
|
{"a_node_id": "c1", "b_node_id": "c2"},
|
|
{"a_node_id": "c2", "b_node_id": "p2"},
|
|
]
|
|
st = build_state_from_nodes_edges(nodes, edges)
|
|
units = find_dual_portal_units(st)
|
|
assert units
|
|
world, reports, attach = layout_and_pack_batch(
|
|
st, units[:1], sink_pos={"z": (500.0, 0.0)}, pad=50.0, links=list(st.links)
|
|
)
|
|
assert world
|
|
assert attach.get("via") in {"orbit_orphan", "orbit_portal", "orbit_dual", "seed"}
|
|
assert reports and reports[0].get("accepted") is True
|
|
members = units[0].member_ids()
|
|
unit_links = [e for e in st.links if e[0] in members and e[1] in members]
|
|
assert count_edge_crossings(world, unit_links) == 0
|
|
|
|
|
|
def test_tools_registered() -> None:
|
|
names = {str(t.get("name") or "") for t in HTTP_MCP_TOOLS}
|
|
assert "sinkTopologyDualUnits" in names
|
|
assert "copyTopologyViewNodes" in names
|
|
assert len(names) == 14
|