From 83a7099db3bda5281dc2b538fe478e2ef47e92ae Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 20 May 2026 19:12:23 -0400 Subject: [PATCH] test(flutter): stub the transitive provider deps the drift cohort needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). 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) --- .../test/likes/like_button_test.dart | 25 +++++++++++++++++++ .../quarantine/quarantine_provider_test.dart | 13 +++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/flutter_client/test/likes/like_button_test.dart b/flutter_client/test/likes/like_button_test.dart index 5eb1ba9b..f17c7310 100644 --- a/flutter_client/test/likes/like_button_test.dart +++ b/flutter_client/test/likes/like_button_test.dart @@ -1,17 +1,31 @@ +import 'package:drift/native.dart' show NativeDatabase; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:minstrel/api/endpoints/likes.dart'; +import 'package:minstrel/auth/auth_provider.dart' show authControllerProvider, AuthController; +import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider; +import 'package:minstrel/cache/connectivity_provider.dart'; +import 'package:minstrel/cache/db.dart'; import 'package:minstrel/likes/like_button.dart'; import 'package:minstrel/likes/likes_provider.dart'; import 'package:minstrel/models/album.dart'; import 'package:minstrel/models/artist.dart'; import 'package:minstrel/models/page.dart'; import 'package:minstrel/models/track.dart'; +import 'package:minstrel/models/user.dart'; import 'package:minstrel/shared/widgets/lucide_heart.dart'; import 'package:minstrel/theme/theme_data.dart'; +/// LikesController.toggle() early-returns if authControllerProvider has +/// no user, so the test needs a stub controller that yields one. +class _FakeAuthController extends AuthController { + @override + Future build() async => + const User(id: 'u1', username: 'tester', isAdmin: false); +} + class _ThrowingLikesApi implements LikesApi { bool throwOnNext = false; @@ -47,8 +61,19 @@ class _ThrowingLikesApi implements LikesApi { void main() { testWidgets('tap toggles icon optimistically; rollback on error', (tester) async { final api = _ThrowingLikesApi(); + // LikesController.toggle reaches authControllerProvider (for user.id), + // appDbProvider (for drift), and likedIdsProvider (which transitively + // reads connectivityProvider via cacheFirst). All four must be + // stubbed for the optimistic-update path to actually run. final container = ProviderContainer(overrides: [ likesApiProvider.overrideWith((ref) async => api), + authControllerProvider.overrideWith(_FakeAuthController.new), + appDbProvider.overrideWith((ref) { + final db = AppDb(NativeDatabase.memory()); + ref.onDispose(db.close); + return db; + }), + connectivityProvider.overrideWith((ref) => Stream.value(true)), ]); addTearDown(container.dispose); diff --git a/flutter_client/test/quarantine/quarantine_provider_test.dart b/flutter_client/test/quarantine/quarantine_provider_test.dart index d35691df..dc1c155e 100644 --- a/flutter_client/test/quarantine/quarantine_provider_test.dart +++ b/flutter_client/test/quarantine/quarantine_provider_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:minstrel/api/endpoints/quarantine.dart'; import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider; +import 'package:minstrel/cache/connectivity_provider.dart'; import 'package:minstrel/cache/db.dart'; import 'package:minstrel/models/track.dart'; import 'package:minstrel/quarantine/quarantine_provider.dart'; @@ -43,6 +44,12 @@ ProviderContainer _container({required AppDb db, required QuarantineApi api}) { return ProviderContainer(overrides: [ appDbProvider.overrideWithValue(db), quarantineApiProvider.overrideWith((ref) async => api), + // _refreshFromServer() in build() reads connectivityProvider; in tests + // without this override it's a StreamProvider that never emits, so + // tearDown trips Riverpod's "disposed during loading" — visible on + // the throwing-API variant where the catch path's await mutationQueue + // .enqueue advances _refreshFromServer's chain enough to surface it. + connectivityProvider.overrideWith((ref) => Stream.value(true)), ]); } @@ -102,12 +109,6 @@ void main() { expect(mutations, hasLength(1), reason: 'failed flag should have been enqueued for replay'); expect(mutations.first.kind, 'quarantine.flag'); - // Let in-flight drift watch() streams settle before tearDown - // disposes the container — otherwise Riverpod's StreamProvider - // fires "disposed during loading state" on the queued-mutation - // path (the success path resolves before the awaits above - // complete, so it doesn't need this). - await Future.delayed(Duration.zero); }); test('unflag deletes the drift row and calls the server',