Files
MeshTerm/tests/test_header.py
T
Jean-Pierre MartineauandClaude Opus 4.8 dfd7d2332d Battery gauge: a braille fuel gauge pinned to the header's right edge
Add a one-cell braille battery indicator to the status bar, shown only when
the companion reports a pack. The cell fills bottom-to-top in four tiers —
green healthy, orange at a quarter, red near empty — with the true percent
beside it; it blinks red↔dim when critically low and sweeps a filling
animation while charging. A session-long BatteryService polls the pack,
maps millivolts to a state-of-charge estimate off a single-cell LiPo curve,
and infers charging from a rising terminal voltage (firmware exposes no
charging flag). The header pins the gauge flush right and the activity
pulse cedes exactly the cells it needs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:12:43 -04:00

122 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_ACTIVITY_LEVELS, _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_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_thresholds_step_the_bar_heights() -> None:
"""The documented 1/3/8/21 shoulders map counts to 1–4 dots."""
assert _HEADER_ACTIVITY_LEVELS == (1, 3, 8, 21)
histogram = (21, 8, 3, 1) + (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)
# Flipped for display, the four newest minutes read 1,3,8,21 left→right in the
# row's final two cells: heights (1,2) then (3,4).
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