Commit Graph

2 Commits

Author SHA1 Message Date
c7371d7e0d perf(gate): unfreeze "Fetch from sync" and bound the interactive timeout
The button ran a full sync tick inline on the Tk thread, so the fullscreen
lock was dead for its whole duration -- ~18-27s measured against the live
remote, and unbounded in the worst case.

Two changes, and the second is what makes the first cheap:

* the pull moves to a daemon worker thread, with the result marshalled back
  through a queue polled on the Tk thread. The worker touches no widget, by
  construction: Tcl is not thread-safe and that includes `after`. Polling
  also makes the completion path testable with no thread at all.
* the button now runs the *narrow* pull. Its question is identical to the
  gate's -- "did a peer log this slot?" -- and `_reconcile_after_fetch`
  reads only `due_slots()`, so the budget and food banks it used to fetch
  were never consulted here. The full tick still runs after the window
  closes. Measured 84ms against the live remote.

Narrowing also shrinks the write race from tens of seconds to imperceptible:
the pull persists its merge, so a meal logged mid-fetch could be overwritten
by a snapshot taken before it. A second click while a fetch is in flight is
now ignored, which is the reentrancy guard and the race fix in one.

That guard started out as a disabled button, which is how it surfaced a
latent bug worth recording: the fetch button widget is never stored on
`GateWidgets`, so `self._widgets.fetch_button` did not exist and the disable
was a silent no-op -- hidden because the suppression tuple included
`AttributeError`. Coverage would have reported 100% on a guard that did
nothing. Guarding on `_fetch_result` needs no widget at all.

Also adds `INTERACTIVE_TIMEOUT_SECONDS = 2.0` for the paths a user waits on.
`SYNC_TIMEOUT_SECONDS` only ever reached GitHub: crdt_sync v0.6.0's
`firebase_client_for` takes no timeout, so Firebase used its 15s default and
one hung session refresh stalled the lock window -- the journal has five such
refresh failures in five days. Verified to fail closed to the local decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ5YnA3CERitQ57HjMu2Qz
2026-08-21 13:07:37 +02:00
1e201acbdc Split _sync.py and its tests; extract the gate fixtures from conftest
_sync.py 498 -> 165, into siblings that each own one concern:

  _sync_client.py  picking a backend and building the remote store
  _sync_banks.py   pull/merge/push for the food bank and the budget
  _sync_paths.py   the kuhyx/syncs wire layout, shared by both
  _sync_errors.py  SyncError, so _sync and _sync_client need not import
                   each other

The patch targets moved with the code they guard, which is the whole risk of
this commit. unittest.mock.patch and monkeypatch.setattr both bind to a
*module object*, so a patch left pointing at _sync after CONFIG_FILE and
SYNC_TOKEN_FILE moved to _sync_client redirects nothing. That is not
hypothetical here: the first run of the suite took 9m18s instead of 7s and
raised 401s, because tests were reaching the real GitHub API with a fake
token. Repointed:

  conftest       SYNC_TOKEN_FILE, CONFIG_FILE      -> _sync_client
  test_sync*     GitHubSyncClient (30 patch sites) -> _sync_client
  test_backend   mirror/firebase_client_for        -> _sync_client

The repo's own _test_guard caught the same leak independently, which is the
behaviour worth keeping: two guards, both fail-closed.

test_sync.py 482 -> 205, splitting the budget and pull/revision classes into
siblings and the shared fakes into _sync_fakes.py.

conftest.py 284 -> 155, moving the Tk/gatelock fixtures to _gate_fixtures.py.
Importing them back is what registers them (pytest_plugins is an error in a
non-root conftest), and they are named in __all__ because ruff's autofix
otherwise deletes the "unused" import and silently disables Tk blocking --
it did exactly that once here. Verified afterwards with a canary asserting
_gatelock.tk is still a MagicMock, not merely that the suite passes.

Verified: 692 tests / 100% coverage unchanged; ~/.local/share/diet_guard
byte-identical before and after the run; cd /tmp && python3 -m diet_guard sync
-> 402 entries, exit 0. Gate 47 -> 44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK3HCZrM75sRHL1gL8usve
2026-08-15 18:51:18 +02:00