mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 05:20:47 +08:00
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>
This commit is contained in:
parent
918ece86c3
commit
52679a7cfc
150 changed files with 2367 additions and 2812 deletions
145
tests/mcp_remote/topology/test_rigid_orbit.py
Normal file
145
tests/mcp_remote/topology/test_rigid_orbit.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Bridge-orbit densify: drag rigid exclusive body about external tip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from netx_api.mcp_remote.topology.layout_ops.graph_util import build_state_from_nodes_edges
|
||||
from netx_api.mcp_remote.topology.layout_ops.rigid_orbit import (
|
||||
rigid_orbit_candidates_for_group,
|
||||
rigid_orbit_round,
|
||||
)
|
||||
from netx_api.mcp_remote.topology.layout_ops.rigid_units import groups_from_membership
|
||||
from netx_api.mcp_remote.topology.layout_ops.score import score_state
|
||||
from netx_api.mcp_remote.topology.layout_tool import ACTIONS, run_layout_on_graph
|
||||
|
||||
|
||||
def test_actions_include_rigid_orbit() -> None:
|
||||
assert "rigid_orbit" not in ACTIONS
|
||||
|
||||
|
||||
def _sparse_two_units() -> tuple:
|
||||
# Unit A near origin; unit B far right; long bridge a2—b1.
|
||||
nodes = [
|
||||
{"fabric_node_id": "p", "name": "P", "x": 0, "y": 0},
|
||||
{"fabric_node_id": "a1", "name": "A1", "x": 120, "y": 40},
|
||||
{"fabric_node_id": "a2", "name": "A2", "x": 240, "y": 0},
|
||||
{"fabric_node_id": "b1", "name": "B1", "x": 3200, "y": 0},
|
||||
{"fabric_node_id": "b2", "name": "B2", "x": 3400, "y": 60},
|
||||
{"fabric_node_id": "b3", "name": "B3", "x": 3600, "y": -40},
|
||||
]
|
||||
edges = [
|
||||
{"a_node_id": "p", "b_node_id": "a1"},
|
||||
{"a_node_id": "a1", "b_node_id": "a2"},
|
||||
{"a_node_id": "a2", "b_node_id": "b1"},
|
||||
{"a_node_id": "b1", "b_node_id": "b2"},
|
||||
{"a_node_id": "b1", "b_node_id": "b3"},
|
||||
]
|
||||
st = build_state_from_nodes_edges(nodes, edges)
|
||||
groups = groups_from_membership(
|
||||
[
|
||||
("va", ["p", "a1", "a2"]),
|
||||
("vb", ["b1", "b2", "b3"]),
|
||||
]
|
||||
)
|
||||
st.meta = {"compose_views": {"rigid_groups": groups}}
|
||||
return st, groups
|
||||
|
||||
|
||||
def test_candidates_prefer_inward_pull() -> None:
|
||||
st, groups = _sparse_two_units()
|
||||
g = next(g for g in groups if g["key"] == "vb")
|
||||
parsed = {
|
||||
"key": g["key"],
|
||||
"members": g["node_ids"],
|
||||
"pivots": g["pivots"],
|
||||
"exclusive": [n for n in g["node_ids"] if n not in g["pivots"]],
|
||||
"shared": list(g["pivots"]),
|
||||
}
|
||||
# vb has no shared pivots with va (p not in vb) — exclusive = all members
|
||||
cands = rigid_orbit_candidates_for_group(
|
||||
st.positions,
|
||||
st.links,
|
||||
parsed,
|
||||
angle_step=45,
|
||||
radii=(0.5, 0.7, 1.0),
|
||||
bridges_per_group=2,
|
||||
cand_cap=40,
|
||||
x_slack=200,
|
||||
)
|
||||
assert cands, "expected bridge-orbit candidates for far unit"
|
||||
# Best should densify (area_ratio > 1) when pulling toward a2 tip
|
||||
assert cands[0]["area_ratio"] >= 1.0 or cands[0]["radius_scale"] < 1.0
|
||||
|
||||
|
||||
def test_rigid_orbit_round_shrinks_bbox() -> None:
|
||||
st, groups = _sparse_two_units()
|
||||
before = score_state(st, fast=True)
|
||||
util0 = float((before.get("summary") or {}).get("util") or 0.0)
|
||||
xs = [p[0] for p in st.positions.values()]
|
||||
ys = [p[1] for p in st.positions.values()]
|
||||
area0 = (max(xs) - min(xs)) * (max(ys) - min(ys))
|
||||
|
||||
op = rigid_orbit_round(
|
||||
st,
|
||||
groups=groups,
|
||||
top_n=4,
|
||||
bridges_per_group=2,
|
||||
angle_step=30,
|
||||
radii=(0.45, 0.6, 0.75, 0.9),
|
||||
x_slack=500,
|
||||
max_accepts=4,
|
||||
)
|
||||
assert op.op == "rigid_orbit"
|
||||
# Similarity: pairwise distance ratios inside vb stay equal (polar scale).
|
||||
def _pair_lens(pos: dict) -> tuple[float, float]:
|
||||
d12 = math.hypot(pos["b2"][0] - pos["b1"][0], pos["b2"][1] - pos["b1"][1])
|
||||
d13 = math.hypot(pos["b3"][0] - pos["b1"][0], pos["b3"][1] - pos["b1"][1])
|
||||
return d12, d13
|
||||
|
||||
L12_0, L13_0 = _pair_lens(st.positions)
|
||||
L12_1, L13_1 = _pair_lens(op.state.positions)
|
||||
assert L12_0 > 1 and L13_0 > 1
|
||||
r12 = L12_1 / L12_0
|
||||
r13 = L13_1 / L13_0
|
||||
assert abs(r12 - r13) < 1e-5
|
||||
|
||||
if not (op.params or {}).get("reverted"):
|
||||
xs1 = [p[0] for p in op.state.positions.values()]
|
||||
ys1 = [p[1] for p in op.state.positions.values()]
|
||||
area1 = (max(xs1) - min(xs1)) * (max(ys1) - min(ys1))
|
||||
after = score_state(op.state, fast=True)
|
||||
util1 = float((after.get("summary") or {}).get("util") or 0.0)
|
||||
assert area1 < area0 * 0.98 or util1 > util0 + 1e-6
|
||||
|
||||
|
||||
def test_run_layout_rigid_orbit_action() -> None:
|
||||
st, groups = _sparse_two_units()
|
||||
nodes = [
|
||||
{
|
||||
"fabric_node_id": nid,
|
||||
"name": st.names.get(nid, nid),
|
||||
"x": st.positions[nid][0],
|
||||
"y": st.positions[nid][1],
|
||||
}
|
||||
for nid in st.positions
|
||||
]
|
||||
edges = [{"a_node_id": a, "b_node_id": b} for a, b in st.links]
|
||||
try:
|
||||
run_layout_on_graph(
|
||||
nodes,
|
||||
edges,
|
||||
action="rigid_orbit",
|
||||
params={
|
||||
"_rigid_membership": [
|
||||
{"key": g["key"], "node_ids": g["node_ids"], "pivots": g["pivots"]}
|
||||
for g in groups
|
||||
],
|
||||
"top_n": 4,
|
||||
"x_slack": 500,
|
||||
"radii": [0.5, 0.7, 0.9],
|
||||
},
|
||||
)
|
||||
raise AssertionError("rigid_orbit should be unpublished")
|
||||
except ValueError as e:
|
||||
assert "unknown_action" in str(e)
|
||||
Loading…
Add table
Add a link
Reference in a new issue