Files
MeshTerm/tests/test_message_paths.py
T
Jean-Pierre MartineauandClaude Opus 4.8 56b7580e48 Chat shows a message's delivery paths on demand (^P)
The radio's RX log has recorded every frame's relay path all along;
now a chat message can be asked how it got here. ^P floats a paths
window for the picked (or latest) message: one row per logged arrival
— time, reception SNR, and the relay chain through the shared compact
path widget — so a channel broadcast that reached us three ways reads
as three rows over two distinct paths, resends labelled.

Channel messages are matched by evidence, not guesswork: overheard
GRP_TXT frames decrypt under the channel's own key (fetched from the
device when the conversation didn't carry it) and match the message by
content, tolerating the "Name: " sender-prefix convention on the wire
— our own broadcasts included, since repeaters' rebroadcasts of us are
overheard like anything else. Direct frames are ECDH-encrypted, so the
view offers the window's TXT_MSG frames matched by time alone and says
so plainly. The sender-prefix parser moves to core.channels where the
matcher, the transcript, and the dashboard feed now all share it, and
the repository gains the windowed packet_frames_between query.

Both chat kinds now share one message-pick model: ↑ picks, the pick
walks with ↑↓/PgUp/PgDn/^Home and day-jumps with ^PgUp/^PgDn, ^End
returns to compose. Enter on a pick replies in a channel (unchanged)
and opens the paths in a direct chat, each with its banner cue.

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

136 lines
5.5 KiB
Python

"""Message-paths tests: matching a chat message back to its logged arrivals.
Channel arrivals are matched by decrypting overheard GRP_TXT frames with the channel's
own key (firmware-shaped frames are built here exactly as the packet-viewer tests build
theirs); direct arrivals are matched by time alone. Everything runs against a real
temporary repository, so the window query is exercised too.
"""
from __future__ import annotations
from datetime import timedelta
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
from meshterm.core.channels import channel_hash, derive_secret
from meshterm.core.models import ChatMessage, Observation, utcnow
from meshterm.persistence.repository import Repository
from meshterm.services.message_paths import (
channel_arrivals,
direct_frames_near,
distinct_paths,
)
SECRET = derive_secret("#general")
def _grp_txt_raw(secret: bytes, text: str, *, attempt: int = 0) -> dict:
"""A firmware-shaped GRP_TXT packet payload for ``text``, encrypted under ``secret``."""
plain = (0).to_bytes(4, "little") + bytes([attempt]) + text.encode("utf-8")
plain += b"\x00" * (-len(plain) % 16)
crypted = AES.new(secret, AES.MODE_ECB).encrypt(plain)
mac = HMAC.new(secret, digestmod=SHA256)
mac.update(crypted)
return {
"payload_typename": "GRP_TXT",
"chan_hash": channel_hash(secret),
"cipher_mac": mac.digest()[:2].hex(),
"crypted": crypted.hex(),
}
def _repo(tmp_path: Path) -> tuple[Repository, int]:
repo = Repository(tmp_path / "test.db")
run_id = repo.start_run("monitor", {}, None)
return repo, run_id
def _record_frame(repo: Repository, run_id: int, *, when, path: str, raw: dict, snr=2.0):
repo.record_observation(
run_id,
Observation(node=None, kind="packet", path=path, snr=snr, observed_at=when, raw=raw),
)
def test_channel_arrivals_match_by_decrypted_content(tmp_path: Path) -> None:
"""Every overheard copy of a channel message surfaces, each with its own path."""
repo, run = _repo(tmp_path)
now = utcnow()
wire = "Alice: hi mesh"
_record_frame(repo, run, when=now - timedelta(seconds=5), path="3d63", raw=_grp_txt_raw(SECRET, wire))
_record_frame(repo, run, when=now - timedelta(seconds=3), path="3d63,a1b2", raw=_grp_txt_raw(SECRET, wire))
_record_frame(repo, run, when=now - timedelta(seconds=1), path="", raw=_grp_txt_raw(SECRET, "Alice: other"))
message = ChatMessage(text=wire, is_channel=True, created_at=now)
arrivals = channel_arrivals(repo, message, channel_name="#general", secret=SECRET)
assert [a.hops for a in arrivals] == [("3d63",), ("3d63", "a1b2")]
assert distinct_paths(arrivals) == 2
repo.close()
def test_channel_arrivals_tolerate_the_sender_prefix_on_the_wire(tmp_path: Path) -> None:
"""Our own outbound message (stored as typed) matches its prefixed on-air copies."""
repo, run = _repo(tmp_path)
now = utcnow()
_record_frame(
repo, run, when=now + timedelta(seconds=2), path="3d63",
raw=_grp_txt_raw(SECRET, "Homestead: on my way"),
)
message = ChatMessage(text="on my way", outbound=True, is_channel=True, created_at=now)
arrivals = channel_arrivals(repo, message, channel_name="#general", secret=SECRET)
assert len(arrivals) == 1 and arrivals[0].hops == ("3d63",)
repo.close()
def test_channel_arrivals_carry_the_resend_counter(tmp_path: Path) -> None:
"""A decrypted frame's resend counter rides along, telling copies from retries."""
repo, run = _repo(tmp_path)
now = utcnow()
_record_frame(repo, run, when=now, path="", raw=_grp_txt_raw(SECRET, "Alice: hi", attempt=0))
_record_frame(
repo, run, when=now + timedelta(seconds=9), path="",
raw=_grp_txt_raw(SECRET, "Alice: hi", attempt=1),
)
message = ChatMessage(text="Alice: hi", is_channel=True, created_at=now)
arrivals = channel_arrivals(repo, message, channel_name="#general", secret=SECRET)
assert [a.resend for a in arrivals] == [0, 1]
assert distinct_paths(arrivals) == 1 # both arrived direct
repo.close()
def test_channel_arrivals_ignore_frames_outside_the_window(tmp_path: Path) -> None:
"""A matching frame far outside the message's window is someone else's message."""
repo, run = _repo(tmp_path)
now = utcnow()
_record_frame(
repo, run, when=now - timedelta(hours=2), path="3d63",
raw=_grp_txt_raw(SECRET, "Alice: hi"),
)
message = ChatMessage(text="Alice: hi", is_channel=True, created_at=now)
assert channel_arrivals(repo, message, channel_name="#general", secret=SECRET) == []
repo.close()
def test_direct_frames_match_by_time_only(tmp_path: Path) -> None:
"""Direct frames are encrypted, so the window's TXT_MSG frames are the evidence."""
repo, run = _repo(tmp_path)
now = utcnow()
_record_frame(
repo, run, when=now + timedelta(seconds=3), path="3d63",
raw={"payload_typename": "TXT_MSG"},
)
_record_frame( # a channel frame in the window is not direct-message evidence
repo, run, when=now + timedelta(seconds=4), path="",
raw=_grp_txt_raw(SECRET, "Alice: hi"),
)
_record_frame( # a direct frame far outside the tight window doesn't correlate
repo, run, when=now + timedelta(minutes=10), path="",
raw={"payload_typename": "TXT_MSG"},
)
message = ChatMessage(text="see you at 8", peer="d4e5", created_at=now)
arrivals = direct_frames_near(repo, message)
assert len(arrivals) == 1 and arrivals[0].hops == ("3d63",)
repo.close()