Files
MeshTerm/tests/test_message_paths.py
T
Jean-Pierre MartineauandClaude Opus 5 ba6e92107d Message paths: direct frames never matched at all
The direct-message path view looked for frames of class TXT_MSG. The
meshcore library reports that class as TEXT_MSG — the spelling every
other reader uses (core.frames, the packet viewer's lane, the dashboard's
traffic keys) — so the filter matched nothing, and every direct message
reported an empty window no matter how much of the conversation the radio
had overheard. Its test fabricated rows under the same wrong name, so it
passed against a mesh that can't exist.

With the class fixed, the evidence can also be far better than "±90 s of
every direct frame we heard": a TEXT_MSG frame is encrypted, but its
addressing is not — both ends ride in the clear as one-byte key hashes,
and core.frames has been recovering and storing them all along. The
window now narrows to frames running between this conversation's two ends
in either direction, and the summary bills it as matched by address and
time. A frame whose addressing was never recovered drops out rather than
being counted: paths are evidence, and a hash that isn't there proves
nothing.

Still not a content match — a one-byte hash collides — so the view keeps
its warn-coloured summary and makes no claim it can't back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:21:29 -04:00

176 lines
7.1 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 _direct_raw(dest: str = "", src: str = "") -> dict:
"""A direct-message frame's raw payload, spelled as the meshcore library reports it."""
raw: dict = {"payload_typename": "TEXT_MSG"}
if dest:
raw["dest_hash"] = dest
if src:
raw["src_hash"] = src
return raw
def test_direct_frames_match_by_time_only(tmp_path: Path) -> None:
"""With neither end named, every direct frame in the tight window is the evidence."""
repo, run = _repo(tmp_path)
now = utcnow()
_record_frame(repo, run, when=now + timedelta(seconds=3), path="3d63", raw=_direct_raw())
_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=_direct_raw(),
)
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()
def test_direct_frames_narrow_to_the_conversation_ends(tmp_path: Path) -> None:
"""Naming both ends keeps only the frames that ran between them, either direction."""
repo, run = _repo(tmp_path)
now = utcnow()
# Us (a1…) and the peer (d4…), each way round: both are this conversation's traffic.
_record_frame(
repo, run, when=now + timedelta(seconds=1), path="3d63",
raw=_direct_raw(dest="d4", src="a1"),
)
_record_frame(
repo, run, when=now + timedelta(seconds=2), path="c0",
raw=_direct_raw(dest="a1", src="d4"),
)
# Someone else's direct message, overheard in the same window.
_record_frame(
repo, run, when=now + timedelta(seconds=3), path="3d63",
raw=_direct_raw(dest="7f", src="c0"),
)
# Addressed to us, but by a third party — not this conversation.
_record_frame(
repo, run, when=now + timedelta(seconds=4), path="",
raw=_direct_raw(dest="a1", src="7f"),
)
# Its addressing was never recovered, so it can't be shown to belong.
_record_frame(repo, run, when=now + timedelta(seconds=5), path="", raw=_direct_raw())
message = ChatMessage(text="see you at 8", peer="d4e5f6a7", created_at=now)
arrivals = direct_frames_near(
repo, message, ends=("d4e5f6a7", "a1" + "0" * 62),
)
assert [a.hops for a in arrivals] == [("3d63",), ("c0",)]
repo.close()