@Tags(['drift']) library; import 'package:drift/native.dart' show NativeDatabase; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/db.dart'; import 'package:minstrel/models/track.dart'; import 'package:minstrel/quarantine/quarantine_provider.dart'; // SKIP NOTE: drift's NativeDatabase needs the system libsqlite3.so. // The flutter-ci runner image doesn't ship it. Same skip as // sync_controller_test.dart — unblock by adding libsqlite3-dev to the // CI image, or switch to sqlite3/wasm. On-device runs cover real // behaviour today. const _skipDrift = 'libsqlite3 missing on flutter-ci runner; on-device covers'; class _StubQuarantineApi implements QuarantineApi { _StubQuarantineApi({this.shouldThrow = false}); bool shouldThrow; int flagCalls = 0; int unflagCalls = 0; @override Future flag(String trackId, String reason, {String notes = ''}) async { flagCalls++; if (shouldThrow) throw Exception('simulated server failure'); } @override Future unflag(String trackId) async { unflagCalls++; if (shouldThrow) throw Exception('simulated server failure'); } } const _track = TrackRef( id: 't1', title: 'Roygbiv', albumId: 'a1', albumTitle: 'Geogaddi', artistId: 'ar1', artistName: 'Boards of Canada', durationSec: 137, trackNumber: 4, streamUrl: '', ); ProviderContainer _container({required AppDb db, required QuarantineApi api}) { return ProviderContainer(overrides: [ appDbProvider.overrideWithValue(db), quarantineApiProvider.overrideWith((ref) async => api), ]); } Future _seed(AppDb db) async { await db.into(db.cachedQuarantineMine).insert( CachedQuarantineMineCompanion.insert( trackId: 't1', reason: 'bad_rip', createdAt: '2026-05-01T00:00:00Z', trackTitle: 'Roygbiv', albumId: 'a1', albumTitle: 'Geogaddi', artistId: 'ar1', artistName: 'Boards of Canada', ), ); } void main() { test('flag inserts a drift row and calls the server', skip: _skipDrift, () async { final db = AppDb(NativeDatabase.memory()); addTearDown(db.close); final api = _StubQuarantineApi(); final container = _container(db: db, api: api); addTearDown(container.dispose); await container.read(myQuarantineProvider.future); await container .read(myQuarantineProvider.notifier) .flag(_track, 'bad_rip', ''); expect(api.flagCalls, 1); final rows = await db.select(db.cachedQuarantineMine).get(); expect(rows, hasLength(1)); expect(rows.first.trackId, 't1'); expect(rows.first.reason, 'bad_rip'); }); test('flag keeps drift optimistic + queues mutation on server failure', skip: _skipDrift, () async { final db = AppDb(NativeDatabase.memory()); addTearDown(db.close); final api = _StubQuarantineApi(shouldThrow: true); final container = _container(db: db, api: api); addTearDown(container.dispose); await container.read(myQuarantineProvider.future); // No longer rethrows — flag swallows the REST failure and queues // for replay so the user's intent persists offline. await container .read(myQuarantineProvider.notifier) .flag(_track, 'bad_rip', ''); final rows = await db.select(db.cachedQuarantineMine).get(); expect(rows, hasLength(1), reason: 'optimistic drift row should persist across REST failure'); final mutations = await db.select(db.cachedMutations).get(); expect(mutations, hasLength(1), reason: 'failed flag should have been enqueued for replay'); expect(mutations.first.kind, 'quarantine.flag'); }); test('unflag deletes the drift row and calls the server', skip: _skipDrift, () async { final db = AppDb(NativeDatabase.memory()); addTearDown(db.close); await _seed(db); final api = _StubQuarantineApi(); final container = _container(db: db, api: api); addTearDown(container.dispose); await container.read(myQuarantineProvider.future); await container.read(myQuarantineProvider.notifier).unflag('t1'); expect(api.unflagCalls, 1); final rows = await db.select(db.cachedQuarantineMine).get(); expect(rows, isEmpty); }); test('isHidden reflects drift state', skip: _skipDrift, () async { final db = AppDb(NativeDatabase.memory()); addTearDown(db.close); await _seed(db); final api = _StubQuarantineApi(); final container = _container(db: db, api: api); addTearDown(container.dispose); await container.read(myQuarantineProvider.future); final ctrl = container.read(myQuarantineProvider.notifier); expect(ctrl.isHidden('t1'), isTrue); expect(ctrl.isHidden('t2'), isFalse); }); }