`E501` across 69 lines, wrapped rather than exempted. The prose ones — module docstrings in `menu`, `node_detail_screen`, `pathline`, `pathgraph`, `walk_screen`, `trace_runner` and `test_about` — are reflowed as paragraphs, so the block keeps its shape instead of gaining one short orphan line. Nothing they say changed. The code ones mostly gained a line break inside a call. Where a break would have read worse than a name, the sub-expression got one instead: `reach` in `topology`'s quality clamp, `plain_cells` in `pathline`, `numbering` in the trace list, `listed` in `flash.py`'s refusal, and a small `shape`/`is_strip` helper in two tests that were comparing or matching the same shape twice on one line. Four docstring summaries lost a word they were not using. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
356 lines
15 KiB
Python
356 lines
15 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,
|
|
collapse,
|
|
direct_arrivals,
|
|
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 = "", mac: str = "", route: 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
|
|
if mac:
|
|
raw["cipher_mac"] = mac
|
|
if route:
|
|
raw["route_typename"] = route
|
|
return raw
|
|
|
|
|
|
def test_direct_frames_without_a_mac_fall_back_to_address_and_time(tmp_path: Path) -> None:
|
|
"""History recorded before the MAC was kept still correlates — and says that it did.
|
|
|
|
The fallback is honest evidence, not a claim: a frame between the right pair in the
|
|
right window is only *probably* this message, and ``exact`` comes back ``False`` so the
|
|
view can bill it that way.
|
|
"""
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
_record_frame(
|
|
repo, run, when=now + timedelta(seconds=3), path="3d63",
|
|
raw=_direct_raw(dest="d4", src="a1"),
|
|
)
|
|
_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 window doesn't correlate
|
|
repo, run, when=now + timedelta(minutes=10), path="",
|
|
raw=_direct_raw(dest="d4", src="a1"),
|
|
)
|
|
message = ChatMessage(
|
|
text="see you at 8", outbound=True, peer="d4e5", created_at=now
|
|
)
|
|
arrivals, exact = direct_arrivals(
|
|
repo, message, self_key="a1" + "0" * 62, peer_key="d4e5"
|
|
)
|
|
assert [a.hops for a in arrivals] == [("3d63",)]
|
|
assert exact is False
|
|
repo.close()
|
|
|
|
|
|
def test_direct_frames_keep_only_the_direction_the_message_travelled(tmp_path: Path) -> None:
|
|
"""A send shows our outgoing frames; a received message shows the incoming ones.
|
|
|
|
Both ends' hashes sit on every frame of a conversation whichever way it went, so
|
|
matching on the pair alone put our own sends into a received message's view. That is
|
|
where the implausible one-hop rows came from (JP, 2026-09-02): our transmissions heard
|
|
coming back off the repeaters in earshot, correctly one hop, shown as if they were an
|
|
inbound route to us.
|
|
"""
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
ours = dict(dest="d4", src="a1", mac="beef") # us -> peer
|
|
theirs = dict(dest="a1", src="d4", mac="f00d") # peer -> us
|
|
_record_frame(repo, run, when=now + timedelta(seconds=1), path="3d63",
|
|
raw=_direct_raw(**ours))
|
|
_record_frame(repo, run, when=now + timedelta(seconds=2), path="c0",
|
|
raw=_direct_raw(**theirs))
|
|
# Someone else's traffic, overheard in the same window.
|
|
_record_frame(repo, run, when=now + timedelta(seconds=3), path="3d63",
|
|
raw=_direct_raw(dest="7f", src="c0", mac="dead"))
|
|
_record_frame(repo, run, when=now + timedelta(seconds=4), path="",
|
|
raw=_direct_raw(dest="a1", src="7f", mac="cafe"))
|
|
|
|
sent = ChatMessage(text="see you at 8", outbound=True, peer="d4e5f6a7", created_at=now)
|
|
got = ChatMessage(text="ok", outbound=False, peer="d4e5f6a7", created_at=now)
|
|
keys = dict(self_key="a1" + "0" * 62, peer_key="d4e5f6a7")
|
|
|
|
assert [a.hops for a in direct_arrivals(repo, sent, **keys)[0]] == [("3d63",)]
|
|
assert [a.hops for a in direct_arrivals(repo, got, **keys)[0]] == [("c0",)]
|
|
repo.close()
|
|
|
|
|
|
def test_a_shared_mac_separates_one_message_from_the_next(tmp_path: Path) -> None:
|
|
"""Two sends seconds apart keep their own frames — the MAC is the fingerprint.
|
|
|
|
This is the direct-message counterpart of the channel view's content match: the frames
|
|
cannot be read, but copies of one message carry the same MAC over its ciphertext and the
|
|
next message's carry a different one.
|
|
"""
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
# First send, retried twice and heard off two repeaters.
|
|
for offset, path in ((1, "3d63"), (1, "27d4"), (6, "3d63"), (6, "27d4")):
|
|
_record_frame(repo, run, when=now + timedelta(seconds=offset), path=path,
|
|
raw=_direct_raw(dest="d4", src="a1", mac="beef"))
|
|
# Second send, twelve seconds later, same pair and same paths.
|
|
for offset, path in ((13, "3d63"), (13, "27d4")):
|
|
_record_frame(repo, run, when=now + timedelta(seconds=offset), path=path,
|
|
raw=_direct_raw(dest="d4", src="a1", mac="f00d"))
|
|
|
|
keys = dict(self_key="a1" + "0" * 62, peer_key="d4e5f6a7")
|
|
first = ChatMessage(text="one", outbound=True, peer="d4e5f6a7", created_at=now)
|
|
second = ChatMessage(
|
|
text="two", outbound=True, peer="d4e5f6a7",
|
|
created_at=now + timedelta(seconds=12),
|
|
)
|
|
got, exact = direct_arrivals(repo, first, **keys)
|
|
assert exact is True
|
|
assert len(got) == 4, "the first send's four frames, and not the second's"
|
|
assert len(direct_arrivals(repo, second, **keys)[0]) == 2
|
|
repo.close()
|
|
|
|
|
|
def test_the_window_clamps_to_the_messages_going_the_same_way(tmp_path: Path) -> None:
|
|
"""Each direction is bounded by its own neighbours, and bounded differently.
|
|
|
|
Without a clamp a fast exchange put every message's frames in every other's view — nine
|
|
messages of one real conversation fell inside a single flat window. With the wrong
|
|
clamp, a *received* message got an empty one: its stamp is the sender's clock, so
|
|
ordering it against our own sends compares two clocks and can bound it by an edge that,
|
|
in its own clock, hasn't happened yet (JP, 2026-09-02).
|
|
"""
|
|
from meshterm.services.message_paths import _DIRECT_WINDOW, direct_window
|
|
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
sent = [now, now + timedelta(seconds=20)]
|
|
received = [now - timedelta(seconds=30), now + timedelta(seconds=50)]
|
|
for at in sent:
|
|
repo.record_chat_message(
|
|
ChatMessage(text="ours", outbound=True, peer="d4e5f6a7", created_at=at)
|
|
)
|
|
for at in received:
|
|
repo.record_chat_message(
|
|
ChatMessage(text="theirs", outbound=False, peer="d4e5f6a7", created_at=at)
|
|
)
|
|
|
|
# Our own send: our clock, so nothing of it predates it — the window opens at the
|
|
# message and runs forward to the next send, where retries actually live.
|
|
ours = ChatMessage(text="ours", outbound=True, peer="d4e5f6a7", created_at=now)
|
|
start, end = direct_window(repo, ours)
|
|
assert (now - start) < timedelta(seconds=5), "no room behind a send"
|
|
assert end == sent[1], "forward to the next send, not to the next received message"
|
|
|
|
# A received message keeps a centred window, reaching halfway to the received messages
|
|
# either side of it — and is untouched by our own sends in between.
|
|
theirs = ChatMessage(
|
|
text="theirs", outbound=False, peer="d4e5f6a7", created_at=now + timedelta(seconds=50)
|
|
)
|
|
start, end = direct_window(repo, theirs)
|
|
assert start == theirs.created_at - timedelta(seconds=40), "halfway back to the last one"
|
|
assert end == theirs.created_at + _DIRECT_WINDOW, "nothing follows, so that side stands"
|
|
repo.close()
|
|
|
|
|
|
|
|
def test_collapse_folds_a_repeated_path_into_one_counted_row(tmp_path: Path) -> None:
|
|
"""One row per path, carrying its copy count, first sighting and best SNR."""
|
|
from meshterm.services.message_paths import Arrival
|
|
|
|
base = utcnow()
|
|
arrivals = [
|
|
Arrival(when=base, hops=("3d63",), snr=10.0),
|
|
Arrival(when=base + timedelta(seconds=1), hops=("27d4",), snr=5.0),
|
|
Arrival(when=base + timedelta(seconds=5), hops=("3d63",), snr=13.5),
|
|
]
|
|
folded = collapse(arrivals)
|
|
assert [a.hops for a in folded] == [("3d63",), ("27d4",)], "first-heard order"
|
|
assert folded[0].copies == 2 and folded[1].copies == 1
|
|
assert folded[0].when == base, "the first sighting is when the path first worked"
|
|
assert folded[0].snr == 13.5, "the best reading is what the path can do"
|
|
|
|
|
|
|
|
def test_a_routed_frames_path_is_where_it_was_going_not_where_it_has_been(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The route type decides what ``path`` means, and an empty routed one means nothing.
|
|
|
|
A flooded packet accumulates its path — every relay appends itself — so the hops are the
|
|
route it travelled to us. A direct-routed packet carries a route its sender wrote and
|
|
its relays consume, so an empty path means the route was used up, not that the packet
|
|
crossed no relays. Read the second as the first and a message from five hops away is
|
|
drawn as having arrived out of thin air: the "impossible direct path" (JP, 2026-09-02).
|
|
"""
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
_record_frame( # flooded, one relay: it really did come to us that way
|
|
repo, run, when=now + timedelta(seconds=1), path="3d63",
|
|
raw=_direct_raw(dest="a1", src="d4", mac="beef", route="FLOOD"),
|
|
)
|
|
_record_frame( # direct-routed, route consumed: says nothing about how it got here
|
|
repo, run, when=now + timedelta(seconds=2), path="",
|
|
raw=_direct_raw(dest="a1", src="d4", mac="beef", route="DIRECT"),
|
|
)
|
|
message = ChatMessage(text="hi", outbound=False, peer="d4e5f6a7", created_at=now)
|
|
arrivals, _exact = direct_arrivals(
|
|
repo, message, self_key="a1" + "0" * 62, peer_key="d4e5f6a7"
|
|
)
|
|
flooded, routed = arrivals
|
|
assert flooded.routed is False and flooded.route_known is True
|
|
assert routed.routed is True and routed.route_known is False, (
|
|
"an empty routed path is not a zero-hop arrival"
|
|
)
|
|
repo.close()
|
|
|
|
|
|
def test_history_without_a_route_type_is_unknown_rather_than_assumed(tmp_path: Path) -> None:
|
|
"""Frames recorded before the route type was kept read as before, not as guesses."""
|
|
repo, run = _repo(tmp_path)
|
|
now = utcnow()
|
|
_record_frame(
|
|
repo, run, when=now + timedelta(seconds=1), path="",
|
|
raw=_direct_raw(dest="a1", src="d4", mac="beef"),
|
|
)
|
|
message = ChatMessage(text="hi", outbound=False, peer="d4e5f6a7", created_at=now)
|
|
arrivals, _exact = direct_arrivals(
|
|
repo, message, self_key="a1" + "0" * 62, peer_key="d4e5f6a7"
|
|
)
|
|
assert arrivals[0].routed is None
|
|
assert arrivals[0].route_known is True, "unknown falls back to the old reading"
|
|
repo.close()
|
|
|
|
|
|
def test_collapse_keeps_a_routed_path_apart_from_the_same_hops_flooded(tmp_path: Path) -> None:
|
|
"""Identical hashes mean two different things under the two route types."""
|
|
from meshterm.services.message_paths import Arrival
|
|
|
|
base = utcnow()
|
|
folded = collapse([
|
|
Arrival(when=base, hops=("3d63",), snr=1.0, routed=False),
|
|
Arrival(when=base + timedelta(seconds=1), hops=("3d63",), snr=2.0, routed=True),
|
|
])
|
|
assert len(folded) == 2, "a route travelled is not a route intended"
|
|
assert [a.routed for a in folded] == [False, True]
|