The tests' docstrings lead with a sentence too
The last 122 `D205` sites, which takes `ruff check .` to zero. Same shape as the package pass and just as additive: these were a paragraph with no summary, so each one gains a plain sentence saying what is being asserted, and every word that was there moves down into the description. The dense ones benefit most — `test_pathline`'s 23, where "A seam is one cell, not two" now stands alone and the chevron mechanics explain it underneath, and `test_tui`'s wide-glyph repaint test, whose seven lines on prompt_toolkit's relative cursor now sit under the one line that says what the test pins. Four module docstrings (`test_advert`, `test_gallery`, `test_platforms`, `test_repeater_admin`) opened with "X tests: a, b, and c" and now name the subject on the first line with the list below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
466fd33d34
commit
40ecf5151d
31 files changed
+504
-273
No files matched your search
+13
-7
@@ -34,8 +34,10 @@ def _page(builder, *, viewport: int = 40, width: int = 72) -> str:
|
||||
|
||||
|
||||
def test_about_meshterm_leads_with_live_package_facts() -> None:
|
||||
"""The version and copyright come from the package, so the page can't describe a
|
||||
build it isn't running inside.
|
||||
"""The About page leads with facts read live from the package.
|
||||
|
||||
The version and copyright come from the package itself, so the page can't
|
||||
describe a build it isn't running inside.
|
||||
"""
|
||||
text = _page(about_meshterm)
|
||||
|
||||
@@ -140,8 +142,10 @@ def test_the_licence_section_states_the_terms_and_the_credit_it_owes() -> None:
|
||||
|
||||
|
||||
def test_the_discord_page_carries_the_invite_link_and_a_code_to_scan_it_with() -> None:
|
||||
"""The link is the whole page, and on a console with no clipboard the QR is how it
|
||||
leaves the screen — so both have to survive the render.
|
||||
"""The Discord page carries both the invite link and a QR code for it.
|
||||
|
||||
The link is the whole page, and on a console with no clipboard the QR is how it
|
||||
leaves the screen, so both have to survive the render.
|
||||
"""
|
||||
text = _page(join_discord)
|
||||
|
||||
@@ -184,9 +188,11 @@ def test_each_section_heading_is_a_landmark_the_page_can_pin(builder, headings)
|
||||
|
||||
|
||||
def test_the_console_lane_claims_the_section_step_only_while_the_page_scrolls() -> None:
|
||||
"""On the PicoCalc the lane is the only place a chord can advertise itself — but it
|
||||
never advertises a key that would do nothing (``CLAUDE.md``), and a page you can see
|
||||
whole has no section to step to.
|
||||
"""The console lane claims the section step only while the page actually scrolls.
|
||||
|
||||
On the PicoCalc the lane is the only place a chord can advertise itself, but it
|
||||
never advertises a key that would do nothing (``CLAUDE.md``) — and a page you can
|
||||
see whole has no section to step to.
|
||||
"""
|
||||
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Background-advert tests: the store's schedule arithmetic, the scheduler's
|
||||
one-send-per-pass discipline, and the editor/executor integration that lets manual
|
||||
adverts and staged cadence changes share one clock.
|
||||
"""Tests for the background advert.
|
||||
|
||||
The store's schedule arithmetic, the scheduler's one-send-per-pass discipline, and
|
||||
the editor/executor integration that lets manual adverts and staged cadence changes
|
||||
share one clock.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -258,8 +258,10 @@ def test_y_axis_labels_blanks_a_repeated_mark() -> None:
|
||||
|
||||
|
||||
def test_axis_chart_mirrors_marks_where_the_platform_affords_them() -> None:
|
||||
"""The desktop frames a chart with marks on both gutters; the console keeps the
|
||||
right ``├`` tick but spends the mirrored label's cells on the chart instead.
|
||||
"""An axis chart mirrors its marks only where the platform can afford them.
|
||||
|
||||
The desktop frames a chart with marks on both gutters; the console keeps the right
|
||||
``├`` tick but spends the mirrored label's cells on the chart instead.
|
||||
"""
|
||||
from meshterm.platforms import PICOCALC, set_platform
|
||||
from meshterm.ui.braillechart import axis_chrome
|
||||
|
||||
@@ -272,8 +272,10 @@ async def test_channel_capacity_tracks_a_larger_ceiling(ctx: AppContext) -> None
|
||||
|
||||
|
||||
async def test_read_channel_slots_bounds_a_never_rejecting_probe() -> None:
|
||||
"""On firmware that answers every slot (never rejecting an index), the configured-slot
|
||||
scan stops after a run of empty slots instead of walking all CHANNEL_SLOT_PROBE_CAP.
|
||||
"""A probe that is never rejected still bounds itself.
|
||||
|
||||
On firmware that answers every slot, the configured-slot scan stops after a run of
|
||||
empty slots instead of walking all of ``CHANNEL_SLOT_PROBE_CAP``.
|
||||
"""
|
||||
|
||||
class NeverRejects:
|
||||
|
||||
+12
-6
@@ -515,8 +515,10 @@ def test_title_leads_with_openness_glyph(repo: Repository) -> None:
|
||||
|
||||
|
||||
def test_title_contact_dot_reflects_conversation_history(repo: Repository) -> None:
|
||||
"""A contact's dot fills ● once we've talked, hollow ○ before — companion pink both
|
||||
ways — while the name itself carries the contact's key-derived hue.
|
||||
"""The title's contact dot reflects whether there is history with them.
|
||||
|
||||
It fills ● once we've talked and is hollow ○ before, companion pink both ways,
|
||||
while the name itself carries the contact's key-derived hue.
|
||||
"""
|
||||
from meshterm.tools.chat import _COMPANION_DOT_STYLE
|
||||
from meshterm.ui.theme import node_style
|
||||
@@ -824,8 +826,10 @@ def test_byte_counter_shows_used_over_limit_and_colors_only_used() -> None:
|
||||
|
||||
|
||||
def test_byte_counter_stays_bottom_right_when_the_compose_wraps() -> None:
|
||||
"""A compose line long enough to wrap keeps the counter pinned to the last line's
|
||||
right edge — not trailing the cursor down onto the second row.
|
||||
"""The byte counter stays bottom-right when the compose line wraps.
|
||||
|
||||
It stays pinned to the last line's right edge, rather than trailing the cursor
|
||||
down onto the second row.
|
||||
"""
|
||||
screen = _screen(_StubSession(), send=None)
|
||||
text = "this is a long compose line that certainly wraps onto several rows here ok"
|
||||
@@ -1772,8 +1776,10 @@ async def test_picker_pins_its_column_header_over_the_group_heading(
|
||||
|
||||
|
||||
async def test_picker_del_deletes_history_after_a_red_confirm(repo: Repository) -> None:
|
||||
"""Del on a contacted thread: red destructive confirm, history gone, unread cleared,
|
||||
and the rebuilt rows show the thread hollow (no longer deletable).
|
||||
"""Del on a contacted thread deletes its history, behind a red confirm.
|
||||
|
||||
The confirm is the destructive one; afterwards the history is gone, unread is
|
||||
cleared, and the rebuilt rows show the thread hollow and no longer deletable.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
@@ -382,8 +382,9 @@ def test_contact_share_url_defaults_to_companion_type() -> None:
|
||||
|
||||
|
||||
async def test_show_contact_card_pops_the_qr_over_the_link() -> None:
|
||||
"""The share popup is one floating view: a scannable QR code over the raw
|
||||
``meshcore://`` link, titled with the node's name.
|
||||
"""The contact card pops a QR code over its link, in one floating view.
|
||||
|
||||
A scannable code above the raw ``meshcore://`` link, titled with the node's name.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
@@ -72,8 +72,10 @@ def test_path_hash_mode_is_strict_enum() -> None:
|
||||
|
||||
|
||||
def test_highlighted_hash_highlights_path_hash_prefix() -> None:
|
||||
"""The full key is shown with only its path-hash prefix bytes highlighted, lit in
|
||||
the node's hash-derived palette hue (the same hue the node's name wears).
|
||||
"""A key lane lights only its path-hash prefix.
|
||||
|
||||
The full key is shown, with those bytes lit in the node's hash-derived palette
|
||||
hue — the same hue the node's name wears.
|
||||
"""
|
||||
from meshterm.ui.theme import node_style
|
||||
from meshterm.ui.widgets import highlighted_hash
|
||||
@@ -88,8 +90,10 @@ def test_highlighted_hash_highlights_path_hash_prefix() -> None:
|
||||
|
||||
|
||||
def test_highlighted_hash_truncates_on_a_byte_boundary() -> None:
|
||||
"""A key too long for the budget keeps whole leading bytes (an even digit count) plus
|
||||
an ellipsis, padding to the exact width — a half-byte digit never shows.
|
||||
"""A key too long for its budget is truncated on a byte boundary.
|
||||
|
||||
It keeps whole leading bytes (an even digit count) plus an ellipsis, padded to the
|
||||
exact width, so a half-byte digit never shows.
|
||||
"""
|
||||
from meshterm.ui.widgets import highlighted_hash
|
||||
|
||||
@@ -250,8 +254,10 @@ def _contacts_sort(name: str = "name"):
|
||||
|
||||
|
||||
def test_contacts_screen_ctrl_arrows_steer_the_sort() -> None:
|
||||
"""Ctrl+←/→ walk the shared four-column ring (wrapping); Ctrl+↑/↓ force the direction —
|
||||
the Time Machine picker's keys, now the Contacts screen's too.
|
||||
"""Ctrl+arrows steer the Contacts sort.
|
||||
|
||||
Ctrl+←/→ walk the shared four-column ring, wrapping, and Ctrl+↑/↓ force the
|
||||
direction — the Time Machine picker's keys, now the Contacts screen's too.
|
||||
"""
|
||||
from meshterm.ui.contacts_screen import ContactsScreen
|
||||
|
||||
@@ -275,8 +281,10 @@ def test_contacts_screen_ctrl_arrows_steer_the_sort() -> None:
|
||||
|
||||
|
||||
def test_contacts_screen_lists_contacts_in_the_shared_lanes() -> None:
|
||||
"""Contacts render in the shared NAME/HEARD/PKTS/KEY lanes, our node pinned first;
|
||||
plain arrows only move the highlight, and Enter resolves the highlighted node.
|
||||
"""Contacts are listed in the shared lanes, with our own node pinned first.
|
||||
|
||||
They render in NAME/HEARD/PKTS/KEY; plain arrows only move the highlight, and
|
||||
Enter resolves the highlighted node.
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
@@ -620,9 +620,11 @@ def test_serial_port_present_assumes_up_on_enumeration_error(monkeypatch) -> Non
|
||||
|
||||
|
||||
def test_serial_port_present_platform_uart_by_path(monkeypatch) -> None:
|
||||
"""A soldered platform UART (e.g. ``/dev/ttyS1`` on the Luckfox Lyra) is invisible to
|
||||
pyserial's ``comports()`` but its ``/dev`` char-device node persists — so an existing
|
||||
``/dev`` character device reads as present, while a vanished node (a real USB unplug of
|
||||
"""A platform UART counts as present by its ``/dev`` node, not by a scan.
|
||||
|
||||
A soldered UART (``/dev/ttyS1`` on the Luckfox Lyra) is invisible to pyserial's
|
||||
``comports()``, but its char-device node persists — so an existing character
|
||||
device reads as present, while a vanished node (a real USB unplug of
|
||||
``/dev/ttyUSB*``) still reads as absent.
|
||||
"""
|
||||
pytest.importorskip("serial")
|
||||
|
||||
@@ -135,10 +135,12 @@ def test_bare_text_default_emoji_are_left_exactly_as_measured() -> None:
|
||||
|
||||
|
||||
def test_flags_measure_two_cells_in_both_authorities() -> None:
|
||||
"""A country flag is a Regional Indicator pair: prompt_toolkit's wcwidth calls it four
|
||||
cells, Rich and the terminal draw it as one two-cell glyph. Narrowing the indicators as a
|
||||
category makes every flag sum to two in both authorities — no per-country allowlist entry,
|
||||
and flags sharing an indicator (🇨🇦 / 🇨🇳) are all fixed at once.
|
||||
"""A country flag measures two cells in both width authorities.
|
||||
|
||||
A flag is a Regional Indicator pair: prompt_toolkit's wcwidth calls it four cells,
|
||||
while Rich and the terminal draw it as one two-cell glyph. Narrowing the
|
||||
indicators as a category makes every flag sum to two in both — no per-country
|
||||
allowlist entry, and flags sharing an indicator (🇨🇦 / 🇨🇳) are fixed at once.
|
||||
"""
|
||||
# Only the lone-emoji allowlist is passed; flags are handled by category, not by listing.
|
||||
cell_len = ew._make_cell_len(frozenset(_WAVE), frozenset())
|
||||
@@ -192,8 +194,10 @@ def test_calibrate_width1_narrows_the_wave_in_both_authorities(monkeypatch) -> N
|
||||
|
||||
|
||||
def test_calibrate_width2_narrows_nothing_but_still_joins_clusters(monkeypatch) -> None:
|
||||
"""On a terminal that draws emoji two wide, narrowing would itself break the border — but
|
||||
a joined sequence is over-measured at *every* width, so that one correction still lands.
|
||||
"""A width-2 terminal narrows nothing, but still gets the cluster join.
|
||||
|
||||
Narrowing would itself break the border there — but a joined sequence is
|
||||
over-measured at *every* width, so that one correction still lands.
|
||||
|
||||
prompt_toolkit sums a ZWJ sequence's codepoints wherever it runs, which no terminal draws:
|
||||
the family is one two-cell glyph on the widest terminal as surely as on the narrowest. So
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The dual-platform gallery: every screen the interactive menu can open, checked for
|
||||
width discipline under both flavours this app runs as.
|
||||
"""The dual-platform gallery.
|
||||
|
||||
Every screen the interactive menu can open, checked for width discipline under both
|
||||
flavours this app runs as.
|
||||
|
||||
Each entry below builds one full-screen :class:`~meshterm.ui.tui.screen.Screen` against
|
||||
hand-built (simulator-shaped) data — the same "fake session, real screen" approach every
|
||||
@@ -94,8 +96,10 @@ _FAR_KEY = "f2c24f54551e" + "0" * 52
|
||||
|
||||
|
||||
class _GallerySession:
|
||||
"""A minimal TuiSession stand-in every gallery screen is happy with (the same shape
|
||||
each screen's own test file rolls independently as ``_FakeSession``/``_StubSession``).
|
||||
"""A minimal TuiSession stand-in every gallery screen is happy with.
|
||||
|
||||
The same shape each screen's own test file rolls independently as ``_FakeSession``
|
||||
or ``_StubSession``.
|
||||
"""
|
||||
|
||||
def __init__(self, cols: int = 80, rows: int = 24) -> None:
|
||||
|
||||
@@ -1198,8 +1198,10 @@ def _scenario_topo() -> object:
|
||||
|
||||
|
||||
def test_scenario_path_leads_and_ends_with_us_and_the_target() -> None:
|
||||
"""The pathline draws the whole boomerang leg, not just the stored intermediate
|
||||
hops — our own end bare, since every candidate starts from us.
|
||||
"""A scenario path leads and ends with us and the target.
|
||||
|
||||
The pathline draws the whole boomerang leg, not just the stored intermediate hops,
|
||||
with our own end bare since every candidate starts from us.
|
||||
"""
|
||||
topo = _scenario_topo()
|
||||
scenario = PathScenario(
|
||||
|
||||
+20
-11
@@ -1295,9 +1295,11 @@ def test_map_tile_cache_keeps_the_absent_tile_markers() -> None:
|
||||
|
||||
|
||||
class _CapturingSession(_StubSession):
|
||||
"""Drives :func:`open_map` headless: captures the pushed screen, renders it once, then
|
||||
replays a canned key sequence — so a test can assert on where the map opened and what
|
||||
it did (or didn't) persist as the view moves.
|
||||
"""Drives :func:`open_map` headless, so a test can watch what it does.
|
||||
|
||||
Captures the pushed screen, renders it once, then replays a canned key sequence —
|
||||
which is enough to assert on where the map opened and what it did (or didn't)
|
||||
persist as the view moves.
|
||||
"""
|
||||
|
||||
def __init__(self, cols: int, rows: int, keys: tuple[str, ...] = ()) -> None:
|
||||
@@ -1487,7 +1489,9 @@ def test_map_echoes_the_find_query_in_the_body_only_where_the_footer_is_gone() -
|
||||
|
||||
|
||||
def test_map_shifted_vertical_arrows_survive_the_console_keymap(monkeypatch) -> None:
|
||||
"""Shift+↑ arrives as PgUp on the PicoCalc console; with Shift physically down it
|
||||
"""Shifted vertical arrows survive the console keymap flattening them.
|
||||
|
||||
Shift+↑ arrives as PgUp on the PicoCalc console; with Shift physically down it
|
||||
must fine-pan, not zoom — and plain PgUp (the F-lane's zoom) stays a zoom.
|
||||
"""
|
||||
from meshterm.services import modifier_watch
|
||||
@@ -1646,9 +1650,11 @@ def test_map_screen_restores_and_persists_view() -> None:
|
||||
async def test_open_map_focus_centres_on_the_node_without_clobbering_the_saved_view(
|
||||
ctx, monkeypatch
|
||||
) -> None:
|
||||
"""Opening the full map focused on a node (from its detail page) centres there, at the
|
||||
inline preview's zoom — and, being a transient peek, never overwrites the persisted
|
||||
'where you left the map' global view, even as the peek is panned and zoomed.
|
||||
"""A focused map centres on its node without clobbering the saved view.
|
||||
|
||||
Opening the full map from a node's detail page centres there at the inline
|
||||
preview's zoom and, being a transient peek, never overwrites the persisted "where
|
||||
you left the map" global view — even as the peek is panned and zoomed.
|
||||
"""
|
||||
from meshterm.core.geo import clamp_lat
|
||||
from meshterm.ui.map_render import MapMarker
|
||||
@@ -1679,8 +1685,9 @@ async def test_open_map_focus_centres_on_the_node_without_clobbering_the_saved_v
|
||||
async def test_open_map_without_focus_restores_and_persists_the_global_view(
|
||||
ctx, monkeypatch
|
||||
) -> None:
|
||||
"""With no focus the full map is unchanged: it reopens where the user left it and pans
|
||||
persist straight back to that global view.
|
||||
"""With no focus, the full map restores and persists the global view.
|
||||
|
||||
It reopens where the user left it, and pans persist straight back to that view.
|
||||
"""
|
||||
from meshterm.ui.map_render import MapMarker
|
||||
from meshterm.ui.map_screen import open_map
|
||||
@@ -1752,8 +1759,10 @@ def test_map_observation_round_trips_node_type(ctx) -> None:
|
||||
|
||||
|
||||
def test_render_map_labels_take_the_name_hue_ours_white() -> None:
|
||||
"""Node labels carry their key-derived hue; our own label is white while the
|
||||
★ glyph stays yellow; a keyless label lands on the muted grey.
|
||||
"""Map labels take the node hue, and ours takes the you white.
|
||||
|
||||
Our own label is white while the ★ glyph stays yellow, and a keyless label lands
|
||||
on the muted grey.
|
||||
"""
|
||||
from meshterm.ui.map_render import _SELF, MapMarker, render_map
|
||||
from meshterm.ui.widgets import name_rgb
|
||||
|
||||
+17
-7
@@ -98,8 +98,10 @@ def _spans(source: str) -> list[tuple[str, str]]:
|
||||
[("# Title", "brand"), ("## Section", "accent"), ("### Sub", "md.strong")],
|
||||
)
|
||||
def test_each_heading_level_takes_the_hue_its_rank_calls_for(source, style) -> None: # noqa: ANN001
|
||||
"""``#`` is the page's own name, ``##`` a section in the app's body accent, ``###``
|
||||
a sub-heading inside one.
|
||||
"""Each heading level takes the hue its rank calls for.
|
||||
|
||||
``#`` is the page's own name, ``##`` a section in the app's body accent, and
|
||||
``###`` a sub-heading inside one.
|
||||
"""
|
||||
assert _spans(source)[0][1] == style
|
||||
|
||||
@@ -122,8 +124,11 @@ def test_emphasis_inside_a_heading_is_its_qualifier() -> None:
|
||||
|
||||
|
||||
def test_the_page_frame_sits_flush_and_muted() -> None:
|
||||
"""The standfirst under the title and the colophon under the closing rule describe
|
||||
the page rather than saying anything in it, so both recede and neither is indented.
|
||||
"""The page frame sits flush and muted.
|
||||
|
||||
The standfirst under the title and the colophon under the closing rule describe
|
||||
the page rather than saying anything in it, so both recede and neither is
|
||||
indented.
|
||||
"""
|
||||
source = "# Title\n\nthe standfirst\n\n## Section\n\nbody\n\n---\n\nthe colophon\n"
|
||||
lines = _lines(source)
|
||||
@@ -177,7 +182,9 @@ def test_a_numbered_list_counts_from_its_own_start() -> None:
|
||||
|
||||
|
||||
def test_a_quote_keeps_its_rail_down_every_line_it_wraps_to() -> None:
|
||||
"""A rail that only marked the first line would make a three-line quote look like
|
||||
"""A quote keeps its rail down every line it wraps to.
|
||||
|
||||
A rail that only marked the first line would make a three-line quote look like
|
||||
one quoted line followed by two loose ones.
|
||||
"""
|
||||
lines = [line for line in _lines("> " + "word " * 20, 30) if line.strip()]
|
||||
@@ -234,8 +241,11 @@ def test_each_inline_mark_takes_its_own_style() -> None:
|
||||
|
||||
|
||||
def test_a_link_shows_where_it_goes() -> None:
|
||||
"""Nothing is clickable on a framebuffer console, so a link that reads "the repo"
|
||||
has to name the repo or it says nothing at all — scheme stripped, it is noise.
|
||||
"""A link shows where it goes.
|
||||
|
||||
Nothing is clickable on a framebuffer console, so a link that reads "the repo" has
|
||||
to name the repo or it says nothing at all — and with the scheme stripped, it is
|
||||
noise.
|
||||
"""
|
||||
text = _texts("see [the repo](https://www.github.com/example/meshterm/)\n")[0]
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@ def _arrivals() -> list[Arrival]:
|
||||
|
||||
|
||||
def test_paths_screen_renders_graph_rows_and_cursor() -> None:
|
||||
"""The quoted text, the graph endpoints, and two lines per arrival: the route you
|
||||
pick, and the reception facts hanging under it.
|
||||
"""The screen draws the quote, the graph, and two lines for every arrival.
|
||||
|
||||
The route you pick, and the reception facts hanging under it.
|
||||
"""
|
||||
screen = _screen(_arrivals())
|
||||
body = _plain(screen.render_body(76))
|
||||
@@ -91,8 +92,10 @@ def test_paths_screen_stays_quiet_when_no_path_revisits() -> None:
|
||||
|
||||
|
||||
def test_paths_screen_labels_every_relay_with_its_hash_byte() -> None:
|
||||
"""Graph relays carry their first hash byte, both paths at once; the rows carry the
|
||||
names alone — no hash repeated after one, the two tied together by the node's hue.
|
||||
"""Graph relays carry their first hash byte, both paths at once.
|
||||
|
||||
The rows carry the names alone — no hash repeated after one, the two tied
|
||||
together by the node's hue.
|
||||
"""
|
||||
screen = _screen(_arrivals())
|
||||
body = _plain(screen.render_body(76))
|
||||
@@ -105,8 +108,10 @@ def test_paths_screen_labels_every_relay_with_its_hash_byte() -> None:
|
||||
|
||||
|
||||
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 — not the bare one-byte prefix, lit, and never a hash annotated onto itself.
|
||||
"""An unnamed relay stands in its own hash, in muted grey.
|
||||
|
||||
It is drawn at the device's path-hash width — not the bare one-byte prefix, lit,
|
||||
and never a hash annotated onto itself.
|
||||
"""
|
||||
now = utcnow()
|
||||
arrivals = [
|
||||
@@ -192,9 +197,10 @@ def test_paths_screen_scrolls_the_selected_route_sideways() -> None:
|
||||
|
||||
|
||||
def test_paths_screen_cracks_the_chips_the_scroll_cuts(monkeypatch) -> None: # noqa: ANN001
|
||||
"""Where the terminal draws chips, each edge the route runs past breaks the chip off
|
||||
on a half block in its own colour rather than behind an ellipsis: the row is sliding
|
||||
over a route that continues, not shortening a word.
|
||||
"""Where the terminal draws chips, the scroll cracks them rather than ellipsizing.
|
||||
|
||||
Each edge the route runs past breaks the chip off on a half block in its own
|
||||
colour: the row is sliding over a route that continues, not shortening a word.
|
||||
"""
|
||||
monkeypatch.setattr(pathline, "powerline_enabled", lambda: True)
|
||||
now = utcnow()
|
||||
@@ -231,8 +237,11 @@ def test_paths_screen_route_runs_origin_to_us_not_relay_to_relay() -> None:
|
||||
|
||||
|
||||
def test_paths_screen_origin_is_a_star_for_us_and_a_question_for_nobody() -> None:
|
||||
"""The head is named exactly as the graph's left endpoint is: our own ``★`` on a
|
||||
message we sent, a bare ``?`` where the frame named nobody — never a guessed name.
|
||||
"""The origin is a ``★`` for us and a ``?`` for nobody.
|
||||
|
||||
The head is named exactly as the graph's left endpoint is: our own star on a
|
||||
message we sent, a bare question mark where the frame named nobody — never a
|
||||
guessed name.
|
||||
"""
|
||||
now = utcnow()
|
||||
one = [Arrival(when=now, hops=("3d63",), snr=1.0)]
|
||||
@@ -243,8 +252,10 @@ def test_paths_screen_origin_is_a_star_for_us_and_a_question_for_nobody() -> Non
|
||||
|
||||
|
||||
def test_paths_screen_cuts_unselected_rows_the_same_way(monkeypatch) -> None: # noqa: ANN001
|
||||
"""A row you are not on runs off the lane exactly as the selected one does, so it owes
|
||||
the reader the same cracked chip — it just cannot slide to read the rest.
|
||||
"""An unselected row is cut the same way the selected one is.
|
||||
|
||||
It runs off the lane exactly as the selected row does, so it owes the reader the
|
||||
same cracked chip — it just cannot slide to read the rest.
|
||||
"""
|
||||
monkeypatch.setattr(pathline, "powerline_enabled", lambda: True)
|
||||
now = utcnow()
|
||||
@@ -261,8 +272,9 @@ def test_paths_screen_cuts_unselected_rows_the_same_way(monkeypatch) -> None: #
|
||||
|
||||
|
||||
def test_paths_screen_direct_arrival_and_empty_state() -> None:
|
||||
"""A hop-less arrival draws its two endpoints and nothing between; no arrivals at all
|
||||
explain themselves.
|
||||
"""A hop-less arrival draws its two endpoints and nothing between.
|
||||
|
||||
With no arrivals at all, the screen explains itself instead.
|
||||
|
||||
The route no longer collapses to the word *direct*: with both ends on the line,
|
||||
``Alice → ★`` **is** what a direct delivery looks like, and it reads on the same rails
|
||||
@@ -393,8 +405,10 @@ def test_a_short_list_advertises_no_paging() -> None:
|
||||
|
||||
|
||||
def test_the_selection_clamps_at_both_ends() -> None:
|
||||
"""↑ on the first arrival and ↓ on the last stay put: the window follows the highlight,
|
||||
so a wrap would haul the whole list end to end instead of moving one row.
|
||||
"""↑ on the first arrival and ↓ on the last stay put.
|
||||
|
||||
The window follows the highlight, so a wrap would haul the whole list end to end
|
||||
instead of moving one row.
|
||||
"""
|
||||
screen = _screen(_many(6))
|
||||
screen.note_viewport(24)
|
||||
|
||||
@@ -22,8 +22,11 @@ def test_shift_down_reflects_the_raw_flag() -> None:
|
||||
|
||||
|
||||
def test_shift_bank_key_latches_over_the_release_flicker(monkeypatch) -> None:
|
||||
"""An F6-F10 press keeps the lane's shifted read for a grace window, even if the raw
|
||||
watcher (wrongly, per the MCU quirk) reports Shift already back up.
|
||||
"""A shift-bank key latches over the release flicker.
|
||||
|
||||
An F6-F10 press keeps the lane's shifted read for a grace window, even if the raw
|
||||
watcher reports Shift already back up — which, per the MCU quirk, it may do
|
||||
wrongly.
|
||||
"""
|
||||
_reset()
|
||||
try:
|
||||
|
||||
+67
-36
@@ -168,8 +168,10 @@ def test_tab_strip_labels_hold_position_across_the_active_index() -> None:
|
||||
|
||||
|
||||
def test_tab_strip_rule_fills_out_to_the_render_width() -> None:
|
||||
"""The bottom rule extends to the full render width; unlike the tabs, it isn't indented
|
||||
by the left margin — that margin is itself part of the one continuous line.
|
||||
"""The tab strip's bottom rule runs the full render width.
|
||||
|
||||
Unlike the tabs, it isn't indented by the left margin — that margin is itself part
|
||||
of the one continuous line.
|
||||
"""
|
||||
top, _, bot = tab_strip(["Map", "Routes"], 1, width=25).renderables
|
||||
assert top.plain.startswith(" ╭") # the tab boxes sit indented...
|
||||
@@ -193,8 +195,10 @@ def test_tab_strip_collapses_a_lone_tab_to_a_plain_heading() -> None:
|
||||
|
||||
|
||||
def test_route_line_names_its_hops() -> None:
|
||||
"""The pathline runs contact → relays → us, every hop it can place by *name* — the
|
||||
Message paths reading; the tag trails on the context line.
|
||||
"""A route line names every hop it can place.
|
||||
|
||||
The pathline runs contact → relays → us, which is the Message paths reading; the
|
||||
tag trails on the context line.
|
||||
"""
|
||||
path, context = _route_line(
|
||||
"Far", FAR.public_key, ("3d63c6429436",), "device", None, 0,
|
||||
@@ -213,8 +217,10 @@ def test_route_line_names_its_hops() -> None:
|
||||
|
||||
|
||||
def test_route_line_greys_hops_nobody_can_name() -> None:
|
||||
"""A named hop takes its key-derived hue; a hop nobody can name — and a page node with no
|
||||
name — stands in its hash in the unknown-node grey, like the graph.
|
||||
"""A hop nobody can name stands in its hash, in the unknown-node grey.
|
||||
|
||||
A named hop takes its key-derived hue instead; a page node with no name reads the
|
||||
same as any other unnamed one, exactly as the graph draws it.
|
||||
"""
|
||||
from meshterm.ui.theme import node_style
|
||||
|
||||
@@ -232,8 +238,10 @@ def test_route_line_greys_hops_nobody_can_name() -> None:
|
||||
|
||||
|
||||
def test_route_line_hash_width_follows_our_path_hash_mode() -> None:
|
||||
"""Only the hops with no name to show spend hash cells — and those at the device's own
|
||||
path-hash width, three bytes here rather than the 1-byte default.
|
||||
"""A hash lane is as wide as the device's own path-hash mode says.
|
||||
|
||||
Only the hops with no name to show spend hash cells, and they spend them at that
|
||||
width — three bytes here, rather than the 1-byte default.
|
||||
"""
|
||||
path, _context = _route_line(
|
||||
"f2c24f54551e", FAR.public_key, ("3d63c6429436", "abcd1234ef56"), "", None, 0,
|
||||
@@ -332,9 +340,11 @@ def test_routes_view_puts_the_contact_on_the_left_and_us_on_the_right() -> None:
|
||||
|
||||
|
||||
def test_routes_view_tags_every_relay_with_its_hash_byte_alone() -> None:
|
||||
"""Only the two endpoints are named in the graph — a relay wears its first hash byte
|
||||
whether or not a contact can name it, so a label never outgrows two cells and spills
|
||||
across the lanes it sits between (the Message paths graph's rule exactly).
|
||||
"""In the graph, a relay wears its first hash byte and nothing else.
|
||||
|
||||
Only the two endpoints are named, whether or not a contact could name the relay, so
|
||||
a label never outgrows two cells and spills across the lanes it sits between — the
|
||||
Message paths graph's rule exactly.
|
||||
"""
|
||||
topo = _topo_with_route()
|
||||
view = _view(topo, topo.suggested("f2c24f54551e"), ("3d63c6429436",),
|
||||
@@ -408,8 +418,9 @@ def _topo_two_alternatives(): # noqa: ANN202
|
||||
|
||||
|
||||
def test_good_alternatives_keeps_observed_routes_and_drops_outliers() -> None:
|
||||
"""The grey alternatives are the observed routes worth trusting — outliers and the bare
|
||||
direct/device families don't earn a lane.
|
||||
"""The grey alternatives are the observed routes worth trusting.
|
||||
|
||||
Outliers and the bare direct/device families don't earn a lane.
|
||||
"""
|
||||
from meshterm.ui.node_detail_screen import _good_alternatives
|
||||
|
||||
@@ -445,8 +456,10 @@ def test_route_freshness_drops_a_route_with_a_long_quiet_hop() -> None:
|
||||
|
||||
|
||||
def test_contract_bidir_clusters_folds_a_knot_but_keeps_the_rows() -> None:
|
||||
"""A 3+ bidirectional knot contracts to one super-node in the graph draw, while each route
|
||||
row and its trace spec keep every member named in order.
|
||||
"""A 3+ bidirectional knot contracts to one super-node in the graph.
|
||||
|
||||
The rows are untouched: each route row and its trace spec keep every member named,
|
||||
in order.
|
||||
"""
|
||||
from meshterm.ui.node_detail_screen import _contract_bidir_clusters
|
||||
|
||||
@@ -476,7 +489,9 @@ def test_contract_leaves_a_two_node_pair_and_unclustered_routes_alone() -> None:
|
||||
|
||||
|
||||
def test_contract_labels_a_mixed_cluster_generically() -> None:
|
||||
"""A knot whose members are different node types can't wear one type mark — it reads
|
||||
"""A mixed-type knot is labelled generically.
|
||||
|
||||
Its members are different node types, so it can't wear one type mark: it reads
|
||||
``n nodes`` under the plain dot.
|
||||
"""
|
||||
from meshterm.ui.node_detail_screen import _contract_bidir_clusters
|
||||
@@ -662,8 +677,10 @@ def test_node_detail_screen_route_selection_arms_the_trace() -> None:
|
||||
|
||||
|
||||
def test_node_detail_routes_stage_spends_the_caption_and_top_air_on_the_fan() -> None:
|
||||
"""On the PicoCalc the fan opens directly under the tab strip and runs uncaptioned, and
|
||||
both reclaimed rows go back into the graph — never into a taller gap (JP, 2026-08-09).
|
||||
"""On the PicoCalc the Routes stage spends the caption and the top air on the fan.
|
||||
|
||||
The fan opens directly under the tab strip and runs uncaptioned, and both reclaimed
|
||||
rows go back into the graph — never into a taller gap (JP, 2026-08-09).
|
||||
|
||||
The graph's own end padding leaves a whole empty canvas row above its topmost label; the
|
||||
stage draws one row over its ceiling and peels that row off, so the peel pays for itself.
|
||||
@@ -739,8 +756,9 @@ def test_node_detail_screen_context_hangs_under_the_pathline() -> None:
|
||||
|
||||
|
||||
def test_node_detail_screen_context_line_absent_when_theres_nothing_to_show() -> None:
|
||||
"""A route with no weakest SNR, sample count, or tag draws just its pathline — no bare
|
||||
hanging line under it.
|
||||
"""A route with nothing to say draws just its pathline.
|
||||
|
||||
No weakest SNR, sample count, or tag means no bare hanging line under it.
|
||||
"""
|
||||
routes = _RoutesView(
|
||||
routes=[_Route(draw=(), spec="", path=Text("f2 aa"), context=Text(""))],
|
||||
@@ -759,8 +777,10 @@ def test_node_detail_screen_context_line_absent_when_theres_nothing_to_show() ->
|
||||
|
||||
|
||||
def test_node_detail_screen_hscrolls_the_selected_pathline() -> None:
|
||||
"""A pathline too wide for the lane scrolls with ←/→ on the highlighted row only; moving
|
||||
the cursor off it abandons the scroll, and an unselected long row just ellipsizes.
|
||||
"""A pathline too wide for the lane scrolls with ←/→, on the highlighted row only.
|
||||
|
||||
Moving the cursor off it abandons the scroll, and an unselected long row just
|
||||
ellipsizes.
|
||||
"""
|
||||
long_path = Text("f2 " + " ".join(f"{i:02x}" for i in range(40)) + " aa")
|
||||
routes = _RoutesView(
|
||||
@@ -816,10 +836,11 @@ def test_node_detail_screen_hscrolls_the_selected_pathline() -> None:
|
||||
|
||||
|
||||
def test_node_detail_route_row_cracks_a_chip_path_at_both_edges() -> None:
|
||||
"""The same window, drawn in chips: each edge the route runs past breaks the chip off
|
||||
on a half block instead of hiding it behind an ellipsis — the row is a view onto a
|
||||
route that continues, and a cracked segment says so where three dots would claim a
|
||||
shortened word.
|
||||
"""In chips, a route row cracks at both edges rather than ellipsizing.
|
||||
|
||||
Each edge the route runs past breaks the chip off on a half block: the row is a
|
||||
view onto a route that continues, and a cracked segment says so where three dots
|
||||
would claim a shortened word.
|
||||
"""
|
||||
chips = PathLine(
|
||||
[PathHop(f"NODE{i:02d}", key=f"{i:02x}aa") for i in range(12)], mode="powerline"
|
||||
@@ -905,8 +926,10 @@ def _key_row(screen: NodeDetailScreen, width: int) -> str:
|
||||
|
||||
|
||||
def test_node_detail_key_lane_hscrolls_instead_of_wrapping() -> None:
|
||||
"""The key holds one line and slides under ←/→: byte-aligned windows, faint ``…`` marks
|
||||
at whichever edge it continues past, clamped at both ends.
|
||||
"""The key lane holds one line and slides under ←/→ rather than wrapping.
|
||||
|
||||
Byte-aligned windows, faint ``…`` marks at whichever edge it continues past, and
|
||||
clamped at both ends.
|
||||
"""
|
||||
screen = _screen(info_rows=[("key", highlighted_hash(_LONG_KEY, 1)), ("heard", Text("5m ago"))])
|
||||
|
||||
@@ -932,8 +955,10 @@ def test_node_detail_key_lane_hscrolls_instead_of_wrapping() -> None:
|
||||
|
||||
|
||||
def test_node_detail_key_lane_scroll_survives_the_cursor_and_resets_on_tab() -> None:
|
||||
"""The key is pinned chrome, not a cursor row: walking the action rows leaves its scroll
|
||||
alone, while leaving the tab drops it — and a key that fits earns no ←→ at all.
|
||||
"""The key is pinned chrome, not a cursor row.
|
||||
|
||||
Walking the action rows leaves its scroll alone, while leaving the tab drops it —
|
||||
and a key that fits earns no ←→ at all.
|
||||
"""
|
||||
screen = _screen(info_rows=[("key", highlighted_hash(_LONG_KEY, 1)), ("heard", Text("5m ago"))])
|
||||
_key_row(screen, 53)
|
||||
@@ -1000,8 +1025,10 @@ def test_route_labels_light_through_a_coalesced_hop() -> None:
|
||||
|
||||
|
||||
def test_node_detail_route_list_windows_inside_the_page() -> None:
|
||||
"""With more routes than fit, the list windows with edge markers — the pinned chrome
|
||||
never leaves the screen, however many routes a busy node has.
|
||||
"""The route list windows inside the page rather than growing it.
|
||||
|
||||
With more routes than fit, edge markers appear and the pinned chrome stays on
|
||||
screen, however many routes a busy node has.
|
||||
"""
|
||||
routes = _RoutesView(
|
||||
routes=[
|
||||
@@ -1033,8 +1060,10 @@ def test_node_detail_route_list_windows_inside_the_page() -> None:
|
||||
|
||||
|
||||
def test_node_detail_route_cursor_clamps_at_both_ends() -> None:
|
||||
"""The windowed route list does not wrap: ↑ on the first row and ↓ past the last stay
|
||||
where they are, rather than hauling the window end to end.
|
||||
"""The windowed route list does not wrap.
|
||||
|
||||
↑ on the first row and ↓ past the last stay where they are, rather than hauling
|
||||
the window end to end.
|
||||
"""
|
||||
routes = _RoutesView(
|
||||
routes=[
|
||||
@@ -1059,8 +1088,10 @@ def test_node_detail_route_cursor_clamps_at_both_ends() -> None:
|
||||
|
||||
|
||||
def test_fit_blocks_walks_wrapped_rows_into_view() -> None:
|
||||
"""The variable-height fit keeps whole blocks, spends marker lines only when rows hide,
|
||||
and walks the window down to the cursor's row.
|
||||
"""The variable-height fit walks wrapped rows into view.
|
||||
|
||||
It keeps whole blocks, spends marker lines only when rows are actually hidden, and
|
||||
walks the window down to the cursor's row.
|
||||
"""
|
||||
window = ListWindow()
|
||||
top, count = window.fit_blocks([2, 2, 2, 2], 5, 3) # cursor on the last 2-line row
|
||||
|
||||
+20
-10
@@ -80,8 +80,10 @@ def test_identical_paths_collapse_to_the_top_layer() -> None:
|
||||
|
||||
|
||||
def test_emphasis_moves_the_highlight_not_the_layout() -> None:
|
||||
"""Emphasis re-colours the drawn fan on top of a fixed geometry: the same layout, painted
|
||||
in different colours, so a caller can light a different route without it reflowing.
|
||||
"""Emphasis moves the highlight, never the layout.
|
||||
|
||||
It re-colours the drawn fan on top of a fixed geometry — the same layout, painted
|
||||
differently — so a caller can light another route without it reflowing.
|
||||
"""
|
||||
# A two-route fan whose spine (priority) is pinned to route 0. Only emphasis differs
|
||||
# between the two renders — were emphasis to drive geometry (as raising priority would),
|
||||
@@ -102,8 +104,10 @@ def test_emphasis_moves_the_highlight_not_the_layout() -> None:
|
||||
|
||||
|
||||
def test_bidir_clusters_finds_knots_not_pairs() -> None:
|
||||
"""Three-plus mutually-bidirectional nodes are a cluster to contract; a tidy two-way pair
|
||||
(and any lone node) is not.
|
||||
"""Bidirectional clustering finds knots, not pairs.
|
||||
|
||||
Three or more mutually-bidirectional nodes are a cluster to contract; a tidy
|
||||
two-way pair, and any lone node, is not.
|
||||
"""
|
||||
from meshterm.ui.pathgraph import bidir_clusters
|
||||
|
||||
@@ -119,8 +123,10 @@ def test_bidir_clusters_finds_knots_not_pairs() -> None:
|
||||
|
||||
|
||||
def test_emphasis_wins_a_shared_edge_over_a_higher_priority_spine() -> None:
|
||||
"""A highlighted alternative paints its whole run — even the edge it shares with the spine —
|
||||
rather than dropping out where the higher-priority spine would otherwise own the colour.
|
||||
"""On a shared edge, emphasis beats a higher-priority spine.
|
||||
|
||||
A highlighted alternative paints its whole run, including the edge it shares,
|
||||
rather than dropping out where the spine would otherwise own the colour.
|
||||
"""
|
||||
# Both routes leave SRC through the same first relay (aa), so they share the SRC–aa edge;
|
||||
# the spine (priority 2, grey) would win that shared edge by priority, but the emphasised
|
||||
@@ -830,8 +836,10 @@ def test_the_emphasised_path_is_the_only_one_drawn_in_colour() -> None:
|
||||
|
||||
|
||||
def test_a_fan_with_nothing_emphasised_fades_nothing() -> None:
|
||||
"""No emphasis is no selection: with every layer equal there is no route to recede from,
|
||||
and the caller's colours are used exactly as given.
|
||||
"""A fan with nothing emphasised fades nothing.
|
||||
|
||||
No emphasis is no selection: with every layer equal there is no route to recede
|
||||
from, and the caller's colours are used exactly as given.
|
||||
"""
|
||||
layers = [
|
||||
PathLayer(hops=("aa",), color=WHITE, priority=2),
|
||||
@@ -856,8 +864,10 @@ def test_a_lone_path_is_never_faded() -> None:
|
||||
|
||||
|
||||
def test_the_highlight_lights_a_relay_reached_by_a_short_hash() -> None:
|
||||
"""Membership is decided in the graph's own id space, after the prefix coalesce: a route
|
||||
that names a relay ``3d`` still lights the wide marker its hop is folded into.
|
||||
"""The highlight lights a relay a route reached by a short hash.
|
||||
|
||||
Membership is decided in the graph's own id space, after the prefix coalesce, so a
|
||||
route naming a relay ``3d`` still lights the wide marker its hop folded into.
|
||||
"""
|
||||
layers = [
|
||||
PathLayer(hops=("3d63c6429436",), color=GREY, priority=2),
|
||||
|
||||
+90
-51
@@ -61,8 +61,9 @@ def _char_styles(text) -> list[tuple[str, str]]: # noqa: ANN001
|
||||
|
||||
|
||||
def test_plain_mode_matches_the_app_wide_arrow_presentation() -> None:
|
||||
"""Names in their key hue, us white, keyless muted, arrows muted — the
|
||||
``path_text`` look, reproduced exactly.
|
||||
"""Plain mode reproduces the app-wide ``path_text`` look exactly.
|
||||
|
||||
Names in their key hue, us white, keyless muted, arrows muted.
|
||||
"""
|
||||
line = PathLine(
|
||||
[
|
||||
@@ -81,7 +82,9 @@ def test_plain_mode_matches_the_app_wide_arrow_presentation() -> None:
|
||||
|
||||
|
||||
def test_plain_mode_annotation_dim_and_custom_separator() -> None:
|
||||
"""The trace ``(3d)`` note rides muted; a dim hop (and the arrow into it) fades;
|
||||
"""Annotations, dimming, and a caller's own separator all survive plain mode.
|
||||
|
||||
The trace ``(3d)`` note rides muted; a dim hop (and the arrow into it) fades;
|
||||
a surface's own separator — the mesh walk trail's ``›`` — passes straight through.
|
||||
"""
|
||||
line = PathLine(
|
||||
@@ -109,9 +112,11 @@ def test_empty_path_reads_as_the_callers_word() -> None:
|
||||
|
||||
|
||||
def test_chips_are_joined_by_one_interlocked_chevron() -> None:
|
||||
"""A seam is one cell, not two: the previous chip's point laid *on* the next chip's
|
||||
fill, so the route reads as a ribbon whose segments meet on a chevron. The closing
|
||||
edge keeps its lone taper — nothing follows it to lay the point on.
|
||||
"""A seam is one cell, not two.
|
||||
|
||||
The previous chip's point is laid *on* the next chip's fill, so the route reads as
|
||||
a ribbon whose segments meet on a chevron. The closing edge keeps its lone taper —
|
||||
nothing follows it to lay the point on.
|
||||
"""
|
||||
alice_fill = node_style("aa").split()[-1]
|
||||
line = PathLine(
|
||||
@@ -125,8 +130,9 @@ def test_chips_are_joined_by_one_interlocked_chevron() -> None:
|
||||
|
||||
|
||||
def test_chips_keep_the_same_words_and_honour_style_overrides() -> None:
|
||||
"""Chips change colours and separators, never the words; an explicit style
|
||||
override (hex or theme name) becomes the chip fill.
|
||||
"""Chips change colours and separators, never the words.
|
||||
|
||||
An explicit style override (hex or theme name) becomes the chip fill.
|
||||
"""
|
||||
hops = [PathHop("Hub", key="3d", annotation="3d"), PathHop("you", you=True)]
|
||||
plain = PathLine(hops, mode="plain").text().plain
|
||||
@@ -148,8 +154,9 @@ def test_auto_mode_follows_the_terminal_verdict(monkeypatch: pytest.MonkeyPatch)
|
||||
|
||||
|
||||
def test_the_cursor_is_a_hop_of_its_own_in_either_mode() -> None:
|
||||
"""The composer's insertion point is a slot *in* the route, not a gap between two
|
||||
hops: it renders as a hop, so chips stay chips and arrows stay arrows.
|
||||
"""The composer's insertion point is a slot *in* the route, not a gap between hops.
|
||||
|
||||
It renders as a hop, so chips stay chips and arrows stay arrows.
|
||||
"""
|
||||
hops = [PathHop("a"), PathHop(CURSOR_GLYPH, cursor=True), PathHop("b")]
|
||||
plain = PathLine(hops, mode="plain").text()
|
||||
@@ -163,8 +170,10 @@ def test_the_cursor_is_a_hop_of_its_own_in_either_mode() -> None:
|
||||
|
||||
|
||||
def test_ellipsized_elides_the_middle_and_keeps_both_endpoints() -> None:
|
||||
"""A route reads origin and destination first, so the fit drops middle hops
|
||||
behind ``⋯`` — never the tail a right-edge truncation would amputate.
|
||||
"""The fit drops middle hops behind ``⋯`` and keeps both endpoints.
|
||||
|
||||
A route reads origin and destination first, so the tail a right-edge truncation
|
||||
would amputate is exactly what must survive.
|
||||
"""
|
||||
hops = [PathHop(label) for label in ("AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF")]
|
||||
line = PathLine(hops, mode="plain")
|
||||
@@ -177,8 +186,10 @@ def test_ellipsized_elides_the_middle_and_keeps_both_endpoints() -> None:
|
||||
|
||||
|
||||
def test_ellipsized_eats_the_head_when_told_to() -> None:
|
||||
"""A breadcrumb's news is where the walk *is*, so ``ELIDE_HEAD`` spares the origin
|
||||
nothing: the fit keeps as much of the tail as the width holds, behind a ``⋯``.
|
||||
"""``ELIDE_HEAD`` spares the origin nothing and keeps the tail instead.
|
||||
|
||||
A breadcrumb's news is where the walk *is*, so the fit keeps as much of the tail
|
||||
as the width holds, behind a ``⋯``.
|
||||
"""
|
||||
hops = [PathHop(label) for label in ("AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF")]
|
||||
line = PathLine(hops, mode="plain")
|
||||
@@ -208,9 +219,10 @@ def _chips() -> PathLine:
|
||||
|
||||
|
||||
def test_cut_to_cracks_a_chip_in_its_own_fill_instead_of_ellipsizing() -> None:
|
||||
"""A chip caught by the cut breaks off on the half block, coloured by the very chip
|
||||
it shears — the crack reads as a segment that continues, where ``…`` would claim a
|
||||
word was shortened.
|
||||
"""A chip caught by the cut breaks off on a half block in its own fill.
|
||||
|
||||
The crack reads as a segment that continues, where ``…`` would claim a word was
|
||||
shortened.
|
||||
"""
|
||||
full = _chips().text()
|
||||
fitted = cut_to(full, 14)
|
||||
@@ -223,8 +235,10 @@ def test_cut_to_cracks_a_chip_in_its_own_fill_instead_of_ellipsizing() -> None:
|
||||
|
||||
|
||||
def test_cut_to_leaves_arrow_lines_on_the_ellipsis() -> None:
|
||||
"""Only chips crack: an arrow line has no fill to shear, so shortening it is exactly
|
||||
what happened and the classic mark still says so.
|
||||
"""Only chips crack; an arrow line keeps the ellipsis.
|
||||
|
||||
It has no fill to shear, so shortening is exactly what happened, and the classic
|
||||
mark still says so.
|
||||
"""
|
||||
fitted = cut_to(PathLine(_chips().hops, mode="plain").text(), 10)
|
||||
assert fitted.cell_len <= 10
|
||||
@@ -233,8 +247,9 @@ def test_cut_to_leaves_arrow_lines_on_the_ellipsis() -> None:
|
||||
|
||||
|
||||
def test_cut_mark_mirrors_itself_and_reads_the_visible_side() -> None:
|
||||
"""The head mark is the tail's mirror, and each takes the fill of the nearest cell
|
||||
still *drawn* — scanning back for a tail cut, forward for a head cut.
|
||||
"""The head mark is the tail's mirror, and each takes the nearest drawn cell's fill.
|
||||
|
||||
That means scanning back for a tail cut, and forward for a head cut.
|
||||
"""
|
||||
full = _chips().text()
|
||||
body = full.plain.index("AAAA") # inside the first chip, either way you scan
|
||||
@@ -250,8 +265,10 @@ def test_cut_mark_mirrors_itself_and_reads_the_visible_side() -> None:
|
||||
|
||||
|
||||
def test_cut_mark_falls_back_to_the_ellipsis_off_a_chip() -> None:
|
||||
"""An arrow line asks the same question and gets the muted ``…`` — the fallback is
|
||||
the whole mode test, so no caller has to know which mode drew the line.
|
||||
"""Off a chip, the same question answers with the muted ``…``.
|
||||
|
||||
The fallback is the whole mode test, so no caller has to know which mode drew the
|
||||
line.
|
||||
"""
|
||||
arrows = PathLine(_chips().hops, mode="plain").text()
|
||||
for side in (ELIDE_HEAD, ELIDE_TAIL):
|
||||
@@ -284,16 +301,20 @@ def test_the_elision_sits_between_chips_rather_than_being_one() -> None:
|
||||
|
||||
|
||||
def test_elision_hop_is_the_one_definition_both_surfaces_share() -> None:
|
||||
"""The widget and the mesh walk's trail elide with the same mark *and* the same
|
||||
gap semantics, so a head the widget hid reads as a tail the scroll hid.
|
||||
"""One elision definition serves both surfaces.
|
||||
|
||||
The widget and the mesh walk's trail elide with the same mark *and* the same gap
|
||||
semantics, so a head the widget hid reads as a tail the scroll hid.
|
||||
"""
|
||||
mark = elision_hop()
|
||||
assert (mark.label, mark.gap, mark.dim) == ("⋯", True, True)
|
||||
|
||||
|
||||
def test_wrapped_cracks_an_over_wide_lone_chip() -> None:
|
||||
"""The one place a chip line is cut mid-hop rather than folded: a hop wider than the
|
||||
content column stands alone, cracked, still inside the budget.
|
||||
"""A chip wider than the content column is cracked rather than folded.
|
||||
|
||||
It is the one place a chip line is cut mid-hop: the hop stands alone, cracked, and
|
||||
still inside the budget.
|
||||
"""
|
||||
lines = PathLine([PathHop("N" * 40, key="11aa")], mode="powerline").wrapped(20)
|
||||
assert len(lines) == 1
|
||||
@@ -302,8 +323,10 @@ def test_wrapped_cracks_an_over_wide_lone_chip() -> None:
|
||||
|
||||
|
||||
def test_wrapped_breaks_at_hops_under_a_hanging_indent() -> None:
|
||||
"""Plain lines that continue end with the ``→`` cue; continuations hang at the
|
||||
indent, stepped in by :data:`WRAP_OFFSET`; every line respects the full width.
|
||||
"""A wrapped plain path breaks at hops, under a hanging indent.
|
||||
|
||||
Lines that continue end with the ``→`` cue; continuations hang at the indent,
|
||||
stepped in by :data:`WRAP_OFFSET`; every line respects the full width.
|
||||
"""
|
||||
hops = [PathHop(label) for label in ("AAAA", "BBBB", "CCCC", "DDDD")]
|
||||
lines = PathLine(hops, mode="plain").wrapped(16, indent=2)
|
||||
@@ -396,8 +419,10 @@ def test_wrapped_lines_always_fit_their_width() -> None:
|
||||
|
||||
|
||||
def test_wrapped_continuation_cue_follows_the_separator() -> None:
|
||||
"""A comma-joined line (the trace screen's wire spec) continues on a comma, not an
|
||||
arrow — the cue is the separator's own mark, so the spec stays verbatim.
|
||||
"""The continuation cue is the separator's own mark, not always an arrow.
|
||||
|
||||
A comma-joined line (the trace screen's wire spec) continues on a comma, so the
|
||||
spec stays verbatim.
|
||||
"""
|
||||
hops = [PathHop(label) for label in ("3d63ab99", "7f21cd01", "27aa1122")]
|
||||
lines = PathLine(hops, mode="plain", separator=",").wrapped(20, indent=2)
|
||||
@@ -407,9 +432,10 @@ def test_wrapped_continuation_cue_follows_the_separator() -> None:
|
||||
|
||||
|
||||
def test_path_line_factory_matches_path_text_character_for_character() -> None:
|
||||
"""The migration bridge: the trace flavour — device endpoints with annotated
|
||||
hashes, a named hop, a prefix-lit unnamed hop, a dimmed tail — renders through
|
||||
``path_line`` exactly as ``path_text`` renders it, character and style alike.
|
||||
"""``path_line`` renders the trace flavour exactly as ``path_text`` did.
|
||||
|
||||
The migration bridge: device endpoints with annotated hashes, a named hop, a
|
||||
prefix-lit unnamed hop, and a dimmed tail, matching character and style alike.
|
||||
"""
|
||||
names = {"aa11bb": "Alice", "3d63ab": "Hub"}
|
||||
hops = [None, "aa11bb", "77ccddee", "3d63ab", None]
|
||||
@@ -423,8 +449,10 @@ def test_path_line_factory_matches_path_text_character_for_character() -> None:
|
||||
|
||||
|
||||
def test_path_line_factory_matches_path_text_hash_as_name_flavour() -> None:
|
||||
"""The message-paths flavour: an unnamed hop standing as its own muted identity
|
||||
hash, annotated with its addressed byte — same parity guarantee.
|
||||
"""The message-paths flavour matches ``path_text`` too.
|
||||
|
||||
An unnamed hop stands as its own muted identity hash, annotated with its addressed
|
||||
byte — the same parity guarantee.
|
||||
"""
|
||||
names = {"3d63abcdef00": "Hub"}
|
||||
hops = ["3d63abcdef00", "e839f2aabb11"]
|
||||
@@ -436,8 +464,10 @@ def test_path_line_factory_matches_path_text_hash_as_name_flavour() -> None:
|
||||
|
||||
|
||||
def test_path_line_factory_splices_the_cursor_slot_at_a_hop_index() -> None:
|
||||
"""``cursor`` names the position a chosen hop would take, counted over the rendered
|
||||
hops and applied after them — so the slot never shifts what its neighbours show.
|
||||
"""``cursor`` splices the insertion slot in at a hop index.
|
||||
|
||||
It names the position a chosen hop would take, counted over the rendered hops and
|
||||
applied after them, so the slot never shifts what its neighbours show.
|
||||
"""
|
||||
hops = [None, "aa11bb", "3d63ab", None]
|
||||
|
||||
@@ -452,8 +482,10 @@ def test_path_line_factory_splices_the_cursor_slot_at_a_hop_index() -> None:
|
||||
|
||||
|
||||
def test_wrapped_carries_the_cursor_like_any_other_hop() -> None:
|
||||
"""A folded route can't strand its insertion point: the slot is a hop, so it lands
|
||||
on a line of its own accord — wherever it stands, and whatever the mode.
|
||||
"""A folded route can't strand its insertion point.
|
||||
|
||||
The slot is a hop, so it lands on a line of its own accord — wherever it stands,
|
||||
and whatever the mode.
|
||||
"""
|
||||
labels = ("AAAA", "BBBB", "CCCC", "DDDD")
|
||||
for at in range(len(labels) + 1):
|
||||
@@ -467,10 +499,12 @@ def test_wrapped_carries_the_cursor_like_any_other_hop() -> None:
|
||||
|
||||
|
||||
def test_wrapped_chip_lines_open_on_the_break_they_continue() -> None:
|
||||
"""Chip wrapping never splits a chip; every line ends on the pointed edge, and
|
||||
every line *but the first* opens on the break's other half — the point notched out
|
||||
of its own fill in reverse video, so the page shows through and not a trace of the
|
||||
line above bleeds down. The point always faces the way the path flows.
|
||||
"""Chip wrapping never splits a chip; each line opens on the break it continues.
|
||||
|
||||
Every line ends on the pointed edge, and every line *but the first* opens on the
|
||||
break's other half — the point notched out of its own fill in reverse video, so the
|
||||
page shows through and not a trace of the line above bleeds down. The point always
|
||||
faces the way the path flows.
|
||||
"""
|
||||
hops = [PathHop(label, key=key) for label, key in
|
||||
(("AAAA", "aa"), ("BBBB", "77"), ("CCCC", "3d"), ("DDDD", "f2"))]
|
||||
@@ -490,7 +524,9 @@ def test_wrapped_chip_lines_open_on_the_break_they_continue() -> None:
|
||||
|
||||
|
||||
def test_rounded_caps_finish_a_path_only_where_the_font_has_them(monkeypatch) -> None: # noqa: ANN001
|
||||
"""A full Nerd Font rounds the path's two *outer* ends into a lozenge; a core-only
|
||||
"""Rounded caps finish a path only where the font actually has them.
|
||||
|
||||
A full Nerd Font rounds the path's two *outer* ends into a lozenge; a core-only
|
||||
terminal squares the opening and points the close, never drawing tofu. Interior
|
||||
breaks stay angled either way — a rounded end would read as the path stopping.
|
||||
"""
|
||||
@@ -515,8 +551,10 @@ def test_rounded_caps_finish_a_path_only_where_the_font_has_them(monkeypatch) ->
|
||||
|
||||
|
||||
def test_a_seam_between_two_of_one_colour_falls_back_to_the_page() -> None:
|
||||
"""The interlock can't draw a chevron in its own background, so where two chips land
|
||||
on the same fill the point drops its background and the page cuts the wedge instead.
|
||||
"""Where two chips land on the same fill, the seam falls back to the bare page.
|
||||
|
||||
The interlock can't draw a chevron in its own background, so the point drops its
|
||||
background and the page cuts the wedge instead.
|
||||
|
||||
Not a rare accident, either: a mirrored return leg is a run of identically faded hops
|
||||
by construction, and a stretch of keyless hops shares one grey. Either way it stays a
|
||||
@@ -540,10 +578,11 @@ def test_a_seam_between_two_of_one_colour_falls_back_to_the_page() -> None:
|
||||
|
||||
|
||||
def test_bare_self_stands_us_on_a_star_and_fades_both_ends() -> None:
|
||||
"""``bare_self`` replaces our name and hash with the app-wide ★ at both ends, and
|
||||
both fade: setting out from us and landing back on us are fixtures of the route,
|
||||
not choices, so they wear the same automatic grey as a mirrored return leg —
|
||||
and, in chips, the same dark slate rather than the loud you white.
|
||||
"""``bare_self`` stands us on a ★ at both ends, and fades both.
|
||||
|
||||
Setting out from us and landing back on us are fixtures of the route, not choices,
|
||||
so they wear the same automatic grey as a mirrored return leg — and, in chips, the
|
||||
same dark slate rather than the loud you white.
|
||||
"""
|
||||
hops = [None, *(f"{i:02x}aa" for i in range(6)), None]
|
||||
line = path_line(hops, prefix_bytes=2, self_name="Me", show_hash=True,
|
||||
|
||||
@@ -149,8 +149,11 @@ def test_bordered_frame_is_unchanged_on_regular() -> None:
|
||||
|
||||
|
||||
def test_picocalc_header_brands_the_app_not_the_device() -> None:
|
||||
"""The header_atoms wiring, per JP's spec: picocalc shows ``MeshTerm vX`` (a soldered
|
||||
radio's port never changes, so the device segment earns nothing), regular keeps both.
|
||||
"""The PicoCalc header brands the app, not the device.
|
||||
|
||||
Per JP's spec for the ``header_atoms`` wiring: picocalc shows ``MeshTerm vX``,
|
||||
because a soldered radio's port never changes and so the device segment earns
|
||||
nothing. Regular keeps both.
|
||||
"""
|
||||
from meshterm.ui.menu import _header_segments
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The platform seam: resolution order, the active-platform singleton, and the two
|
||||
platform specs' own sanity (PicoCalc is strictly the narrower, plainer flavour).
|
||||
"""Tests for the platform seam.
|
||||
|
||||
Resolution order, the active-platform singleton, and the two platform specs' own
|
||||
sanity — PicoCalc being strictly the narrower, plainer flavour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -146,8 +146,10 @@ def test_record_graph_marks_a_repeater_relay_with_its_triangle() -> None:
|
||||
|
||||
|
||||
def test_record_graph_collapses_a_revisited_route_to_distinct_nodes() -> None:
|
||||
"""A boomerang that doubles back seats each node once — the depth graph can't stack a
|
||||
node on itself — so the revisiting route draws the same graph as its distinct one.
|
||||
"""A revisited route collapses to its distinct nodes in the record graph.
|
||||
|
||||
A boomerang that doubles back seats each node once, the depth graph being unable
|
||||
to stack a node on itself, so it draws the same graph as its distinct route does.
|
||||
"""
|
||||
looping = _dialog(_record(route=(HUB_ID, FAR_ID, HUB_ID)))
|
||||
distinct = _dialog(_record(route=(HUB_ID, FAR_ID)))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Repeater-admin tests: the settings catalog, the per-node store, the simulated
|
||||
remote CLI, and the command-line screen's readline behavior.
|
||||
"""Tests for repeater admin.
|
||||
|
||||
The settings catalog, the per-node store, the simulated remote CLI, and the
|
||||
command-line screen's readline behavior.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -130,8 +130,10 @@ async def test_message_dialog_floats_over_a_blank_backdrop_end_to_end() -> None:
|
||||
|
||||
|
||||
async def test_confirm_floats_over_an_existing_popup_end_to_end() -> None:
|
||||
"""A confirm opened over a floating detail popup renders and resolves — the render
|
||||
loop runs the multi-layer float pool for real (base + detail + confirm), the case
|
||||
"""A confirm floats over an existing popup, end to end.
|
||||
|
||||
Opened over a floating detail popup, it renders and resolves: the render loop runs
|
||||
the multi-layer float pool for real (base + detail + confirm), which is the case
|
||||
the old single-float compositor stretched the detail to full-frame.
|
||||
"""
|
||||
from prompt_toolkit.input.defaults import create_pipe_input
|
||||
|
||||
+24
-12
@@ -53,8 +53,10 @@ def test_match_recommended_knows_the_nerd_font_spellings() -> None:
|
||||
|
||||
|
||||
def test_match_recommended_orders_full_patches_before_core_families() -> None:
|
||||
"""``JetBrainsMono Nerd Font`` must hit its full entry, never the plain
|
||||
``JetBrains Mono`` core prefix — first match wins, so full entries lead.
|
||||
"""A full patched family is matched ahead of the core family it is built on.
|
||||
|
||||
``JetBrainsMono Nerd Font`` must hit its full entry, never the plain ``JetBrains
|
||||
Mono`` core prefix — first match wins, so the full entries lead.
|
||||
"""
|
||||
assert match_recommended("JetBrainsMono Nerd Font Mono").coverage == FULL
|
||||
assert match_recommended("JetBrains Mono").coverage == CORE
|
||||
@@ -141,8 +143,10 @@ def test_windows_terminal_face_reads_legacy_and_defaults_to_cascadia(tmp_path: P
|
||||
def test_vscode_face_precedence_terminal_over_editor_workspace_over_user(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""``terminal.integrated.fontFamily`` beats ``editor.fontFamily``; within a key
|
||||
the workspace file beats the user file; the value's head family is judged.
|
||||
"""VS Code's font settings are read in precedence order.
|
||||
|
||||
``terminal.integrated.fontFamily`` beats ``editor.fontFamily``, and within a key
|
||||
the workspace file beats the user file. The value's head family is the one judged.
|
||||
"""
|
||||
workspace = tmp_path / "repo"
|
||||
(workspace / ".vscode").mkdir(parents=True)
|
||||
@@ -168,8 +172,10 @@ def test_vscode_face_precedence_terminal_over_editor_workspace_over_user(
|
||||
|
||||
|
||||
def test_detect_terminal_font_ladder(tmp_path: Path) -> None:
|
||||
"""WT markers first, then VS Code, then a genuine conhost; TERM disqualifies
|
||||
the conhost probe (some other emulator is hosting the console).
|
||||
"""The terminal is identified by working down a ladder of markers.
|
||||
|
||||
Windows Terminal first, then VS Code, then a genuine conhost — and ``TERM`` being
|
||||
set disqualifies the conhost probe, since some other emulator is hosting it.
|
||||
"""
|
||||
wt = detect_terminal_font(_wt_env(tmp_path, {"profiles": {"list": []}}))
|
||||
assert wt is not None and wt.source == "windows-terminal"
|
||||
@@ -204,8 +210,10 @@ def test_powerline_support_matched_font_sets_the_level(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_powerline_support_renderer_fallback_and_honest_none(tmp_path: Path) -> None:
|
||||
"""An unmatched face on WT still earns core (the bundled-symbol fallback); the
|
||||
same face on a bare conhost is an honest ``none``.
|
||||
"""An unmatched face still earns core where the renderer can supply the glyphs.
|
||||
|
||||
On Windows Terminal that is the bundled-symbol fallback; the same face on a bare
|
||||
conhost is an honest ``none``.
|
||||
"""
|
||||
env = _wt_env(tmp_path, {"profiles": {"list": [{"guid": "{abc-123}"}]}})
|
||||
wt = _powerline_support(env) # face resolves to Cascadia Mono → no match
|
||||
@@ -215,8 +223,10 @@ def test_powerline_support_renderer_fallback_and_honest_none(tmp_path: Path) ->
|
||||
|
||||
|
||||
def test_powerline_support_kitty_ssh_and_unknown() -> None:
|
||||
"""Glyph-drawing terminals earn core by marker; ssh and strangers stay unknown
|
||||
(the font lives on glass we cannot see).
|
||||
"""A glyph-drawing terminal earns core by its marker; anything else stays unknown.
|
||||
|
||||
ssh sessions and terminals we do not recognise are unknown rather than none: the
|
||||
font lives on glass we cannot see.
|
||||
"""
|
||||
kitty = _powerline_support({"TERM": "xterm-kitty"})
|
||||
assert (kitty.level, kitty.source) == (CORE, "renderer:kitty")
|
||||
@@ -227,8 +237,10 @@ def test_powerline_support_kitty_ssh_and_unknown() -> None:
|
||||
|
||||
|
||||
def test_installed_recommended_never_raises() -> None:
|
||||
"""The machine scan is best-effort context for a future nudge screen — whatever
|
||||
the platform answers, it is a RecommendedFont or None, never an exception.
|
||||
"""Scanning the machine for installed fonts never raises.
|
||||
|
||||
It is best-effort context for a future nudge screen, so whatever the platform
|
||||
answers, the result is a RecommendedFont or None.
|
||||
"""
|
||||
result = installed_recommended()
|
||||
assert result is None or isinstance(result, RecommendedFont)
|
||||
+12
-7
@@ -122,9 +122,11 @@ def test_theme16_backgrounds_stay_in_the_dim_bank() -> None:
|
||||
|
||||
|
||||
def test_deploy_script_carries_the_archived_custom_palette() -> None:
|
||||
"""The script's **opt-in** block matches ``theme.vtrgb_lines()`` byte for byte, and
|
||||
its no-kbd OSC fallback spells the same sixteen RGB values — so the archived custom
|
||||
palette stays one environment variable away, in sync with `_VT_SLOTS_CUSTOM`.
|
||||
"""The deploy script still carries the archived custom palette, in sync.
|
||||
|
||||
The script's **opt-in** block matches ``theme.vtrgb_lines()`` byte for byte, and
|
||||
its no-kbd OSC fallback spells the same sixteen RGB values, so the archived
|
||||
palette stays one environment variable away and in step with `_VT_SLOTS_CUSTOM`.
|
||||
"""
|
||||
script = _FONT_SCRIPT.read_text(encoding="utf-8")
|
||||
assert "MESHTERM_CUSTOM_PALETTE" in script, "the opt-in gate vanished"
|
||||
@@ -334,8 +336,10 @@ def test_canvas_drops_emphasis_where_bold_means_brightness() -> None:
|
||||
|
||||
|
||||
def test_font_script_marks_and_fontset_move_together() -> None:
|
||||
"""Every codepoint the script draws or aliases is in the frozen inventory, and every
|
||||
donor it consumes is out — the two files must change in the same commit.
|
||||
"""The font script and the fontset inventory move together.
|
||||
|
||||
Every codepoint the script draws or aliases is in the frozen inventory, and every
|
||||
donor it consumes is out, so the two files must change in the same commit.
|
||||
"""
|
||||
script = _FONT_SCRIPT.read_text(encoding="utf-8")
|
||||
marks = {int(m, 16) for m in re.findall(r"^ (0x[0-9A-Fa-f]{4}): art\(", script, re.M)}
|
||||
@@ -364,8 +368,9 @@ def test_font_script_marks_and_fontset_move_together() -> None:
|
||||
|
||||
|
||||
def test_specimen_renders_clean_on_picocalc() -> None:
|
||||
"""`meshterm specimen` under picocalc obeys every contract it demonstrates:
|
||||
≤53 cells per line, 16-slot SGR only, console-font characters only.
|
||||
"""`meshterm specimen` obeys on picocalc every contract it demonstrates.
|
||||
|
||||
At most 53 cells per line, 16-slot SGR only, and console-font characters only.
|
||||
"""
|
||||
from io import StringIO
|
||||
|
||||
|
||||
+20
-10
@@ -491,9 +491,11 @@ def test_mesh_page_renders_days_rhythm_arrivals_and_ledger(tmp_path: Path) -> No
|
||||
|
||||
|
||||
def test_mesh_page_arrivals_key_lane_flexes_with_width(tmp_path: Path) -> None:
|
||||
"""The arrivals key lane fills the terminal: a resolvable full key shows more
|
||||
than the stored 12 hex, truncating on a byte boundary with an ellipsis; a
|
||||
narrow terminal floors the lane at its old fixed width.
|
||||
"""The arrivals key lane flexes with the terminal width.
|
||||
|
||||
A resolvable full key shows more than the stored 12 hex, truncating on a byte
|
||||
boundary with an ellipsis; a narrow terminal floors the lane at its old fixed
|
||||
width.
|
||||
"""
|
||||
from meshterm.ui.timemachine_screen import _PICK_HASH_W
|
||||
|
||||
@@ -606,8 +608,10 @@ def test_mesh_page_rhythm_left_edge_aligns_with_the_day_charts(tmp_path: Path) -
|
||||
|
||||
|
||||
def test_picker_row_lanes_align_under_the_header() -> None:
|
||||
"""Picker rows lane up under the header; the heard age glows hot, the hash prefix
|
||||
lights in the key's own hue, and a nameless node's placeholder stays muted.
|
||||
"""Picker row lanes align under the header.
|
||||
|
||||
The heard age glows hot, the hash prefix lights in the key's own hue, and a
|
||||
nameless node's placeholder stays muted.
|
||||
"""
|
||||
from meshterm.core.models import HeardNode
|
||||
from meshterm.ui.contactlist import ContactRow, _header, _lane
|
||||
@@ -913,7 +917,9 @@ def test_picker_resort_keeps_the_highlight_on_its_node_and_the_filter() -> None:
|
||||
|
||||
|
||||
def test_picker_name_lane_is_content_sized_and_hash_lane_flexes() -> None:
|
||||
"""Columns anchor left: the name lane hugs its content and stays put as the window
|
||||
"""The name lane is content-sized, and the hash lane takes what is left.
|
||||
|
||||
Columns anchor left: the name lane hugs its content and stays put as the window
|
||||
widens, and the freed width flows to the hash lane so more of each key shows.
|
||||
"""
|
||||
from meshterm.ui.contactlist import _GAP, _LEAD
|
||||
@@ -985,8 +991,10 @@ def test_picocalc_window_ring_stops_at_30_days() -> None:
|
||||
|
||||
|
||||
def _activity_repo(tmp_path: Path) -> Repository:
|
||||
"""A repository seeded with our own outbound life: traces (homed, timed-out, one path
|
||||
walk), channel + direct messages (some acked, one inbound), and a tx-power sample.
|
||||
"""A repository seeded with our own outbound life.
|
||||
|
||||
Traces (homed, timed-out, one path walk), channel and direct messages (some
|
||||
acked, one inbound), and a tx-power sample.
|
||||
"""
|
||||
from meshterm.core.models import (
|
||||
PATH_TRACE_TARGET,
|
||||
@@ -1134,8 +1142,10 @@ def test_picker_lists_you_first_in_the_node_block() -> None:
|
||||
|
||||
|
||||
def test_picker_hash_lane_shows_a_heard_nodes_full_key_when_a_contact_holds_it() -> None:
|
||||
"""A heard node stored as a 12-hex prefix shows its whole key when a contact carries it,
|
||||
so a wide screen reveals more than the twelve stored digits (its stored id still selects).
|
||||
"""A heard node shows its full key when a contact holds it.
|
||||
|
||||
A node stored as a 12-hex prefix reveals more than those twelve digits on a wide
|
||||
screen, and its stored id still selects it.
|
||||
"""
|
||||
from meshterm.core.models import HeardNode
|
||||
|
||||
|
||||
@@ -568,8 +568,10 @@ def test_composer_suggestions_follow_the_cursor_anchor() -> None:
|
||||
|
||||
|
||||
def test_composer_preview_stands_the_cursor_in_the_route() -> None:
|
||||
"""The insertion point is a hop of the route — the ``+`` slot the next node takes —
|
||||
and ←/→ slide it along, one position at a time.
|
||||
"""The composer preview stands the cursor in the route itself.
|
||||
|
||||
The insertion point is a hop — the ``+`` slot the next node takes — and ←/→ slide
|
||||
it along, one position at a time.
|
||||
"""
|
||||
screen = _composer(_topo(), hops=["3d63c6429436"], target=False)
|
||||
|
||||
@@ -980,8 +982,10 @@ def test_a_stub_whose_only_link_was_its_owner_takes_the_owner_out_with_it() -> N
|
||||
|
||||
|
||||
def test_composer_row_cursor_clamps_at_both_ends() -> None:
|
||||
"""The windowed suggestion list does not wrap: ↑ on the first row and ↓ past the last
|
||||
stay where they are, rather than hauling the window end to end.
|
||||
"""The windowed suggestion list does not wrap.
|
||||
|
||||
↑ on the first row and ↓ past the last stay where they are, rather than hauling
|
||||
the window end to end.
|
||||
"""
|
||||
walks = [_traced(("3d", 12.0), ("f2", -5.0), ("3d", -5.5), (None, 12.0))]
|
||||
screen = _composer(_topo(trace_paths=walks))
|
||||
|
||||
+68
-43
@@ -192,8 +192,10 @@ def test_select_callable_title_re_renders_live() -> None:
|
||||
|
||||
|
||||
def test_select_width_aware_title_fits_itself_to_the_row() -> None:
|
||||
"""A title taking the render width fits itself (a route middle-elides); the natural
|
||||
form still feeds filtering and the dialog's own width measure.
|
||||
"""A width-aware title fits itself to the row it is drawn on.
|
||||
|
||||
A route middle-elides, while the natural form still feeds filtering and the
|
||||
dialog's own width measure.
|
||||
"""
|
||||
seen: list[int] = []
|
||||
|
||||
@@ -211,10 +213,11 @@ def test_select_width_aware_title_fits_itself_to_the_row() -> None:
|
||||
|
||||
|
||||
def test_select_row_cracks_a_chip_path_and_ellipsizes_everything_else() -> None:
|
||||
"""A row too wide for the list is *cut*, not truncated: a row carrying a path line
|
||||
(a trophy walk, a probe candidate) breaks its chip off on the crack, while an
|
||||
ordinary prose row keeps the ellipsis. The row itself decides which — the list has
|
||||
no idea it is ever holding a route.
|
||||
"""A row too wide for the list is cut, not truncated.
|
||||
|
||||
A row carrying a path line (a trophy walk, a probe candidate) breaks its chip off
|
||||
on the crack, while an ordinary prose row keeps the ellipsis. The row itself
|
||||
decides which — the list has no idea it is ever holding a route.
|
||||
"""
|
||||
route = PathLine(
|
||||
[PathHop(f"NODE{i:02d}", key=f"{i:02x}aa") for i in range(8)], mode="powerline"
|
||||
@@ -228,8 +231,10 @@ def test_select_row_cracks_a_chip_path_and_ellipsizes_everything_else() -> None:
|
||||
|
||||
|
||||
def test_select_hscroll_highlight_keeps_the_natural_row() -> None:
|
||||
"""In an hscroll list the highlighted row skips self-fitting — ←→ slide the full
|
||||
line — while every other row still elides itself to the width.
|
||||
"""In an hscroll list the highlighted row keeps its natural, unfitted text.
|
||||
|
||||
←→ slide the full line there, while every other row still elides itself to the
|
||||
width.
|
||||
"""
|
||||
|
||||
def fitted(width: int) -> str:
|
||||
@@ -676,7 +681,9 @@ def test_reorder_ignores_typed_characters_including_space() -> None:
|
||||
|
||||
|
||||
def test_reorder_dialog_width_is_stable_across_states() -> None:
|
||||
"""The natural width fits the widest of rows, hints, and dirty actions — and never
|
||||
"""The reorder dialog is one width in every state it can be in.
|
||||
|
||||
The natural width fits the widest of rows, hints, and dirty actions, and never
|
||||
changes as the user grabs a row or dirties the order, so the popup doesn't resize.
|
||||
"""
|
||||
screen = ReorderScreen("order", ["🔒 alpha", "# b"])
|
||||
@@ -869,8 +876,10 @@ def test_logo_takes_the_widest_mark_that_fits_the_columns() -> None:
|
||||
|
||||
|
||||
def test_compose_startup_shows_footnote_under_logo() -> None:
|
||||
"""A footnote (e.g. a copyright) is drawn muted, immediately under the logo and
|
||||
right-aligned to the logo's right edge, so the two read as one signed block.
|
||||
"""The startup footnote sits directly under the logo.
|
||||
|
||||
A footnote (a copyright, say) is drawn muted and right-aligned to the logo's own
|
||||
right edge, so the two read as one signed block.
|
||||
"""
|
||||
screen = SelectScreen("pick", [Choice("a", 1)])
|
||||
screen.chrome = False
|
||||
@@ -911,8 +920,10 @@ _ACCENT = (129, 140, 248) # the theme's accent border, #818cf8
|
||||
|
||||
|
||||
def _find(plain: str, glyphs: frozenset[str]) -> int:
|
||||
"""Index of the first box glyph from ``glyphs`` (the render console may substitute
|
||||
rounded corners with square ones on legacy Windows, so tests match the whole family).
|
||||
"""The index of the first box glyph from ``glyphs``.
|
||||
|
||||
The render console may substitute square corners for rounded ones on legacy
|
||||
Windows, so a test matches the whole family rather than one character.
|
||||
"""
|
||||
return next(i for i, ch in enumerate(plain) if ch in glyphs)
|
||||
|
||||
@@ -1355,8 +1366,10 @@ def test_device_picker_lists_configured_tcp_profile(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_device_picker_lists_configured_serial_profile(tmp_path) -> None:
|
||||
"""A ``[profiles.*]`` serial entry (a soldered ``/dev/ttyS1``) shows up in the picker under
|
||||
its alias, ready to select — even though pyserial's scan never produces that platform port.
|
||||
"""A configured serial profile is listed in the picker under its alias.
|
||||
|
||||
A soldered ``/dev/ttyS1`` is ready to select even though pyserial's scan never
|
||||
produces that platform port.
|
||||
"""
|
||||
from meshterm.core.config import DeviceProfile
|
||||
from meshterm.core.device_store import DeviceStore
|
||||
@@ -1384,8 +1397,10 @@ def test_device_picker_lists_configured_serial_profile(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_device_picker_serial_profile_yields_to_scanned_port(tmp_path) -> None:
|
||||
"""A serial profile whose port pyserial DOES enumerate is not double-listed — the scanned
|
||||
row (with real USB metadata) wins over the bare profile.
|
||||
"""A serial profile yields to the scanned port when both name the same thing.
|
||||
|
||||
The row pyserial found wins, because it carries real USB metadata the bare profile
|
||||
does not.
|
||||
"""
|
||||
from meshterm.core.config import DeviceProfile
|
||||
from meshterm.core.device_store import DeviceStore
|
||||
@@ -2053,11 +2068,12 @@ def test_session_background_is_the_topmost_full_frame_screen() -> None:
|
||||
|
||||
|
||||
def test_dispatch_promotes_nav_actions_while_right_ctrl_is_held(monkeypatch) -> None:
|
||||
"""A bare navigation key arriving while the physical right Ctrl is down becomes its
|
||||
Ctrl chord — the rescue for layouts (Canadian Multilingual Standard) that claim right
|
||||
Ctrl as a character modifier and strip the ctrl flag from the console's arrow event.
|
||||
Actions with no Ctrl sibling pass through untouched, as does everything once the key
|
||||
is released.
|
||||
"""A bare navigation key becomes its Ctrl chord while right Ctrl is held.
|
||||
|
||||
This is the rescue for layouts (Canadian Multilingual Standard) that claim right
|
||||
Ctrl as a character modifier and strip the ctrl flag from the console's arrow
|
||||
event. Actions with no Ctrl sibling pass through untouched, as does everything
|
||||
once the key is released.
|
||||
"""
|
||||
from meshterm.ui.tui import session as session_mod
|
||||
|
||||
@@ -2082,11 +2098,12 @@ def test_dispatch_promotes_nav_actions_while_right_ctrl_is_held(monkeypatch) ->
|
||||
|
||||
|
||||
def test_dispatch_promotes_letter_chords_while_right_ctrl_is_held(monkeypatch) -> None:
|
||||
"""A bare letter typed while the physical right Ctrl is down becomes its Ctrl-letter
|
||||
chord — the same rescue as the nav keys, for the ^R/^P shortcuts a layout-claimed right
|
||||
Ctrl would otherwise strip to plain text. Only the mapped letters promote (case-folded,
|
||||
with the now-stale data dropped); other text — and everything once the key is released —
|
||||
stays text.
|
||||
"""A bare letter becomes its Ctrl-letter chord while right Ctrl is held.
|
||||
|
||||
The same rescue as the nav keys, for the ^R/^P shortcuts a layout-claimed right
|
||||
Ctrl would otherwise strip to plain text. Only the mapped letters promote
|
||||
(case-folded, with the now-stale data dropped); other text — and everything once
|
||||
the key is released — stays text.
|
||||
"""
|
||||
from meshterm.ui.tui import session as session_mod
|
||||
|
||||
@@ -2109,10 +2126,12 @@ def test_dispatch_promotes_letter_chords_while_right_ctrl_is_held(monkeypatch) -
|
||||
|
||||
|
||||
def test_right_ctrl_rescue_covers_the_sessions_own_chords(monkeypatch) -> None:
|
||||
"""The rescue is app-wide, not screen-actions-only: ^V and ^C are answered by the session
|
||||
itself, and a layout-claimed right Ctrl must reach them too — right Ctrl-V pastes the
|
||||
clipboard into a compose line rather than typing a ``v``, right Ctrl-C quits. Neither
|
||||
pseudo-action is ever forwarded to a screen.
|
||||
"""The right-Ctrl rescue reaches the session's own chords, not just screen actions.
|
||||
|
||||
^V and ^C are answered by the session itself, and a layout-claimed right Ctrl must
|
||||
reach them too — right Ctrl-V pastes the clipboard into a compose line rather than
|
||||
typing a ``v``, right Ctrl-C quits. Neither pseudo-action is ever forwarded to a
|
||||
screen.
|
||||
"""
|
||||
from meshterm.ui.tui import session as session_mod
|
||||
|
||||
@@ -2150,9 +2169,11 @@ def test_right_ctrl_rescue_covers_the_sessions_own_chords(monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_every_ctrl_letter_chord_is_bound_on_both_ctrl_keys() -> None:
|
||||
"""The chord table drives the prompt_toolkit bindings, so a chord can never be bound for
|
||||
the left Ctrl without its right-Ctrl rescue (the drift the two used to be able to develop
|
||||
when the letter map was maintained by hand).
|
||||
"""Every Ctrl-letter chord is bound on both Ctrl keys.
|
||||
|
||||
The chord table drives the prompt_toolkit bindings, so a chord can never be bound
|
||||
for the left Ctrl without its right-Ctrl rescue — the drift the two used to be
|
||||
able to develop when the letter map was maintained by hand.
|
||||
"""
|
||||
from prompt_toolkit.keys import Keys
|
||||
|
||||
@@ -2167,9 +2188,11 @@ def test_every_ctrl_letter_chord_is_bound_on_both_ctrl_keys() -> None:
|
||||
|
||||
|
||||
def test_wide_glyph_detection_flags_emoji_not_marks() -> None:
|
||||
"""The desync only ever comes from a width-2 glyph the terminal may draw narrower — an
|
||||
emoji. Node-type marks, status marks and chart braille are width-1 everywhere, so they
|
||||
must not trip the check (that they seemed to was the earlier misdiagnosis).
|
||||
"""The wide-glyph check flags emoji, and leaves the app's own marks alone.
|
||||
|
||||
The desync only ever comes from a width-2 glyph the terminal may draw narrower.
|
||||
Node-type marks, status marks and chart braille are width-1 everywhere, so they
|
||||
must not trip the check — that they seemed to was the earlier misdiagnosis.
|
||||
"""
|
||||
from meshterm.ui.tui.session import _has_wide_glyph
|
||||
|
||||
@@ -2210,12 +2233,14 @@ def _row_text(screen, row: int) -> str:
|
||||
|
||||
|
||||
def test_a_wide_glyph_frame_upgrades_to_a_full_repaint() -> None:
|
||||
"""prompt_toolkit paints differentially with a *relative* cursor — sound only while every
|
||||
glyph is one cell. A width-2 glyph the terminal draws in one cell (an emoji in a chat line)
|
||||
leaves the row's cursor model off; a later paint that skips the unchanged emoji then strands
|
||||
stale cells to its right. With no remembered frame to compare against, a composed frame
|
||||
carrying such a glyph drops pt's cached frame, upgrading the next paint to a full
|
||||
erase_down + redraw. A frame of only width-1 glyphs keeps the fast differential paint —
|
||||
"""A frame carrying a wide glyph upgrades the next paint to a full repaint.
|
||||
|
||||
prompt_toolkit paints differentially with a *relative* cursor — sound only while
|
||||
every glyph is one cell. A width-2 glyph the terminal draws in one cell (an emoji
|
||||
in a chat line) leaves the row's cursor model off; a later paint that skips the
|
||||
unchanged emoji then strands stale cells to its right. With no remembered frame to
|
||||
compare against, such a frame drops pt's cached frame and takes the erase_down +
|
||||
redraw instead. A frame of only width-1 glyphs keeps the fast differential paint —
|
||||
this holds wherever the glyph is, floating dialog or not.
|
||||
"""
|
||||
session, _remembered = _repaint_harness()
|
||||
|
||||
+8
-4
@@ -268,8 +268,10 @@ def test_walk_the_node_walked_in_from_is_not_offered_back_as_a_link() -> None:
|
||||
|
||||
|
||||
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.
|
||||
"""Walking back to an earlier node drops the loop.
|
||||
|
||||
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=[Hub, ALICE])
|
||||
hub = topo.canonical(Hub.public_key)
|
||||
@@ -980,8 +982,10 @@ def test_walk_collapsed_stand_in_wears_the_selection_white() -> None:
|
||||
|
||||
|
||||
def test_walk_link_cursor_clamps_at_both_ends() -> None:
|
||||
"""The windowed link list does not wrap: ↑ on the first row and ↓ past the last stay
|
||||
where they are, rather than hauling the window end to end.
|
||||
"""The windowed link list does not wrap.
|
||||
|
||||
↑ on the first row and ↓ past the last stay where they are, rather than hauling
|
||||
the window end to end.
|
||||
"""
|
||||
screen = WalkScreen(
|
||||
session=_FakeSession(), topo=_hub_topo(14), contacts={}, self_label="us"
|
||||
|
||||
@@ -203,8 +203,10 @@ def test_store_round_trips_node_type(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_watched_row_type_glyph_and_hued_name(tmp_path: Path) -> None:
|
||||
"""The watchlist row leads with the shared type glyph (own colour) and hues the
|
||||
name by the entry's key; silence still reads from the trailing tail.
|
||||
"""A watched row leads with its type glyph and hues the name by key.
|
||||
|
||||
The glyph keeps its own colour and the name takes the entry's key-derived hue;
|
||||
silence still reads from the trailing tail.
|
||||
"""
|
||||
from meshterm.core.watch_store import WatchedNode
|
||||
from meshterm.ui.theme import name_style
|
||||
|
||||
@@ -75,8 +75,11 @@ def test_path_text_empty_reads_as_direct() -> None:
|
||||
|
||||
|
||||
def test_path_text_greys_an_unnamed_hop_whole() -> None:
|
||||
"""An unresolvable hop is an unknown node: its hash reads ``node.unknown`` whole,
|
||||
the prefix never lit — colour marks an identified node, as in the path graph.
|
||||
"""``path_text`` greys an unnamed hop whole.
|
||||
|
||||
An unresolvable hop is an unknown node: its hash reads ``node.unknown`` end to
|
||||
end, the prefix never lit, because colour marks an identified node — the path
|
||||
graph's rule.
|
||||
"""
|
||||
from meshterm.ui.theme import node_style
|
||||
|
||||
|
||||
Reference in new issue
Block a user