Files
MeshTerm/tests/test_header.py
T
Jean-Pierre MartineauandClaude Opus 4.8 fa07d23adb Activity sparkline: drop the recency term that collapsed the scale
The shared scaling peak (activity_peak) was fully-lighting ~60% of the
header pulse's bars on a healthy 1–18 packets/min history. Cause: the
recency damper decayed each bucket's *count* by 0.5^(age/half-life)
before taking the 90th percentile, then that percentile scaled the
*undecayed* bars drawn against it — a unit mismatch. With a 30-min
half-life over a 6h pool, most traffic mass sits older than one
half-life, so the weighted percentile collapsed (1.14 here) and the
floor (3) became the ceiling; every minute with ≥3 packets, anywhere in
the drawn history, then saturated.

Drop recency; keep the two dampers that actually deliver the steady,
shared ceiling: an outlier-robust high percentile over a pool deeper
than is drawn (steady — the percentile barely stirs as one bucket
scrolls off), plus a floor (a lone packet stays a green nub). On the
live DB this restores peak 8, ~4–13% of bars full, heights spread 1–4.

activity_peak loses its bucket_seconds/half_life_min params; menu.py and
channels.py drop the now-unused bucket-seconds plumbing. The tall
dashboard chart was already on an honest max and is untouched.

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

124 lines
5.6 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_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