Files
MeshTerm/tests/test_message_paths_screen.py
T
Jean-Pierre MartineauandClaude Opus 4.8 115fe6895d Dialogs: multi-layer floats, red data-loss tier, unknown-relay hashes
Three independent consistency passes:

1. **Message paths: unknown relays as grey full-hash**
   The message-paths dialog showed unnamed relay hops as a single lit byte (e.g.
   `e8`). Now they render at the device's path-hash-mode width, muted grey, and
   annotated with their addressed byte — `e839f2 (e8)` — so the rows stay
   readable and cross-reference the route graph's byte labels (matching named
   hops' trace presentation). Added `hash_as_name` flag to path_text widget.

2. **Dialogs: multi-layer compositor + backdrop for empty stack**
   Opening a confirm over a floating popup used to stretch that popup to
   full-frame (the single-float compositor's bug). Now the compositor maintains
   a pool of _MAX_DIALOG_LAYERS=8 centered-box floats, each rendering one layer
   above the background — so a popup and its confirm both stay their own size.
   Also: button_dialog/typed_confirm/autocomplete now float over a blank base
   when the stack is empty (via _run_dialog_screen), guaranteeing popups render
   as small boxes on an empty stack, not full-frame chrome. Simplified
   message_dialog to delegate its backdrop logic to button_dialog.

3. **Data-loss dialogs: red `destructive` tier**
   Added a second caution tier to the dialog API: danger=True stays amber
   (disruptive: discard edits, reboot), destructive=True is red (irreversible
   data loss). Updated: single-record delete, channel "clear slot", courier
   "clear finished" (new confirm), and startup "remove device" (splash confirm).
   Per UX spec: bulk/irreversible deletions gate behind typed_confirm (also red);
   single-record delete is a red Cancel/[verb] button dialog.

All changes are backward-compatible. Full suite: 793 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:30:58 -04:00

140 lines
6.2 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.
"""Message-paths dialog tests: the graph-over-rows view behind the chat's ^P.
Driven headless like the atlas tests: render_body is pure lines-out, handle() pure
state, so the selection cursor, the horizontal line scroll, and the selected-path
labelling are all assertable without a terminal.
"""
from __future__ import annotations
import re
from datetime import timedelta
from meshterm.core.models import ChatMessage, utcnow
from meshterm.services.message_paths import Arrival
from meshterm.ui.message_paths_screen import MessagePathsScreen
def _resolve(hop: str) -> str:
return {"3d63": "YUL-Cartierville", "a1b2": "Waymarker"}.get(hop, hop)
def _plain(lines: list[str]) -> str:
return re.sub(r"\x1b\[[0-9;]*m", "", "\n".join(lines))
def _screen(arrivals: list[Arrival], **kwargs) -> MessagePathsScreen:
message = ChatMessage(text="on my way", is_channel=True, created_at=utcnow())
defaults = dict(
matched=True, resolve=_resolve, prefix_bytes=1, self_name="Homestead",
summary="heard twice", source="Alice",
)
defaults.update(kwargs)
return MessagePathsScreen(message, arrivals, **defaults)
def _arrivals() -> list[Arrival]:
now = utcnow()
return [
Arrival(when=now, hops=("3d63",), snr=4.0),
Arrival(when=now + timedelta(seconds=2), hops=("a1b2", "77aa"), snr=-2.0),
]
def test_paths_screen_renders_graph_rows_and_cursor() -> None:
"""The quoted text, the graph endpoints, and one selectable row per arrival."""
screen = _screen(_arrivals())
body = _plain(screen.render_body(76))
assert "“on my way”" in body
assert "Alice" in body and "Homestead" in body # origin and us, on the graph
assert body.count("via") == 2 # one row per arrival
assert "❯" in body
assert "colour = SNR of selected" in body # the graph caption
assert "★ you" in body and "▲ repeater" in body # the node-type legend
assert screen.cursor_line() is not None
def test_paths_screen_labels_every_relay_with_its_hash_byte() -> None:
"""Graph relays carry their first hash byte, both paths at once; names stay
in the rows, annotated with the same byte."""
screen = _screen(_arrivals())
body = _plain(screen.render_body(76))
graph = body.split("origin →")[0]
for byte in ("3d", "a1", "77"): # every relay labelled, selected or not
assert byte in graph
assert "YUL-Cartier" not in graph and "Waymarker" not in graph
assert "YUL-Cartierville (3d)" in body # the rows carry name + hash byte
assert "Waymarker (a1)" in body
def test_paths_screen_shows_unknown_relay_as_grey_mode_width_hash() -> None:
"""An unnamed relay stands in its own hash at the device's path-hash width, muted
grey and annotated with its addressed byte — not the bare one-byte prefix, lit."""
now = utcnow()
arrivals = [
Arrival(when=now, hops=("3d63",), snr=4.0), # selected: a named relay
Arrival(when=now + timedelta(seconds=2), hops=("e839f2ab",), snr=-2.0),
]
screen = _screen(arrivals, prefix_bytes=3) # 3-byte routing → e839f2
lines = screen.render_body(76)
row = next(ln for ln in lines if "e839f2" in _plain([ln]))
assert "e839f2 (e8)" in _plain([row]) # mode-width identity, then the byte
assert "38;2;148;163;184" in row # muted grey over the hash
assert "38;2;94;234;212" not in row # never the brand highlight
graph = _plain(lines).split("origin →")[0]
assert "e8" in graph # the row's byte still cross-references the graph label
def test_paths_screen_colours_selected_path_by_snr_over_gray() -> None:
"""The selected path's edges take its arrival's SNR hue; the unused path stays gray."""
screen = _screen(_arrivals()) # selected arrival's SNR is +4.0 dB → the "ok" band
raw = "\n".join(screen.render_body(76)).split("origin →")[0]
assert "38;2;251;191;36" in raw # snr.ok amber — the selected path, by SNR
assert "38;2;110;110;110" in raw # the other path, gray beneath it
screen.handle("down") # select the weaker arrival (−2.0 dB, still "ok")
raw2 = "\n".join(screen.render_body(76)).split("origin →")[0]
assert "38;2;251;191;36" in raw2 # the newly selected path recolours
def test_paths_screen_marks_a_repeater_relay_with_its_triangle() -> None:
"""A relay whose type resolves to a repeater draws ▲, not the generic dot."""
screen = _screen(_arrivals(), type_of=lambda h: 2 if h == "3d63" else None)
graph = _plain(screen.render_body(76)).split("origin →")[0]
assert "▲" in graph # the repeater relay wears its map glyph
screen.handle("down")
raw = "\n".join(screen.render_body(76)).split("origin →")[0]
assert "38;2;255;255;255" in raw and "38;2;110;110;110" in raw
def test_paths_screen_scrolls_the_selected_line_sideways() -> None:
"""→ shifts the whole selected row under a leading …; ↑↓ snap it back."""
now = utcnow()
long = Arrival(when=now, hops=tuple(f"{i:02x}{i:02x}" for i in range(12)), snr=1.0)
screen = _screen([long, Arrival(when=now, hops=(), snr=None)])
narrow = 40
before = _plain(screen.render_body(narrow))
selected_before = next(ln for ln in before.split("\n") if ln.startswith("❯"))
assert screen._hmax > 0 # the row genuinely overflows at this width
for _ in range(3):
screen.handle("right")
after = _plain(screen.render_body(narrow))
selected_after = next(ln for ln in after.split("\n") if ln.startswith("❯"))
assert selected_after != selected_before
assert "…" in selected_after # the left edge marks the hidden head
stamp = long.when.astimezone().strftime("%H:%M:%S")
assert stamp in selected_before and stamp not in selected_after # whole line shifts
screen.handle("down")
assert screen._hshift == 0 # only the selected line stays scrolled
def test_paths_screen_direct_arrival_and_empty_state() -> None:
"""A hop-less arrival reads direct; no arrivals at all explain themselves."""
now = utcnow()
screen = _screen([Arrival(when=now, hops=(), snr=2.5)])
body = _plain(screen.render_body(76))
assert "direct" in body
empty = _screen([], matched=False)
body = _plain(empty.render_body(76))
assert "No direct-message frames logged in the window." in body
assert empty.footer_hint == "Esc close"