Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
467 lines
18 KiB
Python
467 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_scope_lane_sits_between_subject_and_readings() -> None:
|
|
"""A plain flood is a muted dash; a direct frame, which has no scope, leaves it blank."""
|
|
screen = _feed(_DIRECT, _FLOOD, _scoped("elsewhere"), _scoped("harbour"))
|
|
header, *rows = _stripped(screen.render_body(100))
|
|
lanes = header.split()
|
|
assert lanes.index("SUBJECT") + 1 == lanes.index("SCOPE") == lanes.index("SNR") - 1
|
|
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_a_lane() -> None:
|
|
"""The class name and the scope are drawn at every width; a narrow row scrolls instead.
|
|
|
|
Nothing gives way: at 72 columns (a 68-cell body) the readings run past the edge, and
|
|
the highlighted row's ←→ is what reaches them.
|
|
"""
|
|
screen = _feed(_scoped("harbour"))
|
|
for width in (68, 72, 80, 100):
|
|
header, row = _stripped(screen.render_body(width))[:2]
|
|
assert "CLASS" in header and "chan text" in row, width
|
|
assert _col(header, "SCOPE") == _col(row, "harbour"), width
|
|
screen._selected = 0 # noqa: SLF001
|
|
screen.render_body(68)
|
|
assert screen._hmax > 0 # noqa: SLF001 - the row is wider than the body: ←→ reaches it
|
|
|
|
|
|
def test_the_picocalc_feed_names_the_class_without_an_icon() -> None:
|
|
"""On the console the class icon is a one-glyph stand-in; the name alone says it."""
|
|
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
|
|
|
set_platform(PICOCALC)
|
|
try:
|
|
screen = _feed(_scoped("harbour"))
|
|
header, row = _stripped(screen.render_body(53))[:2]
|
|
assert header.split()[:2] == ["TIME", "CLASS"]
|
|
assert _col(header, "CLASS") == _col(row, "chan text")
|
|
finally:
|
|
set_platform(REGULAR)
|
|
|
|
|
|
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() == ["SLOT", "CHANNEL", "UNREAD", "LAST", "MSGS", "ACTIVITY"]
|
|
assert _channel_header({1: "harbour"}).split() == [
|
|
"SLOT",
|
|
"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()
|