Files
MeshTerm/tests/test_scope_display.py
T
Jean-Pierre MartineauandClaude Opus 5.5 b0d89a5ece A received channel message's card names the region it was flooded into
The radio hands a message over as text, not as the frame it rode in, so a 💬 row opened from the live feed had no scope to show. Its card now finds the message's flooded copies in the packet log, the way the message paths dialog does, and reads the scope off them into a scope row. The copies found are kept per row; the region is resolved on every paint, so a name learned while the card is open names it. Direct messages get no row: their copies are encrypted end to end, and a routed one has no scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 01:42:45 -04:00

456 lines
18 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Where a flood's scope is shown: the packet viewer, the live feed, message paths, monitor.
The scope itself — the maths, the store, the history columns — is pinned in
``test_regions.py``; these tests are about the surfaces that state it, and the one rule
they all share: a flood says which region it was sent into (or that it was unscoped, or
scoped to a region nobody here has named), and a direct frame says nothing at all.
"""
from __future__ import annotations
import io
import json
import re
from datetime import timedelta
from pathlib import Path
from rich.cells import cell_len
from meshterm.core.models import ChatMessage, Observation, utcnow
from meshterm.core.region_store import RegionStore
from meshterm.core.regions import (
UNSCOPED,
Scope,
frame_scope,
region_key,
scope_body,
transport_code,
)
from meshterm.persistence.repository import Repository
from meshterm.services.message_paths import (
Arrival,
channel_arrivals,
collapse,
message_scope,
)
from meshterm.tools.monitor import _live_packets
from meshterm.ui import script
from meshterm.ui.livefeed_screen import LiveFeedScreen
from meshterm.ui.message_paths_screen import MessagePathsScreen
from meshterm.ui.packet_viewer import PacketEntry, PacketViewer
from meshterm.ui.renderers import JsonRenderer, PlainRenderer
from tests.conftest import plain as _plain
from tests.test_message_paths import SECRET, _grp_txt_raw
#: An arbitrary channel-text payload to scope (the code is over it, not over its meaning).
_PAYLOAD = bytes.fromhex("a71c2d00112233445566778899aabbccddeeff")
def _scoped(region: str, payload: bytes = _PAYLOAD, **extra) -> dict:
"""An RX-log frame's raw payload for a channel text flooded under ``region``."""
code = transport_code(region_key(region), scope_body(5, payload))
return {
"route_typename": "TC_FLOOD",
"payload_type": 5,
"payload_typename": "GRP_TXT",
"pkt_payload": payload,
"transport_code": code.to_bytes(2, "little").hex() + "0000",
**extra,
}
def _code(region: str) -> str:
"""The 4-hex code a :func:`_scoped` frame carries."""
return f"{transport_code(region_key(region), scope_body(5, _PAYLOAD)):04x}"
_FLOOD = {"route_typename": "FLOOD", "payload_typename": "GRP_TXT", "chan_hash": "a7"}
_DIRECT = {"route_typename": "DIRECT", "payload_typename": "TEXT_MSG", "dest_hash": "a1"}
def _knows(*names: str): # noqa: ANN202 - a scope reader over a fixed set of names
"""A ``scope_of`` that knows exactly ``names`` — the region store's answer, in small."""
return lambda raw: frame_scope(raw, names) if isinstance(raw, dict) else None
def _stripped(lines: list[str]) -> list[str]:
return [re.sub(r"\x1b\[[0-9;]*m", "", line) for line in lines]
def _col(line: str, needle: str) -> int:
"""The display column (cells, not characters — the class icon is two) of ``needle``."""
return cell_len(line[: line.index(needle)])
# -- the packet viewer -------------------------------------------------------------------
def _card_row(raw: dict, label: str, scope_of=None) -> str | None: # noqa: ANN001
"""The value of the viewer's ``label`` row for one frame, as plain text (``None``: absent)."""
entry = PacketEntry(when=utcnow(), kind="packet", path="3d", raw=raw)
body = _stripped(
PacketViewer([entry], 0, resolve=lambda h: "", scope_of=scope_of).render_body(80)
)
row = next((line for line in body if line.split(None, 1)[:1] == [label]), None)
return None if row is None else row.split(None, 1)[1].rstrip()
def _has_scope_row(text: str, value: str) -> bool:
"""Whether a rendered card carries a ``scope`` row reading ``value``."""
return re.search(rf"^scope\s+{re.escape(value)}\s*$", text, re.M) is not None
def test_the_viewer_names_the_region_a_flood_was_scoped_to() -> None:
"""``tc flood`` said *how* and never *where*: route says flood, its own row the region."""
assert _card_row(_scoped("harbour"), "route", _knows("harbour")) == "flood"
assert _card_row(_scoped("harbour"), "scope", _knows("harbour")) == "harbour"
def test_the_viewer_shows_an_unnamed_scopes_code_and_a_plain_flood_as_unscoped() -> None:
"""A scope no known name reproduces keeps its code; a plain flood reads unscoped."""
assert _card_row(_scoped("elsewhere"), "scope", _knows("harbour")) == (
f"unknown region · code {_code('elsewhere')}"
)
assert _card_row(_FLOOD, "scope", _knows("harbour")) == "unscoped"
def test_the_viewer_without_a_store_still_tells_scoped_from_unscoped() -> None:
"""No names at hand is less of the truth, never a wrong one: the code stands in."""
assert _card_row(_scoped("harbour"), "scope") == f"unknown region · code {_code('harbour')}"
assert _card_row(_FLOOD, "scope") == "unscoped"
def test_the_viewer_never_gives_a_direct_frame_a_scope() -> None:
"""A repeater never region-filters a direct packet: its route word alone, no scope row."""
assert _card_row(_DIRECT, "route", _knows("harbour")) == "direct"
assert _card_row(_DIRECT, "scope", _knows("harbour")) is None
tc_direct = {**_DIRECT, "route_typename": "TC_DIRECT"}
assert _card_row(tc_direct, "route") == "tc direct"
assert _card_row(tc_direct, "scope") is None
def test_the_viewer_keeps_the_scope_body_out_of_the_raw_dump(tmp_path: Path) -> None:
"""The restored HMAC input is plumbing the route row already spoke for."""
raw = _scoped("harbour")
restored = {k: v for k, v in raw.items() if k not in ("payload_type", "pkt_payload")}
restored["scope_body"] = scope_body(5, _PAYLOAD).hex()
body = _plain(
PacketViewer(
[PacketEntry(when=utcnow(), kind="packet", path="", raw=restored)],
0,
resolve=lambda h: "",
scope_of=_knows("harbour"),
).render_body(80)
)
assert _has_scope_row(body, "harbour") # a replayed frame resolves exactly as a live one
assert "scope_body" not in body
def test_the_viewer_renames_a_scope_the_moment_its_region_is_learned(tmp_path: Path) -> None:
"""The card is memoized, but not past a name arriving: the scope is in the cache key."""
store = RegionStore(tmp_path / "regions.json")
entry = PacketEntry(when=utcnow(), kind="packet", path="", raw=_scoped("harbour"))
viewer = PacketViewer([entry], 0, resolve=lambda h: "", scope_of=store.scope_of)
assert not _has_scope_row(_plain(viewer.render_body(80)), "harbour")
store.learn("harbour", "typed")
assert _has_scope_row(_plain(viewer.render_body(80)), "harbour")
# -- the live feed -----------------------------------------------------------------------
class _Session:
def invalidate(self) -> None:
pass
def _feed(*raws: dict) -> LiveFeedScreen:
"""A live feed over one packet row per frame, oldest first, knowing ``harbour``."""
screen = LiveFeedScreen(
session=_Session(),
resolve=lambda h: "",
seed=[
Observation(node="", kind="packet", snr=5.0, rssi=-90.0, observed_at=utcnow(), raw=raw)
for raw in raws
],
scope_of=_knows("harbour"),
)
screen.note_viewport(40)
return screen
def test_the_feed_carries_a_scope_lane_on_a_72_column_terminal() -> None:
"""The framed body there is 68 cells: the icon-only row holds the lane, name and all.
The lane sits between the subject and the readings; a plain flood is a muted dash and
a direct frame, which has no scope, leaves the lane blank.
"""
screen = _feed(_DIRECT, _FLOOD, _scoped("elsewhere"), _scoped("harbour"))
header, *rows = _stripped(screen.render_body(68))
lanes = header.split()
assert lanes.index("SCOPE") == lanes.index("SNR") - 1
for line in (header, *rows):
assert cell_len(line.rstrip()) <= 68
col = _col(header, "SCOPE")
assert _col(rows[0], "harbour") == col
assert _col(rows[1], f"? {_code('elsewhere')}") == col
assert _col(rows[2], " - ") + 1 == col
assert "-" not in rows[3].replace("-9", "") # the direct frame's lane is blank
assert all(row.rstrip().endswith("dBm") for row in rows)
def test_the_feed_never_collapses_the_scope_lane() -> None:
"""The scope is drawn at every width; the class label is what gives way when narrow.
Beside the subject, the lane is on screen even at the PicoCalc's 53, where it is the
readings after it that run off the edge.
"""
screen = _feed(_scoped("harbour"))
for width in (53, 68, 72, 80, 100):
header, row = _stripped(screen.render_body(width))[:2]
assert _col(header, "SCOPE") == _col(row, "harbour"), width
assert "CLASS" not in _stripped(screen.render_body(80))[0]
assert "CLASS" in _stripped(screen.render_body(100))[0]
def test_the_feed_hands_its_scope_reader_to_the_viewer() -> None:
"""Enter opens a card that names the same region the row did."""
screen = _feed(_scoped("harbour"))
opened: list = []
screen._session.run_screen = lambda viewer: opened.append(viewer) # noqa: SLF001
screen._session.run_detached = lambda coro: None # noqa: SLF001
screen.handle("enter")
assert _has_scope_row(_plain(opened[0].render_body(80)), "harbour")
# -- message paths -----------------------------------------------------------------------
def test_a_messages_scope_is_read_once_off_its_arrivals() -> None:
"""Every copy carries the sender's code, so the first flooded copy answers.
A named reading beats an unnamed one, any scope beats unscoped, and direct copies
have nothing to say.
"""
scope_of = _knows("harbour")
when = utcnow()
def arrival(raw): # noqa: ANN001, ANN202
return Arrival(when=when, hops=(), snr=None, frame=raw)
direct, flood = arrival(_DIRECT), arrival(_FLOOD)
named, unnamed = arrival(_scoped("harbour")), arrival(_scoped("elsewhere"))
assert message_scope([direct, unnamed, named], scope_of) == Scope(
"scoped", "harbour", _code("harbour")
)
assert message_scope([flood, unnamed], scope_of).state == "unknown"
assert message_scope([direct, flood], scope_of) is UNSCOPED
assert message_scope([direct], scope_of) is None
assert message_scope([arrival(None)], scope_of) is None
def test_a_stored_scoped_message_resolves_through_its_arrivals(tmp_path: Path) -> None:
"""History keeps what resolution needs, the matcher keeps the frame, collapse keeps it."""
repo = Repository(tmp_path / "test.db")
run = repo.start_run("monitor", {}, None)
now = utcnow()
wire = "Alice: hi harbour"
for i, path in enumerate(("3d63", "3d63", "3d63,a1b2")):
raw = {**_grp_txt_raw(SECRET, wire), **_scoped("harbour")}
repo.record_observation(
run,
Observation(
node=None,
kind="packet",
path=path,
snr=2.0,
observed_at=now - timedelta(seconds=5 - i),
raw=raw,
),
)
message = ChatMessage(text=wire, is_channel=True, created_at=now)
arrivals = collapse(channel_arrivals(repo, message, channel_name="#general", secret=SECRET))
assert len(arrivals) == 2
store = RegionStore(tmp_path / "regions.json")
assert message_scope(arrivals, store.scope_of).state == "unknown"
store.learn("harbour", "channel")
assert message_scope(arrivals, store.scope_of).region == "harbour"
repo.close()
def _paths_screen(scope: Scope | None) -> MessagePathsScreen:
message = ChatMessage(text="hi", is_channel=True, created_at=utcnow())
return MessagePathsScreen(
message,
[Arrival(when=utcnow(), hops=("3d",), snr=1.0)],
matched=True,
resolve=lambda h: h,
prefix_bytes=1,
self_name="Homestead",
summary="heard once",
source="Alice",
scope=scope,
)
def test_the_paths_dialog_states_the_scope_once_in_its_title() -> None:
"""A status atom on the title, in the viewer's words — and none for a direct message."""
assert _paths_screen(Scope("scoped", "harbour", "3fa1")).title == (
"Message paths · scope harbour"
)
assert (
_paths_screen(Scope("unknown", None, "3fa1")).title == "Message paths · unknown scope 3fa1"
)
assert _paths_screen(UNSCOPED).title == "Message paths · unscoped"
assert _paths_screen(None).title == "Message paths"
# …and never per arrival row: the body does not repeat it.
body = _plain(_paths_screen(Scope("scoped", "harbour", "3fa1")).render_body(72))
assert "harbour" not in body
# -- monitor -------------------------------------------------------------------------------
def _capture(renderer_cls, rows: list[dict]) -> str: # noqa: ANN001
buffer = io.StringIO()
with renderer_cls(script.console(buffer)).stream(_live_packets()) as emit:
for row in rows:
emit(row)
return buffer.getvalue()
def _record(scope: Scope | None, name: str) -> dict:
from meshterm.ui.fields import NodeRef
return {
"observed_at": utcnow(),
"node": NodeRef(name=name, hash="d4e5f6a7"),
"kind": "packet",
"snr_db": 1.0,
"rssi_dbm": -90.0,
"position": None,
"scope": scope,
"path": [],
}
def test_monitor_streams_the_scope_before_the_name() -> None:
"""A pinned SCOPE lane on the plain face, and NAME still last.
A long region overruns its lane and pushes the row right, so the one field built to be
pushed is the only one it can push.
"""
text = _capture(
PlainRenderer,
[
_record(Scope("scoped", "harbour", "3fa1"), "Alice"),
_record(Scope("unknown", None, "3fa1"), "Bob"),
_record(UNSCOPED, "Carol"),
_record(None, "Dave"),
],
)
header, *rows = text.splitlines()
assert header.split()[-2:] == ["SCOPE", "NAME"]
assert [row.split()[-2:] for row in rows] == [
["harbour", "Alice"],
["unknown", "Bob"],
["unscoped", "Carol"],
["-", "Dave"],
]
assert (
len(
{
row.index(name)
for row, name in zip(rows, "Alice Bob Carol Dave".split(), strict=True)
}
)
== 1
)
def test_monitor_json_carries_the_scope_object_or_null() -> None:
"""The document speaks the shared scope shape; a frame with none is ``null``."""
text = _capture(
JsonRenderer, [_record(Scope("scoped", "harbour", "3fa1"), "Alice"), _record(None, "Bob")]
)
first, second = (json.loads(line) for line in text.splitlines())
assert first["scope"] == {"state": "scoped", "region": "harbour", "code": "3fa1"}
assert second["scope"] is None
assert list(first)[-2:] == ["scope", "path"]
# -- the channel list --------------------------------------------------------------------
def _channel_header(scopes: dict[int, str]) -> str:
"""The Channels list's column header, with the given slots scoped."""
from meshterm.core.channel_probe import ChannelSlot
from meshterm.ui.channels import _LiveStats, _menu_items
from meshterm.ui.tui.select import SelectScreen
from tests.test_gallery import DEFAULT_PUBLIC_SECRET, _ChannelsCtx
slots = [
ChannelSlot(idx=0, name="Public", secret=DEFAULT_PUBLIC_SECRET),
ChannelSlot(idx=1, name="Ops", secret=bytes(range(16))),
]
ctx = _ChannelsCtx(muted=set(), scopes={slots[i].identity: n for i, n in scopes.items()})
title, items = _menu_items(ctx, slots, 8, _LiveStats(ctx))
return _stripped(SelectScreen(title, items).render_body(68))[0]
def test_the_channel_list_draws_scope_only_when_a_channel_has_one() -> None:
"""A column of blanks says nothing; one scoped channel brings the lane back."""
assert _channel_header({}).split() == ["CHANNEL", "UNREAD", "LAST", "MSGS", "ACTIVITY"]
assert _channel_header({1: "harbour"}).split() == [
"CHANNEL",
"SCOPE",
"UNREAD",
"LAST",
"MSGS",
"ACTIVITY",
]
# -- a decoded message's card --------------------------------------------------------------
def test_a_decoded_messages_card_reads_its_scope_off_its_logged_copies(tmp_path: Path) -> None:
"""A 💬 row carries no frame; its card finds the flooded copy in the log and names it."""
from types import SimpleNamespace
from meshterm.persistence.repository import Repository
from meshterm.ui.livefeed_screen import message_scope_reader
text = "Alice: road closed"
raw = _grp_txt_raw(SECRET, text)
payload = bytes.fromhex(raw["chan_hash"] + raw["cipher_mac"] + raw["crypted"])
code = transport_code(region_key("harbour"), scope_body(5, payload))
raw.update(
route_typename="TC_FLOOD",
payload_type=5,
pkt_payload=payload,
transport_code=code.to_bytes(2, "little").hex() + "0000",
)
repo = Repository(tmp_path / "log.db")
when = utcnow()
repo.record_observation(
repo.start_run("monitor", {}),
Observation(node=None, kind="packet", path="3d", observed_at=when, raw=raw),
)
store = RegionStore(tmp_path / "regions.json")
ctx = SimpleNamespace(repo=repo, region_store=store)
read = message_scope_reader(ctx, {1: ("#general", SECRET)})
entry = PacketEntry(when=when, kind="message", channel=1, text=text)
assert read(entry).state == "unknown" # the copy is found; its region not yet named
store.learn("harbour", "typed")
assert read(entry).region == "harbour" # named the moment the region is known
viewer = PacketViewer([entry], 0, resolve=lambda h: "", message_scope=read)
assert _has_scope_row(_plain(viewer.render_body(80)), "harbour")
other = PacketEntry(when=when, kind="message", channel=1, text="Bob: something else")
assert read(other) is None # no copy of it in the log: no scope row, no guess
assert read(PacketEntry(when=when, kind="message", channel=9, text=text)) is None
repo.close()