Last 2 of the 6 surfaced drift-cohort failures. Diagnoses below.
quarantine_provider_test 'flag keeps drift optimistic + queues
mutation on server failure':
MyQuarantineController.build() schedules unawaited _refreshFromServer()
which reads connectivityProvider (a StreamProvider<bool>). In tests
without an override, that stream never emits — its real impl listens
to connectivity_plus's platform channel which has no fixture in
flutter test. The success-path quarantine tests resolve their main
flow before _refreshFromServer's chain reaches the connectivity read,
so they don't trip the "disposed during loading" guard at tearDown.
The throwing-API variant's `await ref.read(mutationQueueProvider)
.enqueue(...)` advances enough async work that the connectivity read
IS reached, then container.dispose() trips Riverpod's invariant.
Fix: override connectivityProvider in the shared _container helper
with `Stream.value(true)` so it resolves immediately. The previous
`await Future.delayed(Duration.zero)` workaround is removed — the
cleaner fix makes that band-aid unnecessary.
like_button_test 'tap toggles icon optimistically; rollback on error':
LikesController.toggle:
final user = _ref.read(authControllerProvider).value;
if (user == null) return;
The test only overrode `likesApiProvider`; authControllerProvider was
in AsyncLoading state at toggle() time → user was null → early return
→ no drift write → likedIdsProvider never emits "liked" → heart never
fills → assertion fails. The test would never have passed against the
current controller; it was a stale survivor of an older API.
Fix: stub authControllerProvider with _FakeAuthController that yields
User(id: 'u1', …) immediately. Also add overrides for appDbProvider
(NativeDatabase.memory, bypasses drift_flutter's Timer) and
connectivityProvider (Stream.value(true), unblocks cacheFirst's
isOnline check) — both are transitive dependencies of
likedIdsProvider's cacheFirst.
After this push, the full drift cohort should be all-green. Fable #399
/ local #62 closes when CI lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minstrel mobile client
Flutter (iOS + Android) sibling of the SvelteKit web SPA. v1 first slice ships: scaffold, auth, library browse (home / artist / album), likes, player with background audio + lock-screen controls.
Setup
cd flutter_client
flutter pub get
./tool/sync_shared.sh # copy tokens / error-copy / placeholders from ../web
dart run tool/gen_tokens.dart # regen lib/theme/tokens.dart from shared/fabledsword.tokens.json
flutter run
sync_shared.sh is idempotent. CI runs it on every build; rerun locally
whenever web/src/lib/styles/tokens.json,
web/src/lib/styles/error-copy.json, or
web/static/placeholders/album-fallback.svg changes.
Project layout
See docs/superpowers/specs/2026-05-02-flutter-mobile-foundation-design.md
section 3. Briefly:
lib/api/— dio client, ApiError, error-copy loader, per-surface endpoints.lib/auth/— server URL screen, login screen, AuthController + secure storage.lib/library/— home, artist detail, album detail, providers, widgets.lib/likes/— LikeButton + optimistic toggle controller.lib/player/— audio handler, player provider, mini PlayerBar, NowPlaying.lib/theme/— FabledSword token Dart class (generated), ThemeExtension, ThemeData.lib/shared/— routing (go_router shell), version gate, connection error banner.
shared/fabledsword.tokens.json and assets/error-copy.json are
synced from web/. Don't edit them directly — edit web/src/lib/styles/
and re-run ./tool/sync_shared.sh.
Architecture
- State: Riverpod 2 (
AsyncValue,AsyncNotifier, family providers). - HTTP: dio 5 with a Bearer-auth interceptor; 401 clears the session and the router redirects to login.
- Audio: just_audio wrapped by audio_service for background playback + lock-screen / notification controls.
- Auth: Bearer tokens in flutter_secure_storage. Server already
supports both cookie (web SPA) and Bearer (
internal/auth/session.go). - Routing: go_router with a ShellRoute so the PlayerBar persists
across navigation. Cold launch flow: no server-url →
/server-url, url set / no token →/login, token present →/home.
CI
.forgejo/workflows/flutter.yml runs on push to dev/main, tag pushes,
PR to main, and manual dispatch — path-filtered to flutter_client/**
plus the shared web inputs. Steps: sync, analyze, test, build APK.
Debug APKs upload as artifacts; release APKs attach to the Forgejo
release on tag.
Versioning
pubspec.yaml version mirrors the server tag (e.g. server v0.1.0
→ Flutter 0.1.0+1). The server's /healthz returns
min_client_version; old clients show an Update Required modal and
refuse to operate. Bump internal/server/version.go MinClientVersion
when a server-side change requires a paired client update.
Distribution
- Android: APK on the Forgejo release page. No Play Store for v1.
- iOS: TestFlight on tag (manual upload v1, automate later).
Out-of-scope for slice 1
Search, discover, requests, settings beyond server URL, admin, listening history, playlists, offline cache (#357). Those land in subsequent slices with their own brainstorm/spec/plan cycles. Per the operator's M7 ordering decision (2026-05-02), slice 2+ pauses to ship the missing web features first (#352 playlists, #362 theme toggle, #363 queue UI, #364 listening history, etc.) before mirroring on Flutter.