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

325 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for world-map strip-pack compose_views."""
from __future__ import annotations
import math
from netx_topology_mcp.layout_ops import LayoutParams, build_state_from_nodes_edges
from netx_topology_mcp.layout_ops.compose_views import (
ComposeBlock,
compose_into_state,
compose_params_from_overrides,
strip_pack_blocks,
)
from netx_topology_mcp.layout_tool import ACTIONS, run_layout_on_graph
def test_actions_include_compose_views() -> None:
assert "compose_views" not in ACTIONS
def test_strip_pack_preserves_relative_geometry() -> None:
a = ComposeBlock(
key="va",
positions={
"a1": (10.0, 20.0),
"a2": (110.0, 20.0),
"a3": (10.0, 120.0),
},
)
b = ComposeBlock(
key="vb",
positions={
"b1": (0.0, 0.0),
"b2": (50.0, 0.0),
},
)
merged, meta = strip_pack_blocks([a, b], pad=100.0)
assert meta["slots"] == 2
assert set(merged) == {"a1", "a2", "a3", "b1", "b2"}
# Relative offsets inside block A preserved.
dx = merged["a2"][0] - merged["a1"][0]
dy = merged["a2"][1] - merged["a1"][1]
assert abs(dx - 100.0) < 1e-6
assert abs(dy) < 1e-6
# Blocks occupy distinct slots (centroids far apart vs pad).
ax = sum(merged[n][0] for n in ("a1", "a2", "a3")) / 3
bx = sum(merged[n][0] for n in ("b1", "b2")) / 2
ay = sum(merged[n][1] for n in ("a1", "a2", "a3")) / 3
by = sum(merged[n][1] for n in ("b1", "b2")) / 2
assert abs(ax - bx) > 50 or abs(ay - by) > 50
def test_strip_pack_first_owner_wins_duplicates() -> None:
a = ComposeBlock(key="va", positions={"shared": (0.0, 0.0), "a": (10.0, 0.0)})
b = ComposeBlock(key="vb", positions={"shared": (999.0, 999.0), "b": (0.0, 0.0)})
# Portal-grow still glues the later shared block rigidly.
merged, meta = strip_pack_blocks([a, b], pad=50.0, merge_shared=True)
assert "shared" in merged
assert "a" in merged and "b" in merged
via = meta.get("merged_via") or {}
assert "rigid_shared" in via.values()
assert meta.get("order_mode") == "portal_grow"
def test_portal_grow_attaches_shared_before_orphan_misc() -> None:
"""Connected dual units grow together; huge misc without shared packs last."""
unit_ab = ComposeBlock(
key="ab",
positions={
"p_a": (0.0, 0.0),
"p_b": (200.0, 0.0),
"mid": (100.0, 40.0),
},
)
unit_bc = ComposeBlock(
key="bc",
positions={
"p_b": (0.0, 0.0),
"p_c": (200.0, 0.0),
"x1": (50.0, 30.0),
"x2": (100.0, 30.0),
"x3": (150.0, 30.0),
"x4": (80.0, -30.0),
},
)
misc = ComposeBlock(
key="misc",
positions={f"m{i}": (float(i) * 40.0, float(i % 3) * 40.0) for i in range(24)},
)
merged, meta = strip_pack_blocks(
[misc, unit_ab, unit_bc], pad=80.0, merge_shared=True
)
order = meta.get("order") or []
assert meta.get("order_mode") == "portal_grow"
# Misc is largest area but has no shared glue → last.
assert order[-1] == "misc"
# ab and bc share p_b → one of them merges rigidly onto the other.
via = meta.get("merged_via") or {}
assert via.get("ab") == "rigid_shared" or via.get("bc") == "rigid_shared"
# Shared portal has a single world coord.
assert "p_b" in merged
assert abs(merged["p_b"][0] - merged["p_b"][0]) < 1e-9
def test_portal_centroid_keeps_shared_units_near() -> None:
"""Units that share portals get closer origins than unrelated orphans."""
# Chain A—B—C via portals; orphan D has no shared nodes.
unit_a = ComposeBlock(
key="ua",
positions={
"p_ab": (0.0, 0.0),
"a1": (100.0, 0.0),
"a2": (100.0, 80.0),
},
)
unit_b = ComposeBlock(
key="ub",
positions={
"p_ab": (0.0, 0.0),
"p_bc": (200.0, 0.0),
"b1": (100.0, 40.0),
},
)
unit_c = ComposeBlock(
key="uc",
positions={
"p_bc": (0.0, 0.0),
"c1": (100.0, 0.0),
"c2": (100.0, 60.0),
},
)
orphan = ComposeBlock(
key="orphan",
positions={f"o{i}": (float(i) * 30.0, 0.0) for i in range(8)},
)
merged, meta = strip_pack_blocks(
[orphan, unit_c, unit_a, unit_b], pad=80.0, merge_shared=True
)
assert meta.get("pack_mode") == "portal_centroid"
origins = meta["origins"]
def _slot_c(key: str) -> tuple[float, float]:
sm = meta["slot_meta"][key]
ox, oy = origins[key]
return ox + 0.5 * sm["w"], oy + 0.5 * sm["h"]
ca, cb, cc, co = _slot_c("ua"), _slot_c("ub"), _slot_c("uc"), _slot_c("orphan")
# Portal-graph neighbors closer than the A–C skip.
d_ab = math.hypot(ca[0] - cb[0], ca[1] - cb[1])
d_bc = math.hypot(cb[0] - cc[0], cb[1] - cc[1])
d_ac = math.hypot(ca[0] - cc[0], ca[1] - cc[1])
assert d_ab < d_ac
assert d_bc < d_ac
# Orphans sit below the glued cluster.
glued_ymax = max(
origins[k][1] + meta["slot_meta"][k]["h"] for k in ("ua", "ub", "uc")
)
assert origins["orphan"][1] >= glued_ymax - 1e-6
# Rigid merge still glues the chain.
via = meta.get("merged_via") or {}
assert "rigid_shared" in via.values()
assert "p_ab" in merged and "p_bc" in merged
def test_fabric_bridges_pull_disjoint_units_closer() -> None:
"""No shared portal: fabric edge + higher bridge_boost / lower ideal_scale."""
unit_a = ComposeBlock(
key="ua",
positions={
"a0": (0.0, 0.0),
"a1": (120.0, 0.0),
"a2": (60.0, 80.0),
},
)
unit_b = ComposeBlock(
key="ub",
positions={
"b0": (0.0, 0.0),
"b1": (120.0, 0.0),
"b2": (60.0, 80.0),
},
)
unit_c = ComposeBlock(
key="uc",
positions={
"c0": (0.0, 0.0),
"c1": (120.0, 0.0),
"c2": (60.0, 80.0),
},
)
# Only A↔B has a fabric spoke; C is an unrelated peer (orphan strip below).
links = [("a1", "b0")]
blocks = [unit_a, unit_b, unit_c]
def _origin_gap(meta: dict, ka: str, kb: str) -> float:
oa, ob = meta["origins"][ka], meta["origins"][kb]
return math.hypot(oa[0] - ob[0], oa[1] - ob[1])
loose, meta_loose = strip_pack_blocks(
blocks,
pad=200.0,
merge_shared=True,
links=links,
fabric_bridges=True,
bridge_boost=4.0,
ideal_scale=0.85,
)
tight, meta_tight = strip_pack_blocks(
blocks,
pad=200.0,
merge_shared=True,
links=links,
fabric_bridges=True,
bridge_boost=40.0,
ideal_scale=0.3,
)
off, meta_off = strip_pack_blocks(
blocks, pad=200.0, merge_shared=True, links=links, fabric_bridges=False
)
assert meta_off.get("fabric_bridges") is False
assert meta_tight.get("fabric_bridges") is True
assert meta_tight.get("bridge_boost") == 40.0
assert meta_tight.get("ideal_scale") == 0.3
# Tunable spring: denser knobs shrink A–B slot gap.
assert _origin_gap(meta_tight, "ua", "ub") < _origin_gap(meta_loose, "ua", "ub") * 0.75
# Fabric glue puts C below the A–B component (not interleaved in strip).
glued_ymax = max(
meta_tight["origins"][k][1] + meta_tight["slot_meta"][k]["h"]
for k in ("ua", "ub")
)
assert meta_tight["origins"]["uc"][1] >= glued_ymax - 1e-6
del loose, tight, off
def test_compose_params_from_overrides_knobs() -> None:
knobs = compose_params_from_overrides(
{
"pad": 400,
"fabric_bridges": "on",
"bridge_boost": 12,
"ideal_scale": 0.4,
"spring_iters": 100,
"merge_shared": False,
}
)
assert knobs["pad"] == 400.0
assert knobs["fabric_bridges"] is True
assert knobs["bridge_boost"] == 12.0
assert knobs["ideal_scale"] == 0.4
assert knobs["spring_iters"] == 100
assert knobs["merge_shared"] is False
def test_dual_portal_flip_shortens_external_bridge() -> None:
"""When two portals fix the chord, pick the bank that shortens fabric spokes."""
unit_a = ComposeBlock(
key="ua",
positions={
"p0": (0.0, 0.0),
"p1": (200.0, 0.0),
"t": (100.0, -160.0),
},
)
unit_b = ComposeBlock(
key="ub",
positions={
"p0": (0.0, 0.0),
"p1": (200.0, 0.0),
"b": (100.0, 120.0),
},
)
links = [("b", "t")]
merged, _meta = strip_pack_blocks(
[unit_a, unit_b], pad=40.0, merge_shared=True, links=links
)
db = math.hypot(merged["b"][0] - merged["t"][0], merged["b"][1] - merged["t"][1])
# Unflipped bank would put b roughly opposite t (~280); flipped ~40.
assert db < 120.0
def test_strip_pack_merge_shared_false_keeps_first_only() -> None:
a = ComposeBlock(key="va", positions={"shared": (0.0, 0.0), "a": (10.0, 0.0)})
b = ComposeBlock(key="vb", positions={"shared": (999.0, 999.0), "b": (0.0, 0.0)})
merged, meta = strip_pack_blocks([a, b], pad=50.0, merge_shared=False)
assert meta.get("merge_shared") is False
assert "shared" in merged and "b" in merged
def test_compose_into_state_and_run_layout() -> None:
nodes = [
{"fabric_node_id": "a1", "name": "A1", "x": 0, "y": 0},
{"fabric_node_id": "a2", "name": "A2", "x": 10, "y": 0},
{"fabric_node_id": "b1", "name": "B1", "x": 0, "y": 0},
{"fabric_node_id": "b2", "name": "B2", "x": 10, "y": 0},
]
edges = [
{"a_node_id": "a1", "b_node_id": "a2"},
{"a_node_id": "b1", "b_node_id": "b2"},
]
st = build_state_from_nodes_edges(nodes, edges)
blocks = [
ComposeBlock("va", {"a1": (0.0, 0.0), "a2": (100.0, 0.0)}),
ComposeBlock("vb", {"b1": (0.0, 0.0), "b2": (80.0, 0.0)}),
]
op = compose_into_state(st, blocks, LayoutParams(), pad=200.0)
assert op.op == "compose_views"
assert op.params.get("slots") == 2
assert abs(op.state.positions["a2"][0] - op.state.positions["a1"][0] - 100.0) < 1e-6
try:
run_layout_on_graph(
nodes,
edges,
action="compose_views",
params={
"_compose_blocks": [
{"key": "va", "positions": {"a1": [0, 0], "a2": [100, 0]}},
{"key": "vb", "positions": {"b1": [0, 0], "b2": [80, 0]}},
],
"pad": 200,
},
)
raise AssertionError("compose_views should be unpublished")
except ValueError as e:
assert "unknown_action" in str(e)