The contact page's route graph fanned every path as a free lane from the two endpoints and averaged shared relays together, so a well-connected node drew as an acute tangle of near-overlapping lines. Worse, the same physical node could appear twice: a 1-byte trace hop (27) standing beside its own wide contact id (27d4396a2967) whenever the short hash was too ambiguous to widen — YUL-Poly drew YUL-UpperSalaberry as two separate markers. Redraw the route graph as a layered left-to-right graph (pathgraph.py): * longest-path ranks put every node in the column of its hop distance, so each edge steps strictly forward and a relay shared by several routes is drawn once; * an edge that skips a column routes through invisible waypoints, bending around the nodes between its ends instead of slicing through them; * nodes are ordered to cut crossings (barycentre sweeps) then spread down the full height with their ys pulled toward their neighbours' (PAVA packing), which straightens each route into a lane and opens the branch angles up; * first-appearance ordering keeps the layout identical on every repaint. Kill the double-drawn node at two levels. build_topology now coalesces an under-specified id into the one longer node it can only be (27 -> 27d4396a2967), folding their links — fixing the atlas and the scenario ranker too, not just this graph — while leaving a genuinely ambiguous stub (c5 -> two nodes) alone. The widget then closes the same gap locally across its own layers, catching a hash that is ambiguous against the whole contact list but unmistakable among these paths (be -> be1d1c1dbc4b). On the node detail page specifically: name every node, not just the two ends (a relay reads by its contact name, unknown ones keep the honest hash byte), and draw the target with its own map glyph (repeater, room, sensor) to match the header. us stays on the left, the target on the right. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
5.0 KiB
Python
146 lines
5.0 KiB
Python
"""Unit tests for the shared route-graph widget (the fan-lane braille renderer)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from meshterm.ui.pathgraph import (
|
|
DST_NODE,
|
|
SRC_NODE,
|
|
PathLayer,
|
|
_coalesce_prefixes,
|
|
render_path_graph,
|
|
)
|
|
|
|
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
WHITE = (255, 255, 255)
|
|
GREEN = (74, 222, 128)
|
|
YELLOW = (250, 204, 21)
|
|
GREY = (120, 120, 120)
|
|
|
|
|
|
def _glyph(node: str) -> tuple[str, str]:
|
|
# Deliberately off-palette marker colours, so colour assertions about *edges*
|
|
# can never accidentally match a marker's escape sequence.
|
|
if node in (SRC_NODE, DST_NODE):
|
|
return ("★", "#123456")
|
|
return ("●", "#654321")
|
|
|
|
|
|
def _render(layers, label_of=None): # noqa: ANN001
|
|
return render_path_graph(
|
|
layers,
|
|
60,
|
|
glyph_of=_glyph,
|
|
label_of=label_of or (lambda node: "you" if node in (SRC_NODE, DST_NODE) else node[:2]),
|
|
label_rgb_of=lambda _node: WHITE,
|
|
)
|
|
|
|
|
|
def _sgr(rgb: tuple[int, int, int]) -> str:
|
|
r, g, b = rgb
|
|
return f"38;2;{r};{g};{b}m"
|
|
|
|
|
|
def test_no_layers_render_nothing() -> None:
|
|
assert render_path_graph(
|
|
[], 60, glyph_of=_glyph, label_of=lambda n: None, label_rgb_of=lambda n: WHITE
|
|
) == []
|
|
|
|
|
|
def test_identical_paths_collapse_to_the_top_layer() -> None:
|
|
"""The same route in two layers draws once, in the higher priority's colour."""
|
|
layers = [
|
|
PathLayer(("aa", "bb"), YELLOW, 2),
|
|
PathLayer(("aa", "bb"), WHITE, 4),
|
|
]
|
|
joined = "\n".join(_render(layers))
|
|
assert _sgr(WHITE) in joined
|
|
assert _sgr(YELLOW) not in joined
|
|
|
|
|
|
def test_distinct_layers_draw_in_their_own_colours() -> None:
|
|
"""Three different routes keep three edge colours on one canvas."""
|
|
layers = [
|
|
PathLayer(("aa", "bb"), YELLOW, 2),
|
|
PathLayer(("cc",), GREEN, 3),
|
|
PathLayer(("dd", "ee", "ff"), WHITE, 4),
|
|
]
|
|
joined = "\n".join(_render(layers))
|
|
for rgb in (YELLOW, GREEN, WHITE):
|
|
assert _sgr(rgb) in joined
|
|
|
|
|
|
def test_labels_follow_the_callback_only_self_named() -> None:
|
|
"""A ``None`` label leaves the marker bare — a caller's unnamed-relay mode."""
|
|
layers = [PathLayer(("ab", "cd"), WHITE, 4)]
|
|
lines = _render(
|
|
layers,
|
|
label_of=lambda node: "Base" if node in (SRC_NODE, DST_NODE) else None,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
assert "Base" in plain
|
|
assert "ab" not in plain and "cd" not in plain
|
|
|
|
|
|
def test_endpoint_labels_may_leave_the_marker_row() -> None:
|
|
"""Endpoints place like relays: a long name lands whole, even off the centre row."""
|
|
layers = [PathLayer(tuple(f"{i:02x}" for i in range(6)), WHITE, 4)]
|
|
lines = _render(
|
|
layers,
|
|
label_of=lambda node: "VeryLongStationName"
|
|
if node in (SRC_NODE, DST_NODE)
|
|
else None,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
# The name fits the 60-cell canvas, so it is kept whole — no fixed label budget clips it.
|
|
assert "VeryLongStationName" in plain
|
|
assert "…" not in plain
|
|
|
|
|
|
def test_prefix_dupe_relay_folds_onto_its_only_wide_match() -> None:
|
|
"""A 1-byte hop beside the same node's wide id, across layers, becomes one node."""
|
|
layers = [
|
|
PathLayer(("e9043958308d", "be"), WHITE, 3),
|
|
PathLayer(("3d63c6429436", "be1d1c1dbc4b"), GREY, 2),
|
|
]
|
|
out = _coalesce_prefixes(layers)
|
|
assert out[0].hops == ("e9043958308d", "be1d1c1dbc4b") # "be" rewritten to the wide id
|
|
assert out[1].hops == ("3d63c6429436", "be1d1c1dbc4b")
|
|
|
|
|
|
def test_ambiguous_prefix_relay_is_left_alone() -> None:
|
|
"""A short hop that prefixes two distinct present nodes is not guessed onto either."""
|
|
layers = [
|
|
PathLayer(("be",), WHITE, 3),
|
|
PathLayer(("be1d1c1dbc4b",), GREY, 2),
|
|
PathLayer(("bedd2bff0011",), GREY, 2),
|
|
]
|
|
out = _coalesce_prefixes(layers)
|
|
assert out[0].hops == ("be",) # opens two present nodes — kept as its honest short self
|
|
|
|
|
|
def test_a_shared_relay_draws_a_single_marker() -> None:
|
|
"""A relay two paths pass through is one vertex, labelled once — never doubled."""
|
|
layers = [PathLayer(("aa", "bb"), WHITE, 3), PathLayer(("cc", "bb"), YELLOW, 2)]
|
|
plain = _ANSI.sub("", "\n".join(_render(layers)))
|
|
assert plain.count("bb") == 1 # the shared relay is seated once
|
|
assert plain.count("aa") == 1 and plain.count("cc") == 1
|
|
|
|
|
|
def test_labels_ellipsize_only_when_wider_than_the_canvas() -> None:
|
|
"""A name shortens only when it physically cannot fit — wider than the whole canvas."""
|
|
name = "x" * 30
|
|
lines = render_path_graph(
|
|
[PathLayer(("aa",), WHITE, 4)],
|
|
20,
|
|
glyph_of=_glyph,
|
|
label_of=lambda node: name if node in (SRC_NODE, DST_NODE) else None,
|
|
label_rgb_of=lambda _node: WHITE,
|
|
)
|
|
plain = _ANSI.sub("", "\n".join(lines))
|
|
assert name not in plain # the full 30-cell name cannot fit a 20-cell canvas
|
|
assert "…" in plain # so it is ellipsized to what fits
|
|
assert all(len(_ANSI.sub("", ln)) <= 20 for ln in lines) # never overruns the width
|