Files
MeshTerm/tests/test_walk.py
T
Jean-Pierre MartineauandClaude Opus 5 cbdabef174 Find query: put it where the platform can actually show it
The map and the mesh walk echo the query you are typing in their footer
hint. The PicoCalc draws no footer hint -- the F-key lane took that row
-- so on the device both screens silently filtered themselves while the
reader had no way to see what they had typed.

The app already had an answer: the select list and the path composer
each draw the query as a warn-styled /query line above the rows it
narrows. That is now render.query_line, one definition, and the map and
the walk draw it too -- but only where the footer isn't, so the desktop
is untouched and no screen ever shows the query twice.

The row it costs comes from what the query narrows: the walk's link
list, the map's canvas. The map rebuilds its viewport a row shorter
through the ordinary resize path, so beginning a find loses a strip of
ground along the bottom rather than pushing the last row out of frame.

base_body_size was still mirroring only compose_base's bordered branch,
so a screen sizing itself against it left one row of a borderless frame
permanently blank -- which is the row the echo now wants back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:35:20 -04:00

550 lines
23 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.
"""Mesh walk tests: the focus-and-walk browser over a hand-built topology.
The screen is driven headless against a fake session, the same approach as the
dashboard tests: render_body is pure lines-out, handle() is pure state, so walking,
backtracking, find, and the canvas/list split are all assertable without a terminal.
"""
from __future__ import annotations
from datetime import timedelta
from meshterm.core.models import Contact, utcnow
from meshterm.services.topology import MeshTopology
from meshterm.ui.walk_screen import WalkScreen
US = "aa" * 6
YUL = Contact(name="YUL-Cartierville", public_key="3d" * 32, node_type=2)
ALICE = Contact(name="Alice", public_key="b2" * 32, last_seen=utcnow() - timedelta(minutes=5))
class _FakeSession:
def __init__(self) -> None:
self.repaints = 0
def invalidate(self) -> None:
self.repaints += 1
def _topo(*, with_island: bool = False) -> MeshTopology:
"""us — YUL — Alice as a two-ring chain, optionally plus a detached island pair."""
topo = MeshTopology(US, contacts=[YUL, ALICE])
yul = topo.canonical(YUL.public_key)
alice = topo.canonical(ALICE.public_key)
when = utcnow()
topo.add_walk([topo.self_id, yul], snrs=[6.0], when=when, source="trace")
topo.add_walk([yul, alice], snrs=[-2.0], when=when, source="packet")
if with_island:
topo.add_walk(["c3" * 6, "d4" * 6], snrs=[1.0], when=when, source="neighbour")
return topo
def _screen(topo: MeshTopology, cell_h: int = 24) -> WalkScreen:
screen = WalkScreen(
session=_FakeSession(),
topo=topo,
contacts={
topo.canonical(YUL.public_key): YUL,
topo.canonical(ALICE.public_key): ALICE,
},
self_label="Homestead",
)
screen.note_viewport(cell_h) # the frame records this before every real paint
return screen
from tests.conftest import plain as _plain # THE strip-and-join screen reader
# --- rendering ----------------------------------------------------------------------------
def test_walk_opens_focused_on_us_with_the_link_list() -> None:
"""The default focus is our own node: canvas, legend, and its links beneath."""
screen = _screen(_topo())
body = _plain(screen.render_body(80))
assert "Homestead" in body and "this device" in body
assert "Links" in body and "strongest observed first" in body
assert "YUL-Cartierville" in body # our one neighbour, as a selectable row
assert "edge = SNR" in body # the canvas legend
assert screen.title == "Mesh walk — Homestead · 3 nodes · 2 links"
def test_walk_link_rows_carry_snr_evidence_and_onward_count() -> None:
"""A neighbour row reads SNR, samples, source tags, age, and its onward links."""
screen = _screen(_topo())
body = _plain(screen.render_body(80))
row = next(line for line in body.split("\n") if "YUL-Cartierville" in line and "❯" in line)
assert "+6.0" in row # the link's median SNR
assert "1×" in row # samples
assert "T" in row # trace evidence tag
assert "⋯ 1" in row # one link onward (YUL — Alice)
assert "3d" * 6 in row # the whole 12-hex id, not a truncated prefix…
assert "…" not in row # …and nothing about it elided
def test_walk_link_row_shrinks_the_key_before_the_name() -> None:
"""A long name keeps its letters; the key gives ground first, on a byte boundary.
When the row can't hold both a long name and the full 12-hex id, the key truncates to
its lit hash plus a ``…`` (an even, whole-byte count) rather than the name losing
characters — and the hash prefix is never the part dropped.
"""
long_repeater = Contact(
name="Repeater-Downtown-01", public_key="d4c3b2a1f0e9" + "00" * 26, node_type=2
)
topo = MeshTopology(US, contacts=[long_repeater])
node = topo.canonical(long_repeater.public_key)
topo.add_walk([topo.self_id, node], snrs=[4.0], when=utcnow(), source="trace")
screen = WalkScreen(
session=_FakeSession(),
topo=topo,
contacts={node: long_repeater},
self_label="Homestead",
prefix_bytes=1, # the first byte (``d4``) is the addressed hash
)
screen.note_viewport(24)
row = next(
line for line in _plain(screen.render_body(68)).split("\n")
if "Repeater-Downtown-01" in line and "❯" in line # the list row, not a canvas label
)
assert "Repeater-Downtown-01" in row # the name shows whole, not clipped
assert "…" in row # the key is the lane that gave ground
assert "d4c3" in row # its lit hash (and a byte or two more) survives
assert "d4c3b2a1f0e9" not in row # …but not the whole id — it was truncated
def test_walk_graph_labels_names_in_full_when_the_canvas_has_room() -> None:
"""A neighbour's name is drawn whole on the canvas, not clipped to a flat short cap.
The fan pulls west of the edge and each label clamps to the room actually there, so a
long contact name keeps its letters wherever the canvas can hold it.
"""
long_names = [
Contact(name="YUL-Polytechnique", public_key="e8" * 32, node_type=2),
Contact(name="Repeater-Downtown-01", public_key="d4" * 32, node_type=2),
]
topo = MeshTopology(US, contacts=long_names)
when = utcnow()
for c, snr in zip(long_names, (8.0, 1.0)):
topo.add_walk([topo.self_id, topo.canonical(c.public_key)], snrs=[snr], when=when,
source="trace")
screen = WalkScreen(
session=_FakeSession(),
topo=topo,
contacts={topo.canonical(c.public_key): c for c in long_names},
self_label="Homestead",
)
screen.note_viewport(24)
# The canvas rows are everything above the legend line.
lines = _plain(screen.render_body(80)).split("\n")
canvas = "\n".join(lines[: next(i for i, l in enumerate(lines) if "edge = SNR" in l)])
assert "YUL-Polytechnique" in canvas # 17 chars, drawn whole — no "YUL-Polytechniq…"
assert "Repeater-Downtown-01" in canvas # 20 chars, whole
def test_walk_selected_link_lights_the_route_that_reaches_it() -> None:
"""Selecting a link draws its edge *and* the approach into the focus at full strength.
With every link stale (so an un-highlighted edge fades to half), the selected
neighbour's edge and the came_from → focus approach are drawn undimmed — the lit
route — while an unselected neighbour's edge stays faded.
"""
from meshterm.ui.walk_screen import _snr_rgb
stale = utcnow() - timedelta(days=10) # older than a week → _freshness 0.5
yul = Contact(name="YUL", public_key="3d" * 32, node_type=2)
alice = Contact(name="Alice", public_key="b2" * 32)
bob = Contact(name="Bob", public_key="c4" * 32)
topo = MeshTopology(US, contacts=[yul, alice, bob])
y = topo.canonical(yul.public_key)
topo.add_walk([topo.self_id, y], snrs=[10.0], when=stale, source="trace") # approach: green
topo.add_walk([y, topo.canonical(alice.public_key)], snrs=[0.0], when=stale, source="trace") # amber
topo.add_walk([y, topo.canonical(bob.public_key)], snrs=[-15.0], when=stale, source="trace") # red
screen = WalkScreen(
session=_FakeSession(),
topo=topo,
contacts={topo.canonical(c.public_key): c for c in (yul, alice, bob)},
self_label="Homestead",
)
screen.note_viewport(24)
screen.render_body(80)
screen.handle("enter") # walk to YUL — now came_from is us, the approach edge
# Land the selection on Alice (a fan neighbour, not the ⌫-back row).
while screen._rows()[screen._index] != topo.canonical(alice.public_key):
screen.handle("down")
ansi_lines = screen.render_body(80)
legend = next(i for i, l in enumerate(ansi_lines) if "edge = SNR" in _plain([l]))
canvas = "".join(ansi_lines[:legend])
def code(rgb: tuple[int, int, int]) -> str:
return f"38;2;{rgb[0]};{rgb[1]};{rgb[2]}m"
def scaled(rgb: tuple[int, int, int], f: float) -> tuple[int, int, int]:
return tuple(max(0, min(255, round(c * f))) for c in rgb)
assert code(_snr_rgb(10.0)) in canvas # approach edge, full-strength green
assert code(_snr_rgb(0.0)) in canvas # Alice's edge, full-strength amber
assert code(scaled(_snr_rgb(-15.0), 0.5)) in canvas # Bob's edge stays faded…
assert code(_snr_rgb(-15.0)) not in canvas # …and never reaches full strength
def test_walk_empty_graph_renders_guidance() -> None:
"""With no evidence at all the screen explains how the walk fills up."""
topo = MeshTopology(US, contacts=[])
screen = _screen(topo)
body = _plain(screen.render_body(80))
assert "no evidence to draw yet" in body
assert "trace" in body
# --- walking ------------------------------------------------------------------------------
def test_walk_enter_walks_and_grows_the_trail() -> None:
"""Enter focuses the highlighted neighbour; the breadcrumb trail reads the walk."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
screen.handle("enter") # walk to YUL (our only neighbour)
assert screen._focus == topo.canonical(YUL.public_key)
body = _plain(screen.render_body(80))
assert "Homestead › YUL-Cartierville" in body # the trail
# The focus line reads name (hash) — the glyph carries the type, not a spelled-out kind.
assert "YUL-Cartierville (3d)" in body and "1 hop out" in body
assert "Alice" in body # YUL's onward neighbour is now a row
assert "⌫ back" in body # the row leading home is marked
def test_walk_backspace_steps_back_along_the_trail() -> None:
"""⌫ pops the trail one step; at the trail's start it does nothing."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
screen.handle("enter")
assert len(screen._trail) == 2
screen.handle("backspace")
assert screen._trail == [topo.self_id]
screen.handle("backspace") # already home — inert
assert screen._trail == [topo.self_id]
def test_walk_walking_into_the_back_node_pops_instead_of_growing() -> None:
"""Enter on the trail-back row retraces rather than appending a ping-pong walk."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
screen.handle("enter") # us → YUL
rows = screen._rows()
screen._index = rows.index(topo.self_id) # highlight the row leading back home
screen.handle("enter")
assert screen._trail == [topo.self_id] # popped, not [us, yul, us]
def test_walk_walking_to_an_earlier_node_drops_the_loop() -> None:
"""Revisiting a node already on the trail truncates the stack to its first appearance,
dropping the circular stretch walked to get back there."""
topo = MeshTopology(US, contacts=[YUL, ALICE])
yul = topo.canonical(YUL.public_key)
alice = topo.canonical(ALICE.public_key)
when = utcnow()
topo.add_walk([topo.self_id, yul], snrs=[6.0], when=when, source="trace")
topo.add_walk([yul, alice], snrs=[-2.0], when=when, source="packet")
topo.add_walk([topo.self_id, alice], snrs=[3.0], when=when, source="trace") # closes the loop
screen = _screen(topo)
def walk_to(node: str) -> None:
screen.render_body(80)
screen._index = screen._rows().index(node)
screen.handle("enter")
walk_to(yul) # us → YUL
walk_to(alice) # YUL → Alice
assert screen._trail == [topo.self_id, yul, alice]
walk_to(yul) # Alice → YUL: loops back, so Alice is dropped, not re-appended
assert screen._trail == [topo.self_id, yul]
def test_walk_trail_drops_the_head_not_the_tail_when_narrow() -> None:
"""A trail too long for the line loses its head behind a leading ⋯, snapped right."""
screen = _screen(_topo())
us = screen._topo.self_id
yul = screen._topo.canonical(YUL.public_key)
alice = screen._topo.canonical(ALICE.public_key)
screen._trail = [us, yul, alice]
width = 20 # too narrow for the whole "Homestead › YUL-Cartierville › Alice"
text = screen._trail_text(width).plain
assert text.lstrip().startswith("⋯")
assert text.endswith("Alice") # the focus is always kept
assert "Homestead" not in text # the head was dropped, not the tail
assert len(text) == width # the surviving tail snaps flush to the right edge
def test_walk_trail_sits_left_until_it_overflows() -> None:
"""A trail that fits is left-aligned; only an elided one snaps to the right edge."""
screen = _screen(_topo())
screen._trail = [screen._topo.self_id]
assert screen._trail_text(40).plain == "Homestead"
def test_walk_trail_names_carry_their_node_hues() -> None:
"""Trail names take the per-node key hue (ours the white you-style), the focus bold."""
from meshterm.ui.theme import node_style
screen = _screen(_topo())
us = screen._topo.self_id
yul = screen._topo.canonical(YUL.public_key)
screen._trail = [us, yul]
text = screen._trail_text(80)
plain = text.plain
yul_at = plain.index("YUL-Cartierville")
assert any(
s.start <= yul_at < s.end and "bold" in str(s.style) and node_style(yul) in str(s.style)
for s in text.spans
)
def test_walk_home_refocuses_us() -> None:
"""Home resets the walk to our own node from anywhere."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
screen.handle("enter") # us → YUL
screen.handle("home")
assert screen._trail == [topo.self_id]
def test_walk_echoes_the_find_query_above_the_matches_it_narrows() -> None:
"""The PicoCalc draws no hint line, so the query it carries moves into the body."""
from meshterm.platforms import PICOCALC, REGULAR, set_platform
try:
set_platform(REGULAR)
desktop = _screen(_topo())
desktop.render_body(80)
for ch in "al":
desktop.handle("text", ch)
body = _plain(desktop.render_body(80))
assert "/al" not in body and "find: al" in desktop.footer_hint
set_platform(PICOCALC)
device = _screen(_topo())
device.render_body(53)
for ch in "al":
device.handle("text", ch)
lines = [_plain(line) for line in device.render_body(53)]
assert len(lines) <= 24 # still inside the viewport it was handed
# The echo sits directly above the heading of the list it narrows — the list, not
# the canvas, is what cedes the row for it.
echo = next(i for i, line in enumerate(lines) if line.strip() == "/al")
assert lines[echo + 1].startswith("Matches")
# Clearing the find takes the row back with it.
for _ in "al":
device.handle("backspace")
assert not any(line.strip().startswith("/") for line in
(_plain(l) for l in device.render_body(53)))
finally:
set_platform(REGULAR)
def test_walk_fkey_lane_says_you_where_the_shared_lane_would_say_top() -> None:
"""Home drops the trail rather than scrolling, and End is bound to nothing at all."""
from meshterm.ui.tui.fkeys import action_for
screen = _screen(_topo())
screen.render_body(80)
lane = screen.fkey_lane
assert [pair.label if pair else None for pair in lane] == [
None, None, None, "Page ↓", "Page ↑",
]
# The jump behind Page ↑ goes back to us; the one behind Page ↓ doesn't exist here.
assert lane[4].opp_label == "You" and action_for(lane, 10) == "home"
assert lane[3].opp_label == "" and action_for(lane, 9) is None
def test_walk_came_from_anchors_west_and_the_fan_stays_east() -> None:
"""The trail-back node sits at the far west; every fan node east of the focus."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
screen.handle("enter") # focus YUL; we came from us
alice = topo.canonical(ALICE.public_key)
fx, fy = screen._focus_pos(80, 12)
placed = screen._place_neighbours(80, 12, [alice], topo.self_id, False)
bx, by = placed[topo.self_id]
assert bx < fx and by > fy # back home: far left, ducked under the focus label
assert placed[alice][0] > fx # the fan is east of the focus
assert fx <= (80 * 2) // 3 # and the focus itself leans left
def test_walk_fan_rim_leaves_spread_east_not_curling_back() -> None:
"""The fan is a flattened arc: the top/bottom leaves reach well east too.
On the old circular arc only the due-east leaf reached the far side; the rim leaves
curled back toward the focus (to ~0.31 of the reach), leaving the corners empty. The
wedge flattens the horizontal reach, so a rim leaf lands east of the midpoint between
the fan's anchor and its due-east tip — the fan spreads across the width.
"""
fan = [f"{i + 0x20:02x}" * 6 for i in range(5)]
topo = MeshTopology(US, contacts=[])
when = utcnow()
for i, node in enumerate(fan):
topo.add_walk([topo.self_id, node], snrs=[5.0 - i], when=when, source="trace")
screen = _screen(topo)
ax, _ay, _name = screen._focus_anchor(80, 14)
placed = screen._place_neighbours(80, 14, fan, None, False)
tip = max(x for x, _y in placed.values()) # the due-east leaf, the fan's far tip
midpoint = ax + (tip - ax) / 2
assert placed[fan[0]][0] > midpoint # the topmost leaf reaches past halfway east…
assert placed[fan[-1]][0] > midpoint # …and so does the bottommost
def _hub_topo(spokes: int) -> MeshTopology:
"""Us at the centre of a ``spokes``-neighbour hub, strengths descending."""
topo = MeshTopology(US, contacts=[])
when = utcnow()
for i in range(spokes):
node = f"{i:02x}" * 6
for _ in range(spokes - i): # more samples = stronger, so the order is fixed
topo.add_walk([topo.self_id, node], snrs=[5.0], when=when, source="trace")
return topo
def test_walk_collapses_the_weak_links_into_one_ellipsis_marker() -> None:
"""Beyond the area's capacity, weaker neighbours fold into a single ``…`` node."""
screen = WalkScreen(
session=_FakeSession(), topo=_hub_topo(14), contacts={}, self_label="us"
)
screen.note_viewport(20)
body = _plain(screen.render_body(80))
assert "weaker" in body # the collapsed marker is labelled "+n weaker"
canvas_part = body.split("Links")[0]
assert "…" in canvas_part
# The list still names every neighbour — selection is the list's job.
assert len(screen._rows()) == 14
def test_walk_fan_stays_sparse_and_collapses_the_rest() -> None:
"""The fan is kept deliberately sparse: even a modest hub sheds its weakest links."""
screen = WalkScreen(
session=_FakeSession(), topo=_hub_topo(8), contacts={}, self_label="us"
)
screen.note_viewport(20)
canvas_part = _plain(screen.render_body(80)).split("Links")[0]
assert "…" in canvas_part and "weaker" in canvas_part # not all eight are drawn
def test_walk_labels_non_selected_nodes_to_the_right_of_their_icon() -> None:
"""A fan node is named just to the right of its marker — the walk's reading way."""
from meshterm.ui.mapcanvas import MapCanvas
screen = _screen(_topo())
canvas = MapCanvas(80, 12)
canvas.marker(40, 20, "●", (255, 255, 255)) # a marker with room to its east
screen._label_right(canvas, 40, 20, "Bravo", (200, 200, 200))
marker_cx = 40 >> 1
assert canvas._label_cells # the name landed
assert all(cx > marker_cx for cx, _cy in canvas._label_cells) # every cell east
def test_walk_selecting_a_collapsed_row_lights_the_ellipsis_with_its_name() -> None:
"""Highlighting a weak (collapsed) row surfaces its name at the ``…`` marker."""
screen = WalkScreen(
session=_FakeSession(), topo=_hub_topo(14), contacts={}, self_label="us"
)
screen.note_viewport(20)
screen.render_body(80)
screen._index = len(screen._rows()) - 1 # the weakest row, surely collapsed
body = _plain(screen.render_body(80))
weakest = screen._rows()[-1][:8]
canvas_part = body.split("Links")[0]
assert weakest in canvas_part # the ellipsis marker took the selection's label
assert "weaker" not in canvas_part # ...replacing the "+n weaker" count
def test_walk_body_fits_the_viewport_and_windows_the_list() -> None:
"""The screen never outgrows the frame; only the link list scrolls, marked."""
screen = WalkScreen(
session=_FakeSession(), topo=_hub_topo(16), contacts={}, self_label="us"
)
screen.note_viewport(22)
lines = screen.render_body(80)
assert len(lines) <= 22 # canvas + chrome + list window == the viewport
body = _plain(lines)
assert "↓" in body and "more" in body # the window marks the rows below
assert "Links" in body
def test_walk_pgdn_pages_the_highlight_by_the_list_window() -> None:
"""PgUp/PgDn stride by the list window, and the window follows the highlight."""
screen = WalkScreen(
session=_FakeSession(), topo=_hub_topo(16), contacts={}, self_label="us"
)
screen.note_viewport(22)
screen.render_body(80)
stride = screen._list.page
assert stride >= 1
screen.handle("pagedown")
assert screen._index == min(15, stride)
for _ in range(6):
screen.handle("pagedown")
assert screen._index == 15 # clamped at the last row
body = _plain(screen.render_body(80))
assert "↑" in body and "more" in body # rows scrolled off above are counted
# --- find ---------------------------------------------------------------------------------
def test_walk_find_lists_matches_and_teleports() -> None:
"""Typing filters every known node; Enter focuses the match and restarts the trail."""
topo = _topo()
screen = _screen(topo)
screen.render_body(80)
for ch in "ali":
screen.handle("text", ch)
assert "find: ali" in screen.footer_hint
body = _plain(screen.render_body(80))
assert "Matches" in body and "Alice" in body
assert "2 hops out" in body # the match row says how far away it sits
assert "1 match" in screen.title
screen.handle("enter")
alice = topo.canonical(ALICE.public_key)
assert screen._focus == alice
assert screen._trail == [alice] # a teleport restarts the trail
assert screen._filter == ""
def test_walk_find_marks_islands() -> None:
"""A match with no path to us reads island, and focusing it says why."""
screen = _screen(_topo(with_island=True))
screen.render_body(80)
for ch in "c3":
screen.handle("text", ch)
body = _plain(screen.render_body(80))
assert "island" in body
screen.handle("enter")
body = _plain(screen.render_body(80))
assert "island — no observed path to you" in body
def test_walk_esc_peels_find_then_dismisses() -> None:
"""Esc clears an active find first; the next Esc leaves the screen."""
import asyncio
screen = _screen(_topo())
screen.render_body(80)
screen.handle("text", "a")
async def drive() -> object:
screen.future = asyncio.get_running_loop().create_future()
screen.handle("escape") # peels the filter
assert screen._filter == "" and not screen.future.done()
screen.handle("escape") # dismisses
return await screen.future
assert asyncio.run(drive()) is None