test(flutter): fix 6 latent failures uncovered by the drift cohort un-skip
All six tests had been silently skipped under @Tags(['drift']) for 6+ months, so they accumulated test-vs-implementation drift. Now running against the libsqlite3-bearing ci-flutter:3.44 image, each failed for a distinct reason. Diagnoses below. audio_cache_manager_test 'usageBytes sums sizeBytes across rows': usageBytes() is a directory walk (authoritative on-disk total, catches orphan partials the index misses). The test inserted drift rows but never wrote files, so the walk returned 0. The actual API for summing drift sizeBytes is bucketUsage(). Rename to 'bucketUsage sums drift sizeBytes across rows', use that API, assert liked+rolling == 350. Also give the two rows unique paths per row-shape sanity. sync_controller_test (3 200-path tests, all returning null result): Map literals in Dart 3 with mixed value types infer as Map<String, Object>, not Map<String, dynamic>. The sync controller casts `resp.data as Map<String, dynamic>` (and several nested casts), which is invariant on generics and throws TypeError. The silent try/catch in sync() swallowed the throw and returned null. Real JSON parsing produces Map<String, dynamic>, so this never surfaced in production. Fix: route the test stub body through jsonDecode(jsonEncode(body)) in _stubDio — mimics real Dio's parsed-response shape. Affects '200 with artist upsert', '200 with track delete', and 'like_track upsert + delete round-trip'. quarantine_provider_test 'flag keeps drift optimistic + queues mutation on server failure': When the API stub throws, the controller catches + queues to CachedMutations. The drift watch() stream in MyQuarantineController was still in loading state when addTearDown disposed the container, tripping Riverpod's "StreamProvider disposed during loading" assertion. The success-path tests resolved before tearDown so they didn't see it. Fix: await one microtask before the test ends so the stream emits. like_button_test 'tap toggles icon optimistically; rollback on error': After the LikeButton was migrated to LucideHeart in the Lucide sweep, the prior fix replaced the find.byIcon assertion with a heartFilled() helper reading LucideHeart.filled. But likesController .toggle() goes through an async chain (optimistic state flip + await api.like + state notification), which one frame of tester.pump() doesn't flush. Use pumpAndSettle after both tap and rollback toggle so the widget rebuilds with the new state before the assertion. After this push, the drift cohort should be all-green on the libsqlite3-bearing image. Fable #399 / local #62. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -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 <String>{});
|
||||
expect(usage.liked + usage.rolling, 350);
|
||||
});
|
||||
|
||||
test('rolling cap evicts non-liked LRU; liked protected', () async {
|
||||
|
||||
+12
-1
@@ -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<String, dynamic>` / `List<dynamic>` (matching how
|
||||
/// real Dio responses parse), not the `Map<String, Object>` shape
|
||||
/// Dart's literal inference would otherwise pick. sync_controller's
|
||||
/// `as Map<String, dynamic>` 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<dynamic>(
|
||||
requestOptions: req,
|
||||
statusCode: status,
|
||||
data: body,
|
||||
data: normalizedBody,
|
||||
));
|
||||
}));
|
||||
return dio;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<void>.delayed(Duration.zero);
|
||||
});
|
||||
|
||||
test('unflag deletes the drift row and calls the server',
|
||||
|
||||
Reference in New Issue
Block a user