diet-guard/diet_guard/tests/test_sync_refresh.py
Krzysztof kuhy Rudnicki b53ac92c59 perf(gate): narrow the pre-lock pull and defer the heavy imports
The gate's pre-lock refresh ran a *full* sync tick -- peer logs, budget,
both food banks, a rebuild and a push -- in front of the fullscreen lock.
Measured against the live remote: ~18-27s, of which the curated-bank loop
alone was 17s spent fetching nothing (absent on Firebase for every peer, so
each of ~21 peers fell through to a GitHub round trip returning `{}`).

It was also failing: five `sync unavailable (network error trying to refresh
the session)` entries in the last five days, one of them a meal that logged
locally but never published.

The gate has one question -- "has a peer already logged this slot?" -- and
only the peer logs can answer it. `_sync_refresh` does that and nothing
else: one bulk revision-map read, a fetch of only the peers whose revision
moved, merge, re-sign, write. No budget, no banks, no push. The full tick
still runs, after the window closes, so nothing is permanently skipped --
the exhaustive pass just moves off the path the user waits on.

Three state invariants have their own regression tests, because each is
silent when broken: `pushed_rev` survives verbatim (this pass never pushes,
and overwriting it would make the device stop publishing forever),
`peer_revs` is merged rather than replaced (only changed peers are visited),
and the log is written before the state.

Separately, `gate --check` was importing an HTTP stack and a GUI it never
uses. `requests` (~78ms) arrived via `_cli_sync` -> `crdt_sync` and via
`_resolve` -> `_estimator_off`; both are now resolved through module-level
`__getattr__` (PEP 562) and `import_module`, which keeps every existing
`patch.object` target and needs no lint suppression.

Measured: `gate --check` 216ms -> ~110ms; narrow pull 92ms against the live
remote. Accepted trade, documented in `_sync_refresh`: a budget edited on
the phone is not visible in the lock dashboard until the post-window tick.
It cannot affect the lock decision, which is why it is safe to defer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ5YnA3CERitQ57HjMu2Qz
2026-08-21 12:58:44 +02:00

249 lines
9.1 KiB
Python

"""Tests for the gate's narrow, pre-lock refresh.
The full tick measured ~21s against the live remote, which is far too slow to
sit in front of a fullscreen lock. ``_sync_refresh`` answers the gate's single
question -- "has a peer already logged this slot?" -- from the peer logs alone.
The three state invariants below are the traps that make this pass dangerous
if it drifts, so each has its own regression test:
* ``pushed_rev`` must survive verbatim (this pass never pushes),
* ``peer_revs`` must be *merged*, never replaced (only changed peers visited),
* the log must be written before the state.
"""
from __future__ import annotations
import json
from typing import cast
from unittest.mock import MagicMock, patch
from crdt_sync import FileSyncStateStore, GitHubSyncError, SyncState
import pytest
from diet_guard import _sync_client, _sync_refresh
from diet_guard._device import device_id
from diet_guard._state import log_meal
from diet_guard._state_sync import read_raw_log, write_raw_log
from diet_guard.sync_merge import daylog_to_log
from diet_guard.tests._sync_fakes import _mock_client, _nutrition, _write_token
def _state_store() -> FileSyncStateStore:
"""Return a store bound to the *redirected* state path, resolved per call."""
from diet_guard import _sync
return FileSyncStateStore(_sync.SYNC_STATE_FILE)
def _peer_log_text() -> str:
"""Return the wire text a peer would push for one logged meal.
Built through ``log_meal`` into the (redirected) local log, serialised, then
the local log is emptied again -- so the merge under test is unambiguously
the peer's record arriving, not one that was already there.
"""
log_meal("peer porridge", _nutrition(), 8)
text = json.dumps(
{rid: rec.to_dict() for rid, rec in daylog_to_log(read_raw_log()).items()},
indent=2,
)
write_raw_log({})
return text
class TestCandidatePeers:
"""Enumerating peers without the ~445ms mirrored directory listing."""
def test_uses_the_revision_map_and_the_cache(self) -> None:
"""The union covers peers that stopped publishing revisions."""
client = MagicMock()
state = SyncState(pushed_rev=None, peer_revs={"cached": "r"})
peers = _sync_refresh._candidate_peers(client, {"published": "r"}, state)
assert peers == ["cached", "published"]
client.list_directory.assert_not_called()
def test_falls_back_to_listing_without_a_revision_map(self) -> None:
"""No revisions means no information -- not "no peers"."""
client = _mock_client(devices=("phone",))
state = SyncState(pushed_rev=None, peer_revs={})
assert _sync_refresh._candidate_peers(client, {}, state) == ["phone"]
client.list_directory.assert_called_once()
class TestPeerIsCurrent:
"""Which peers this narrow pass is allowed to skip."""
def test_skips_a_matching_revision(self) -> None:
state = SyncState(pushed_rev=None, peer_revs={"phone": "r1"})
assert _sync_refresh._peer_is_current("phone", {"phone": "r1"}, state)
def test_fetches_a_moved_revision(self) -> None:
state = SyncState(pushed_rev=None, peer_revs={"phone": "r1"})
assert not _sync_refresh._peer_is_current("phone", {"phone": "r2"}, state)
def test_skips_a_rev_less_peer_already_merged(self) -> None:
"""Removes the standing cost of the frozen legacy role-id directories."""
state = SyncState(pushed_rev=None, peer_revs={"desktop": "whatever"})
assert _sync_refresh._peer_is_current("desktop", {}, state)
def test_fetches_a_genuinely_new_rev_less_peer(self) -> None:
state = SyncState(pushed_rev=None, peer_revs={})
assert not _sync_refresh._peer_is_current("newcomer", {}, state)
def test_a_null_cached_revision_still_counts_as_seen(self) -> None:
"""``peer_revs`` holds explicit nulls; ``in`` is the only safe test.
Using ``.get(...) is not None`` here would re-download those peers on
every single tick, forever.
"""
# A real ``sync_state.json`` holds explicit nulls here even though the
# annotation says ``str`` -- that mismatch is exactly what this test
# pins, so the value is cast in rather than suppressed at the line.
peer_revs = cast("dict[str, str]", {"pc": None})
state = SyncState(pushed_rev=None, peer_revs=peer_revs)
assert _sync_refresh._peer_is_current("pc", {}, state)
class TestRefreshPeerLogs:
"""The pass itself."""
def test_no_candidates_touches_nothing(self) -> None:
_write_token()
client = _mock_client(devices=())
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
client.get_file_text.assert_not_called()
def test_skips_this_device(self) -> None:
"""Merging our own pushed log back in is pure waste."""
_write_token()
client = _mock_client(devices=(device_id(),))
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
client.get_file_text.assert_not_called()
def test_a_missing_remote_file_is_skipped(self) -> None:
_write_token()
client = _mock_client(devices=("phone",), files={})
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
assert read_raw_log() == {}
def test_an_unparsable_payload_is_skipped_and_not_recorded(self) -> None:
"""A corrupt push must be retried next tick, never remembered as merged."""
_write_token()
client = _mock_client(
devices=("phone",),
files={"diet-guard-sync/devices/phone/food_log.json": "{not json"},
)
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
assert "phone" not in _state_store().load().peer_revs
def test_merges_a_peer_entry_into_the_local_log(self) -> None:
_write_token()
text = _peer_log_text()
client = _mock_client(
devices=("phone",),
files={"diet-guard-sync/devices/phone/food_log.json": text},
)
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
assert any(
entry.get("desc") == "peer porridge"
for entries in read_raw_log().values()
for entry in entries
)
def test_never_pushes(self) -> None:
"""This pass is pull-only; a push here would block the lock window."""
_write_token()
text = _peer_log_text()
client = _mock_client(
devices=("phone",),
files={"diet-guard-sync/devices/phone/food_log.json": text},
)
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
client.put_file_text.assert_not_called()
def test_preserves_pushed_rev(self) -> None:
"""Overwriting it would make this device stop pushing forever."""
_write_token()
store = _state_store()
store.save(SyncState(pushed_rev="mine-v1", peer_revs={}))
text = _peer_log_text()
client = _mock_client(
devices=("phone",),
files={"diet-guard-sync/devices/phone/food_log.json": text},
)
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
assert store.load().pushed_rev == "mine-v1"
def test_merges_peer_revs_rather_than_replacing_them(self) -> None:
"""Only changed peers are visited, so a replace would drop the rest."""
_write_token()
store = _state_store()
store.save(SyncState(pushed_rev=None, peer_revs={"other": "r-other"}))
text = _peer_log_text()
client = _mock_client(
devices=("phone", "other"),
files={"diet-guard-sync/devices/phone/food_log.json": text},
)
with patch.object(_sync_client, "GitHubSyncClient", return_value=client):
_sync_refresh.refresh_peer_logs()
assert store.load().peer_revs["other"] == "r-other"
class TestPullPeerLogs:
"""The fail-closed wrapper the gate actually calls."""
def test_returns_none_on_success(self) -> None:
with patch.object(_sync_refresh, "refresh_peer_logs"):
assert _sync_refresh.pull_peer_logs() is None
def test_returns_a_reason_on_a_sync_failure(self) -> None:
with patch.object(
_sync_refresh, "refresh_peer_logs", side_effect=GitHubSyncError("boom")
):
reason = _sync_refresh.pull_peer_logs()
assert reason is not None
assert "boom" in reason
def test_an_unexpected_error_is_not_swallowed(self) -> None:
"""A bug outside the known failure surface must surface, not hide."""
with (
patch.object(
_sync_refresh, "refresh_peer_logs", side_effect=KeyError("bug")
),
pytest.raises(KeyError),
):
_sync_refresh.pull_peer_logs()