Files
MeshTerm/tests/test_pathgraph.py
T
Jean-Pierre MartineauandClaude Opus 4.8 3ac43b7c35 Node graph: a layered redrawing, one marker per node, names throughout
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>
2026-07-20 00:47:23 -04:00

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