Files
MeshTerm/tests/test_header.py
T
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.

Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.

What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.

The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.

Add this commit to `.git-blame-ignore-revs`, which follows immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 01:07:19 -04:00

144 lines
6.5 KiB
Python

"""Header tests: the one-line status bar's width-filling activity sparkline."""
from __future__ import annotations
from types import SimpleNamespace
from meshterm.ui.menu import _header
def _ctx(histogram=(), unread=0, alerts=0, battery=None) -> SimpleNamespace:
"""A minimal stand-in exposing only what the header reads (simulator path).
``battery`` is the poller's cached reading (a ``percent``/``charging`` object) or
``None`` for a companion with no pack — the default, so most tests see the old
battery-free header.
"""
return SimpleNamespace(
mock=True,
chat=SimpleNamespace(unread_total=lambda: unread),
monitor=SimpleNamespace(
activity_histogram=lambda: tuple(histogram),
activity_session_flags=lambda: (True,) * len(tuple(histogram)),
),
watchtower=SimpleNamespace(unacked_count=lambda: alerts),
battery=SimpleNamespace(reading=lambda: battery),
)
def test_header_shows_the_watchtower_badge() -> None:
"""Unacknowledged alerts appear as the triangle badge; none means no badge."""
assert "▲ 2" in _header(_ctx(alerts=2), {}, 80).plain
assert "▲" not in _header(_ctx(alerts=0), {}, 80).plain
def test_header_segments_style_each_run_on_its_own() -> None:
"""A segment's styles sit side by side, never layered one under the next.
The segments are built as their own ``Text`` objects and joined (see
:func:`~meshterm.ui.menu._header_segments`), which makes it easy to reach for a
constructor ``style=`` — but that becomes the object's *base* style and blankets
everything appended after it, so the version string would render brand-under-muted and
each badge's count would inherit its glyph's error red.
"""
header = _header(_ctx(unread=3, alerts=2), {}, 120)
covering = {
header.plain[span.start : span.end]: span.style
for span in header.spans
if span.style in ("brand", "err")
}
assert covering.get("MeshTerm") == "brand" # not "MeshTerm v1.2.3"
assert set(covering) == {"MeshTerm", "●", "▲"} # neither badge's count is swept in
def test_header_sparkline_fills_the_row_exactly() -> None:
"""Every cell the fixed segments leave is sparkline — no short rows, no overflow."""
for width in (60, 100):
header = _header(_ctx(histogram=(21,) * 360), {}, width)
assert header.cell_len == width
assert "⣿" in header.plain # maxed-out cells all the way to the edge
def test_header_puts_now_at_the_right_edge() -> None:
"""The current minute's traffic draws in the row's final cell, not its first."""
header = _header(_ctx(histogram=(21,) + (0,) * 359), {}, 80).plain
spark = [ch for ch in header if 0x2800 <= ord(ch) <= 0x28FF]
# The newest bucket is the final cell's *right* dot column (its left column is
# the minute before, silent bar the continuing baseline dot).
assert spark[-1] == chr(0x2800 | 0xB8 | 0x40)
assert all(ch == chr(0x2800 | 0x40 | 0x80) for ch in spark[:-1]) # older = flatline
def test_header_wider_terminal_shows_deeper_history() -> None:
"""A burst 90 minutes ago is off a narrow header but visible on a wide one.
One minute per dot column: 90 minutes back needs 45 cells of sparkline, so it
renders as a lit cell only when the row leaves that much room.
"""
histogram = tuple(0 if i != 90 else 21 for i in range(360))
def burst_cells(row: str) -> list[str]:
# Anything lit above the bottom dot row (bits beyond dots 7/8) is the burst;
# the baseline flatline and blank braille never set those bits.
return [ch for ch in row if 0x2800 <= ord(ch) <= 0x28FF and ord(ch) & 0x3F]
assert not burst_cells(_header(_ctx(histogram=histogram), {}, 60).plain)
assert burst_cells(_header(_ctx(histogram=histogram), {}, 120).plain)
def test_header_scales_to_the_window_peak() -> None:
"""The pulse scales relative to a peak, not a fixed threshold ladder.
A 16/12/8/4 ramp climbs one dot per step against the robust ceiling ``activity_peak``
computes for it (~14.8 here — a floored 90th percentile of the counts), so the four
newest minutes read heights 1,2,3,4 left→right in the row's final two cells.
"""
histogram = (16, 12, 8, 4) + (0,) * 356
header = _header(_ctx(histogram=histogram), {}, 80).plain
spark = [ch for ch in header if 0x2800 <= ord(ch) <= 0x28FF]
left = (0x00, 0x40, 0x44, 0x46, 0x47)
right = (0x00, 0x80, 0xA0, 0xB0, 0xB8)
assert spark[-2:] == [chr(0x2800 | left[1] | right[2]), chr(0x2800 | left[3] | right[4])]
def test_header_tight_row_drops_to_compact_separators() -> None:
"""When roomy separators would squeeze the pulse, they narrow to ' · '."""
roomy = _header(_ctx(), {}, 100).plain
assert " · " in roomy
tight = _header(_ctx(unread=12, alerts=3), {}, 60).plain
assert " · " not in tight and " · " in tight
# The compaction buys real sparkline room even on a badge-laden 60-column row.
spark = [ch for ch in tight if 0x2800 <= ord(ch) <= 0x28FF]
assert len(spark) >= 14
def test_header_survives_a_sliver_terminal() -> None:
"""When the fixed segments already overflow the width, the sparkline just stays out."""
header = _header(_ctx(histogram=(9,) * 360), {}, 10)
# No room left: no braille at all, nothing stretched past the edge.
assert all(not (0x2800 <= ord(ch) <= 0x28FF) for ch in header.plain)
def _spark(row: str) -> list[str]:
return [ch for ch in row if 0x2800 <= ord(ch) <= 0x28FF]
def test_header_pins_the_battery_gauge_to_the_right_edge() -> None:
"""A companion with a pack shows its charge in the corner; the pulse cedes the room."""
reading = SimpleNamespace(percent=87, charging=False)
header = _header(_ctx(histogram=(21,) * 360, battery=reading), {}, 80)
assert header.cell_len == 80 # still fills the row exactly, gauge included
assert header.plain.rstrip().endswith("87%") # the gauge sits flush right
# The pulse gave up real cells to make room (fewer sparkline cells than a bare row,
# even counting the gauge's own full-cell glyph).
with_batt = len(_spark(header.plain))
bare = len(_spark(_header(_ctx(histogram=(21,) * 360), {}, 80).plain))
assert with_batt < bare
def test_header_hides_the_gauge_without_a_battery() -> None:
"""No pack, no gauge — the pulse keeps the whole row it always had."""
header = _header(_ctx(histogram=(21,) * 360, battery=None), {}, 80)
assert "%" not in header.plain
assert header.cell_len == 80