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

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