Files
MeshTerm/tests/test_settings_offer.py
T
Jean-Pierre MartineauandClaude Fable 5 2d18c45917 Saved settings: the position rule, proven over a scripted device
The offer flow end to end: a device reporting the 0/0 null island
(exactly what SELF_INFO carries for unset - signed microdegrees over
1e6, verified against the meshcore reader) gets its remembered fix
written back silently; a real conflicting fix still prompts; mixed
drift splits between the silent restore and the prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 00:10:53 -04:00

155 lines
6.1 KiB
Python

"""The connect-time settings offer, driven end to end over a scripted device.
The scenario that motivated the position rule (JP, 2026-08-08/09): a GPS-less companion
holds its manually-set coordinates only until it restarts, then reports the 0/0 null
island — SELF_INFO always carries ``adv_lat``/``adv_lon`` as signed microdegrees / 1e6,
so "unset" arrives as exactly ``0.0``, never as a missing key (verified against the
meshcore reader). The offer must write the remembered fix straight back (filling an
empty slot overwrites nothing) and never ask the operator to weigh a position against
no position — while a device reporting a *real*, different fix still gets the honest
prompt, and non-position drift keeps prompting alongside the silent restore.
"""
from __future__ import annotations
import asyncio
from pathlib import Path
from types import SimpleNamespace
import meshterm.ui.settings_offer as offer_mod
from meshterm.core.settings_store import SettingsStore
from meshterm.ui.settings_offer import offer_remembered_settings
_PUB = "e4a392456ead6f500725758729eb9f9decccf0ccd22d1d7723131e973eeb12a4"
#: The coordinates JP's store really held when this flow was diagnosed — exact
#: microdegree quotients, so they round-trip the wire encoding byte-for-byte.
_LAT, _LON = 45.535456, -73.708698
class _ScriptedDevice:
"""A companion that answers SELF_INFO from a dict and records coordinate writes."""
def __init__(self, *, adv_lat: float, adv_lon: float, name: str = "Homestead-Pico") -> None:
self._info = {
"public_key": _PUB,
"name": name,
"adv_lat": adv_lat,
"adv_lon": adv_lon,
}
self.coords_writes: list[tuple[float, float]] = []
async def get_self_info(self) -> dict:
return dict(self._info)
async def set_coords(self, lat: float, lon: float) -> None:
self.coords_writes.append((lat, lon))
self._info["adv_lat"], self._info["adv_lon"] = lat, lon
async def set_name(self, name: str) -> None:
self._info["name"] = name
# The optional snapshot reads a bare bridge doesn't support — build_snapshot skips them.
async def get_tuning(self) -> dict:
raise RuntimeError("unsupported")
async def get_path_hash_mode(self) -> int:
raise RuntimeError("unsupported")
async def get_autoadd_config(self) -> int:
raise RuntimeError("unsupported")
async def get_default_flood_scope(self) -> dict:
raise RuntimeError("unsupported")
def _ctx(store: SettingsStore, device: _ScriptedDevice) -> SimpleNamespace:
"""The slice of AppContext the offer reads, over the scripted device."""
import logging
async def dev() -> _ScriptedDevice:
return device
return SimpleNamespace(
settings_store=store,
is_connected=True,
device=dev,
devstate=SimpleNamespace(invalidate_config=lambda: None),
log=logging.getLogger("test-settings-offer"),
)
def _run(ctx, monkeypatch, *, choice=None): # noqa: ANN001, ANN202
"""Drive the offer once, capturing whether (and with what) the prompt was shown."""
shown: list[list] = []
async def fake_prompt(_ctx, drifted): # noqa: ANN001, ANN202
shown.append(list(drifted))
return choice
monkeypatch.setattr(offer_mod, "_prompt", fake_prompt)
asyncio.run(offer_remembered_settings(ctx))
return shown
def test_offer_restores_a_position_the_device_lost_without_asking(
tmp_path: Path, monkeypatch,
) -> None:
"""Device reports the null island, store remembers a fix → silent restore, no prompt."""
store = SettingsStore(tmp_path / "settings.json")
store.remember(_PUB, "adv_lat", _LAT)
store.remember(_PUB, "adv_lon", _LON)
device = _ScriptedDevice(adv_lat=0.0, adv_lon=0.0)
shown = _run(_ctx(store, device), monkeypatch)
assert shown == [] # the operator was never bothered
# Both coordinates were written back; the second write carries the first's value
# (the coupled set_coords rebuilds from the updated snapshot), so the device ends
# on the full remembered fix.
assert device.coords_writes[-1] == (_LAT, _LON)
# The store still remembers the fix — nothing was adopted or forgotten.
assert store.settings(_PUB) == {"adv_lat": _LAT, "adv_lon": _LON}
def test_offer_still_asks_when_the_device_reports_a_real_conflicting_fix(
tmp_path: Path, monkeypatch,
) -> None:
"""A device with an actual different position is a true conflict: prompt, don't write."""
store = SettingsStore(tmp_path / "settings.json")
store.remember(_PUB, "adv_lat", _LAT)
store.remember(_PUB, "adv_lon", _LON)
device = _ScriptedDevice(adv_lat=45.533451, adv_lon=-73.710235)
shown = _run(_ctx(store, device), monkeypatch, choice=None) # operator dismisses
assert len(shown) == 1 and {d.key for d in shown[0]} == {"adv_lat", "adv_lon"}
assert device.coords_writes == [] # dismissing wrote nothing
assert store.settings(_PUB) == {"adv_lat": _LAT, "adv_lon": _LON}
def test_offer_splits_a_mixed_drift_between_silent_position_and_prompted_rest(
tmp_path: Path, monkeypatch,
) -> None:
"""Position restores silently while the other drifted settings still reach the prompt."""
store = SettingsStore(tmp_path / "settings.json")
store.remember(_PUB, "adv_lat", _LAT)
store.remember(_PUB, "adv_lon", _LON)
store.remember(_PUB, "name", "Homestead-Pico")
device = _ScriptedDevice(adv_lat=0.0, adv_lon=0.0, name="MeshCore-abcd")
shown = _run(_ctx(store, device), monkeypatch, choice=None)
assert device.coords_writes[-1] == (_LAT, _LON) # the fix came back silently
assert len(shown) == 1 and [d.key for d in shown[0]] == ["name"] # only the real conflict
def test_offer_stays_quiet_when_nothing_is_remembered(tmp_path: Path, monkeypatch) -> None:
"""A device with no remembered settings pays one probe and sees nothing — no writes."""
store = SettingsStore(tmp_path / "settings.json")
device = _ScriptedDevice(adv_lat=0.0, adv_lon=0.0)
shown = _run(_ctx(store, device), monkeypatch)
assert shown == [] and device.coords_writes == []