Files
MeshTerm/tests/test_records.py
T
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00

324 lines
12 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Unit tests for the trophy-case records: walk scoring, derivation, and persistence.
The scoring layer is pure measurement (the UI owns the radio), so everything here runs
headless: hand-built trace results, synthetic positions, and a temp repository for the
leaderboard invariants.
"""
from __future__ import annotations
from meshterm.core.models import Hop, TraceResult
from meshterm.persistence.repository import Repository
from meshterm.services.records import (
CATEGORY_BY_ID,
compute_walk_stats,
first_repeated_edge,
max_hops,
walk_from_trace,
walk_scores,
)
#: Montréal-ish positions ~1.1 km apart, so distance scores are easy to sanity-check.
SELF_POS = (45.500, -73.600)
HUB_ID, FAR_ID, LEAF_ID = "3d63c6429436", "f2c24f54551e", "27d4396a2967"
POSITIONS = {
HUB_ID: (45.510, -73.600), # ~1.11 km north of us
FAR_ID: (45.510, -73.585), # north-east of us
LEAF_ID: (45.490, -73.600), # ~1.11 km south of us
}
def _result(*snrs: float, rtt: float = 250.0, nodes=None) -> TraceResult: # noqa: ANN001
"""A successful walk reply: one hop per SNR, the last one hash-less (us)."""
hops = []
for i, snr in enumerate(snrs):
node = None if i == len(snrs) - 1 else (nodes[i] if nodes else f"{i:02x}")
hops.append(Hop(index=i, node=node, snr=snr))
return TraceResult(target="walk", success=True, hops=hops, round_trip_ms=rtt)
# --- scoring ---------------------------------------------------------------------
def test_walk_stats_measure_distance_reach_and_area() -> None:
"""The circuit us→Hub→Far→us is positioned end to end: exact km, far, and area."""
stats = compute_walk_stats(
(HUB_ID, FAR_ID),
_result(8.0, 6.0, 7.0).hops,
rtt_ms=250.0,
positions=POSITIONS,
self_pos=SELF_POS,
)
assert stats.km_complete
assert stats.km_travelled > 2.0 # up ~1.1 km, across, and back home
assert stats.far_km is not None and 1.0 < stats.far_km < 2.5
assert stats.area_km2 is not None and stats.area_km2 > 0.3
assert stats.distinct_nodes == 2 and not stats.repeats
assert stats.min_snr == 6.0
def test_walk_stats_unpositioned_segments_score_a_lower_bound() -> None:
"""A hop with no position contributes 0 km and clears the complete flag."""
positions = {HUB_ID: POSITIONS[HUB_ID]} # Far is off the map
stats = compute_walk_stats(
(HUB_ID, FAR_ID),
_result(8.0, 6.0, 7.0).hops,
rtt_ms=None,
positions=positions,
self_pos=SELF_POS,
)
assert not stats.km_complete
assert 1.0 < stats.km_travelled < 1.3 # only the positioned us→Hub leg counts
def test_longest_leg_measures_one_link_and_names_its_ends() -> None:
"""The longest single segment of the circuit, with the two nodes it spanned.
Hub sits ~1.11 km north and Leaf ~1.11 km south, so the Hub→Leaf crossing (~2.2 km)
beats both of the legs touching us — a link between two hops, neither of them ours.
"""
stats = compute_walk_stats(
(HUB_ID, LEAF_ID),
_result(8.0, 6.0, 7.0).hops,
rtt_ms=None,
positions=POSITIONS,
self_pos=SELF_POS,
)
assert stats.leg_km is not None and 2.0 < stats.leg_km < 2.5
assert stats.leg_link == (HUB_ID, LEAF_ID)
# Far point measures reach, not span: the furthest node is only half as far away.
assert stats.far_km is not None and stats.far_km < stats.leg_km
assert stats.as_dict()["leg_link"] == [HUB_ID, LEAF_ID] # JSON-shaped for storage
def test_longest_leg_names_our_own_end_as_none() -> None:
"""A leg that leaves or comes home marks our end ``None`` — the ★ the UI draws."""
stats = compute_walk_stats(
(HUB_ID,),
_result(8.0, 7.0).hops,
rtt_ms=None,
positions=POSITIONS,
self_pos=SELF_POS,
)
assert stats.leg_link == (None, HUB_ID) # us → Hub, the outbound half
def test_longest_leg_needs_two_positioned_ends() -> None:
"""No segment with both ends placed scores nothing rather than a bogus zero."""
stats = compute_walk_stats(
(HUB_ID,),
_result(8.0, 7.0).hops,
rtt_ms=None,
positions=POSITIONS,
self_pos=None,
)
assert stats.leg_km is None and stats.leg_link is None
assert "long_leg" not in walk_scores(stats)
def test_cross_category_scores_score_every_game_at_once() -> None:
"""One walk competes everywhere it can — the every-board-at-once rule."""
stats = compute_walk_stats(
(HUB_ID, FAR_ID, HUB_ID),
_result(8.0, -11.0, 7.0, 9.0).hops,
rtt_ms=100.0,
positions=POSITIONS,
self_pos=SELF_POS,
)
scores = walk_scores(stats)
assert scores["grand_tour"] == 2.0 # two distinct nodes, revisit allowed
assert "clean_trail" not in scores # Hub repeats: disqualified, not zeroed
assert scores["thin_thread"] == -11.0 # the weakest surviving link
assert scores["long_haul"] > 3.0
assert scores["far_point"] > 1.0
assert scores["long_leg"] > 0.5
def test_category_titles_are_plain_and_ids_are_stable() -> None:
"""Ids are database keys (unchanged); titles read plainly for the trophy case."""
assert CATEGORY_BY_ID["grand_tour"].title == "Most nodes"
assert CATEGORY_BY_ID["thin_thread"].title == "Weakest link"
assert CATEGORY_BY_ID["long_haul"].title == "Longest distance"
# Every id still resolves — the persisted keys never changed under the rename.
assert set(CATEGORY_BY_ID) == {
"long_haul",
"far_point",
"long_leg",
"grand_tour",
"clean_trail",
"thin_thread",
"big_loop",
}
def test_max_hops_follows_the_64_byte_path_field() -> None:
"""The width bounds the walk: 64 hops at 1 byte, 16 at 4."""
assert max_hops(1) == 64
assert max_hops(2) == 32
assert max_hops(4) == 16
# --- the no-cheat rule (a record walk must be a trail) -------------------------------
def test_first_repeated_edge_flags_only_a_same_direction_recross() -> None:
"""a→b twice breaks the trail; the boomerang's b→a return leg never does."""
assert first_repeated_edge(("a", "b", "c")) is None
# A target boomerang retraces every link backwards by design: still a trail.
assert first_repeated_edge(("a", "b", "t", "b", "a")) is None
assert first_repeated_edge(("a", "b", "a", "b")) == ("a", "b")
assert first_repeated_edge(("A ", "b", "a", "B")) == ("a", "b") # case/space folded
assert first_repeated_edge(()) is None
def test_walk_scores_disqualify_a_non_trail_from_every_board() -> None:
"""Riding a link twice the same way pumps km and hops for free — every board says no."""
stats = compute_walk_stats(
(HUB_ID, FAR_ID, HUB_ID, FAR_ID), # Hub→Far ridden twice
_result(8.0, 6.0, 7.0, 5.0, 9.0).hops,
rtt_ms=100.0,
positions=POSITIONS,
self_pos=SELF_POS,
)
assert stats.km_travelled > 0 # it would have scored…
assert walk_scores(stats) == {} # …but the arbiter disqualifies it outright
# --- deriving a walk from a trace ---------------------------------------------------
def test_walk_from_trace_derives_spec_route_and_stats() -> None:
"""A successful reply yields its re-walkable spec, canonical route, and stats."""
result = _result(8.0, 6.0, 7.0, nodes=["3d", "f2"]) # two relays, then us
canon = {"3d": HUB_ID, "f2": FAR_ID}
derived = walk_from_trace(
result,
canonical=lambda h: canon.get(h),
positions=POSITIONS,
self_pos=SELF_POS,
)
assert derived is not None
spec, route, stats = derived
assert spec == "3d,f2" # the reply hops, joined — re-walkable verbatim
assert route == (HUB_ID, FAR_ID) # canonicalized for stable display + geometry
assert stats.km_complete and stats.far_km is not None
def test_walk_from_trace_ignores_a_failure_or_a_reply_with_no_relays() -> None:
"""A failed trace, or one that only came back off us, scores nothing."""
failed = TraceResult(target="walk", success=False, hops=[])
assert walk_from_trace(failed, canonical=lambda h: h, positions={}, self_pos=None) is None
# A direct-to-us reply carries only the hash-less homecoming hop: no relay to score.
direct = TraceResult(target="walk", success=True, hops=[Hop(index=0, node=None, snr=9.0)])
assert walk_from_trace(direct, canonical=lambda h: h, positions={}, self_pos=None) is None
# --- the leaderboards ----------------------------------------------------------------
def test_leaderboard_keeps_five_dedupes_and_prunes_the_worst(tmp_path) -> None: # noqa: ANN001
"""Placement, spec dedupe, and pruning hold the top-5 invariant."""
repo = Repository(tmp_path / "t.db")
try:
for i in range(6):
repo.record_discovery(
"grand_tour",
1,
f"a{i},b{i}",
(f"a{i}", f"b{i}"),
score=float(i),
stats={},
app_version="0.1.0",
)
rows = repo.discoveries("grand_tour", width_bytes=1)
assert len(rows) == 5
assert min(r.score for r in rows) == 1.0 # the 0-score walk fell off
# Re-walking a stored spec only ever *improves* its row.
assert (
repo.record_discovery(
"grand_tour",
1,
"a5,b5",
("a5", "b5"),
score=2.0,
stats={},
app_version="0.1.0",
)
is None
)
improved = repo.record_discovery(
"grand_tour",
1,
"a5,b5",
("a5", "b5"),
score=9.0,
stats={"hop_count": 2},
app_version="0.1.1",
)
assert improved is not None
rows = repo.discoveries("grand_tour", width_bytes=1)
assert len(rows) == 5
top = max(rows, key=lambda r: r.score)
assert top.score == 9.0 and top.app_version == "0.1.1"
finally:
repo.close()
def test_leaderboard_ascends_for_thin_thread(tmp_path) -> None: # noqa: ANN001
"""Weakest link keeps the *lowest* scores: a strong link never places."""
repo = Repository(tmp_path / "t.db")
try:
for i, snr in enumerate((-2.0, -8.0, -5.0, -11.0, -3.0)):
repo.record_discovery(
"thin_thread",
1,
f"c{i}",
(f"c{i}",),
score=snr,
stats={},
app_version="0.1.0",
ascending=True,
)
strong = repo.record_discovery(
"thin_thread",
1,
"c9",
("c9",),
score=7.0,
stats={},
app_version="0.1.0",
ascending=True,
)
assert strong is None # +7 dB is a great link and a terrible record
rows = repo.discoveries("thin_thread", width_bytes=1)
assert len(rows) == 5 and max(r.score for r in rows) == -2.0
finally:
repo.close()
def test_record_deletion_by_row_category_and_wholesale(tmp_path) -> None: # noqa: ANN001
"""The three deletion grains: one record, one category, everything."""
repo = Repository(tmp_path / "t.db")
try:
for category in ("grand_tour", "long_haul"):
for width in (1, 2):
repo.record_discovery(
category,
width,
f"{category[:2]},{width}",
("x",),
score=1.0,
stats={},
app_version="0.1.0",
)
first = repo.discoveries("grand_tour", width_bytes=1)[0]
assert repo.delete_discovery(first.id)
assert not repo.delete_discovery(first.id) # already gone
assert repo.delete_discoveries("grand_tour") == 1 # the width-2 row
assert repo.discoveries("grand_tour") == []
assert repo.delete_discoveries() == 2 # long_haul at both widths
assert repo.discoveries() == []
finally:
repo.close()