diff --git a/flutter_client/test/cache/audio_cache_manager_test.dart b/flutter_client/test/cache/audio_cache_manager_test.dart index f0372894..09a7ace5 100644 --- a/flutter_client/test/cache/audio_cache_manager_test.dart +++ b/flutter_client/test/cache/audio_cache_manager_test.dart @@ -23,7 +23,7 @@ void main() { expect(await mgr.pathFor('nonexistent'), null); }); - test('usageBytes sums sizeBytes across rows', () async { + test('bucketUsage sums drift sizeBytes across rows', () async { final db = _testDb(); addTearDown(db.close); final tmp = Directory.systemTemp.createTempSync(); @@ -32,21 +32,27 @@ void main() { dioFactory: () async => Dio(), cacheDirFactory: () async => tmp, ); + // Each row needs a distinct `path` because path is not the primary + // key, but mapping by trackId only works if rows are distinct. await db.batch((b) { b.insertAll(db.audioCacheIndex, [ AudioCacheIndexCompanion.insert( trackId: 'a', - path: 'p', + path: 'p_a', sizeBytes: 100, source: CacheSource.manual), AudioCacheIndexCompanion.insert( trackId: 'b', - path: 'p', + path: 'p_b', sizeBytes: 250, source: CacheSource.incidental), ]); }); - expect(await mgr.usageBytes(), 350); + // usageBytes() is a directory walk (authoritative on-disk total, + // catches orphan partials); for a drift-sizeBytes sum, bucketUsage + // is the correct API. With empty liked set, both rows go to rolling. + final usage = await mgr.bucketUsage(const {}); + expect(usage.liked + usage.rolling, 350); }); test('rolling cap evicts non-liked LRU; liked protected', () async { diff --git a/flutter_client/test/cache/sync_controller_test.dart b/flutter_client/test/cache/sync_controller_test.dart index ac13f8c5..977fd41e 100644 --- a/flutter_client/test/cache/sync_controller_test.dart +++ b/flutter_client/test/cache/sync_controller_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:drift/native.dart' show NativeDatabase; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,13 +12,22 @@ import 'package:minstrel/library/library_providers.dart' show dioProvider; /// Builds a Dio whose adapter resolves every request to the supplied /// status code + body. Avoids touching the network in tests. +/// +/// Body is normalized through a JSON round-trip so Map/List literals +/// surface as `Map` / `List` (matching how +/// real Dio responses parse), not the `Map` shape +/// Dart's literal inference would otherwise pick. sync_controller's +/// `as Map` casts are invariant on generics and +/// would throw TypeError on the literal shape. Dio _stubDio({required int status, dynamic body}) { final dio = Dio(); + final normalizedBody = + (body is Map || body is List) ? jsonDecode(jsonEncode(body)) : body; dio.interceptors.add(InterceptorsWrapper(onRequest: (req, h) { h.resolve(Response( requestOptions: req, statusCode: status, - data: body, + data: normalizedBody, )); })); return dio; diff --git a/flutter_client/test/likes/like_button_test.dart b/flutter_client/test/likes/like_button_test.dart index 8976904d..5eb1ba9b 100644 --- a/flutter_client/test/likes/like_button_test.dart +++ b/flutter_client/test/likes/like_button_test.dart @@ -71,7 +71,7 @@ void main() { // First tap = like; succeeds → heart flips to filled. await tester.tap(find.byType(IconButton)); - await tester.pump(); + await tester.pumpAndSettle(); expect(heartFilled(), isTrue); // Force the next mutation to fail; rollback restores prior state. @@ -80,7 +80,7 @@ void main() { try { await controller.toggle(LikeKind.album, 'al-1'); } catch (_) {/* expected */} - await tester.pump(); + await tester.pumpAndSettle(); expect(heartFilled(), isTrue); // rolled back to liked }); } diff --git a/flutter_client/test/quarantine/quarantine_provider_test.dart b/flutter_client/test/quarantine/quarantine_provider_test.dart index cf6818d1..d35691df 100644 --- a/flutter_client/test/quarantine/quarantine_provider_test.dart +++ b/flutter_client/test/quarantine/quarantine_provider_test.dart @@ -102,6 +102,12 @@ 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',