`ruff format .` across the repo: 153 files, no behaviour change. This is the second gate CONTRIBUTING.md promises, and until now it reported 154 files needing reformat — so a first contributor ran the command the guide told them to run and could not tell their own diff from the background. Landed on its own, with nothing else in it, because a whole-repo reformat is only safe to skip past in blame if it is provably nothing but layout. It was checked that way rather than assumed: every changed file was formatted in memory and compared by full `ast.dump()`, and 152 of 153 come out structurally identical (the one exception was a docstring opening on a quote character, fixed in the commit before this one). The string and comment token streams were diffed separately, so a quote flip or an edited comment could not hide inside a re-layout — five quote-style normalisations and sixteen implicit-concatenation joins, all value-identical, and one commented-out line gaining the space after its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite is 1888 green, gallery gate and `test_theme16` included. What actually changed, in order of how much of the diff it is: wrapped calls and signatures rejoin onto one line where they fit inside the hundred columns; the magic trailing comma expands what it is there to expand; complex slices gain their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are laid out to be read were fenced in the previous commit and are untouched here. The aligned trailing-comment column is gone from about ninety lanes that were not fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced where it sits. Every comment stays on the entry it describes, so nothing there reads wrong; it just no longer lines up. Add this commit to `.git-blame-ignore-revs`, which follows immediately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
323 lines
12 KiB
Python
323 lines
12 KiB
Python
"""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()
|