Drops the libsqlite3-missing skip cohort now that the ci-flutter
runner image installs libsqlite3-0 (CI-runner commit on its main).
Per-file removals (no behavior change in tests themselves — they
just stop being skipped):
- `@Tags(['drift'])` + `library;` directive from 5 files.
- `const _skipDrift = ...;` declaration + its rationale comment
from 6 files (the 5 above + like_button_test.dart, which had its
own _skipDrift for the rollback-via-drift case).
- `skip: _skipDrift` annotations from 17 test invocations across
those 6 files (16 single-line + 1 multi-line in like_button).
- Stale `@Tags(['drift']) tier covers it` reference in
home_screen_test.dart's drift-coverage comment.
Net -79 +18 lines across 7 files; 17 previously-silent tests are
now part of the CI signal. Fable #399.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User intent (likes, hides, playlist adds, Lidarr requests, cancels)
now persists across network loss. Controllers write their optimistic
local state to drift first, then try the REST call; on failure
the call is enqueued in cached_mutations rather than rolled back.
MutationReplayer drains the queue on connectivity transitions and
a 1-minute periodic tick.
**Infrastructure (schema 8):**
* CachedMutations table — id / kind / payload (JSON) / createdAt
/ lastAttemptAt / attempts. Drop-after-5-attempts semantics: a
permanently-failing mutation eventually drops, and next sync
reconciles drift to the server's authoritative state.
* MutationQueue.enqueue / pendingCount
* MutationReplayer.start + .drain — start fires from app.dart's
postFrameCallback alongside SyncController / Prefetcher / etc.
* Kind registry: like.add / like.remove / quarantine.flag /
quarantine.unflag / playlist.append / request.create /
request.cancel — each with a Ref+payload handler that re-fires
the corresponding REST call.
**Wired surfaces:**
* LikesController.toggle — optimistic drift like/unlike stays
across REST failure; queues the call. Drops the old rollback.
* MyQuarantineController.flag / .unflag — same pattern. Hide/unhide
visibly persists offline; replays when back online.
* addToPlaylistActionProvider — now does an optimistic
cached_playlist_tracks write (position = max + 1) so the
playlist detail screen shows the new track instantly. Queues
appendTracks on REST failure.
* DiscoverScreen._request — queues request.create on DioException.
No drift state for the request itself (myRequestsProvider is
still REST-only) so the row won't show on /requests until replay
succeeds — acceptable for v1.
* MyRequestsController.cancel — optimistic in-memory remove no
longer restores on failure; queues request.cancel instead.
**Test update:**
quarantine_provider_test "flag rolls back on server failure"
renamed and rewritten to assert the new offline behavior:
optimistic drift row persists, mutation is enqueued for replay.
**Out of scope (v2):**
* Playlist create / rename / delete (no Flutter UI exposes these yet)
* Lidarr request optimistic local row (would need a cached_requests
drift table)
* UI "syncing N pending changes" indicator (operator preference:
silent unless we find a concrete need)
Final slice of the smooth-loading pass. Adds CachedQuarantineMine
(schema 5, columnar so flag/unflag can do row-level mutation) and
rewires MyQuarantineController to read from drift via watch() + SWR
refresh; flag/unflag write drift first and roll back on REST failure.
Public API (.flag / .unflag / .isHidden) unchanged so existing call
sites (library_screen Hidden tab, TrackActionsSheet) keep working.
Tests updated to match: bypassed-build-via-_StubController approach
no longer makes sense now that state lives in drift, so the suite is
rewritten against NativeDatabase.memory() with the same libsqlite3
skip the sync_controller suite uses on the CI runner.
The Hidden tab now paints from disk on cold open, the list is
queryable offline, and a flag from another device that arrived in
this user's quarantine via SSE-triggered invalidate lands the same
way as a local flag.
AsyncNotifier wrapping /api/quarantine/mine with optimistic flag /
unflag mutations and an isHidden(trackId) convenience for menu state.
Mirrors the LikedIdsController pattern: optimistic update, rollback
on server error.
Used by the next slice's TrackActionsSheet to power Hide / Unhide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>