netx/tests/mcp_remote/topology/test_pin_beam_soft_petals.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

84 lines
3.4 KiB
Python

"""Tests for stage-2 pin_beam + soft_petals actions."""
from __future__ import annotations
from netx_api.mcp_remote.topology.layout_ops import LayoutParams, build_state_from_nodes_edges
from netx_api.mcp_remote.topology.layout_ops.pin_beam import pin_beam_rigid
from netx_api.mcp_remote.topology.layout_ops.soft_petals import soft_petals_greedy
from netx_api.mcp_remote.topology.layout_tool import ACTIONS, run_layout_on_graph
def _starish():
"""Two cores + two AN hubs each with a few stubs (positions intentionally messy)."""
nodes = [
{"fabric_node_id": "c0", "name": "X-CN0-Y", "x": 100, "y": 500},
{"fabric_node_id": "c1", "name": "X-CN1-Y", "x": 900, "y": 100},
{"fabric_node_id": "a0", "name": "X-AN0-Y", "x": 200, "y": 800},
{"fabric_node_id": "a1", "name": "X-AN1-Y", "x": 800, "y": 50},
{"fabric_node_id": "e0", "name": "X-EN0-Y", "x": 50, "y": 900},
{"fabric_node_id": "e1", "name": "X-EN1-Y", "x": 350, "y": 950},
{"fabric_node_id": "e2", "name": "X-EN2-Y", "x": 750, "y": 20},
{"fabric_node_id": "e3", "name": "X-EN3-Y", "x": 950, "y": 40},
{"fabric_node_id": "e4", "name": "X-EN4-Y", "x": 250, "y": 700},
{"fabric_node_id": "e5", "name": "X-EN5-Y", "x": 850, "y": 200},
]
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": "a0", "b_node_id": "e1"},
{"a_node_id": "a0", "b_node_id": "e4"},
{"a_node_id": "a1", "b_node_id": "e2"},
{"a_node_id": "a1", "b_node_id": "e3"},
{"a_node_id": "a1", "b_node_id": "e5"},
{"a_node_id": "e0", "b_node_id": "e4"},
{"a_node_id": "e2", "b_node_id": "e5"},
]
return nodes, edges
def test_actions_include_pin_and_petals() -> None:
assert "pin_beam" not in ACTIONS
assert "soft_petals" not in ACTIONS
def test_pin_beam_aligns_cores() -> None:
nodes, edges = _starish()
st = build_state_from_nodes_edges(nodes, edges)
st.positions = {n["fabric_node_id"]: (float(n["x"]), float(n["y"])) for n in nodes}
op = pin_beam_rigid(st, LayoutParams())
assert op.op == "pin_beam"
ys = [op.state.positions["c0"][1], op.state.positions["c1"][1]]
# Either accepted with near-collinear cores, or refused (no_improvement) keeping input
if op.params.get("accepted"):
assert abs(ys[0] - ys[1]) < 5.0
else:
assert op.note in {"no_improvement", "need_ge_2_cores"}
def test_soft_petals_runs() -> None:
nodes, edges = _starish()
st = build_state_from_nodes_edges(nodes, edges)
st.positions = {n["fabric_node_id"]: (float(n["x"]), float(n["y"])) for n in nodes}
op = soft_petals_greedy(st, LayoutParams())
assert op.op == "soft_petals"
assert "accepted_n" in op.params
def test_run_layout_on_graph_pin_beam_unpublished() -> None:
nodes, edges = _starish()
try:
run_layout_on_graph(nodes, edges, action="pin_beam")
raise AssertionError("pin_beam should be unpublished")
except ValueError as e:
assert "unknown_action" in str(e)
def test_run_layout_on_graph_soft_petals_unpublished() -> None:
nodes, edges = _starish()
try:
run_layout_on_graph(nodes, edges, action="soft_petals")
raise AssertionError("soft_petals should be unpublished")
except ValueError as e:
assert "unknown_action" in str(e)