diff --git a/lib/features/rides/ride_state_machine.dart b/lib/features/rides/ride_state_machine.dart index ccd6b02..a57771c 100644 --- a/lib/features/rides/ride_state_machine.dart +++ b/lib/features/rides/ride_state_machine.dart @@ -12,7 +12,6 @@ import '../../services/audio_alert_service.dart'; import '../records/records_service.dart'; import '../garage/garage_providers.dart'; import '../../services/field_test/field_test_logger.dart'; -import '../../services/github/github_sync_service.dart'; enum RideState { idle, ready, riding, paused, finished } @@ -377,14 +376,6 @@ class RideStateMachine extends StateNotifier { } } catch (_) {} - // Offline-first background synchronization to GitHub (if enabled) - final completedRideId = state.dbId; - if (completedRideId != null) { - try { - ref.read(githubSyncServiceProvider).queueRideForUpload(completedRideId); - } catch (_) {} - } - state = state.copyWith( state: RideState.finished, healthStatus: RecordingHealthStatus.recordingComplete, diff --git a/lib/main.dart b/lib/main.dart index 93a1190..2b3e6d2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,7 @@ import 'firebase_options.dart'; import 'core/theme/theme.dart'; import 'core/router.dart'; import 'features/records/records_service.dart'; -import 'services/sync/sync_providers.dart'; +import 'services/sync/sync_observer.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -47,9 +47,9 @@ class _MotorcycleDashAppState extends ConsumerState { @override void initState() { super.initState(); - // Eagerly construct the sync manager so persisted outbox work is - // reconciled immediately at app startup, not only after visiting a sync UI. - ref.read(syncManagerProvider); + // Eagerly construct the sync observer so completed rides enter the durable + // outbox at startup scope, not only after visiting a sync UI. + ref.read(syncObserverProvider); _initDeepLinks(); } diff --git a/test/sync_test.dart b/test/sync_test.dart index 37d5559..23b6049 100644 --- a/test/sync_test.dart +++ b/test/sync_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:drift/native.dart'; @@ -428,5 +430,102 @@ void main() { observer.onRideSessionChanged(prevSession, nextSession); expect(outbox.count, equals(1)); }); + + test('Stage 2B: production startup instantiates the ride sync observer', () { + final mainSource = File('lib/main.dart').readAsStringSync(); + + expect(mainSource, contains("import 'services/sync/sync_observer.dart';")); + expect(mainSource, contains('ref.read(syncObserverProvider);')); + }); + + test('Stage 2B: completed ride transition enters durable firebase outbox', () async { + final outbox = SyncOutbox(prefs); + final provider = FakeSyncProvider( + providerId: 'firebase', + behavior: FakeSyncBehavior.authRequired, + ); + final manager = SyncManager( + outbox, + initialProviders: [provider], + defaultProviderId: 'firebase', + ); + final observer = SyncObserver(manager); + addTearDown(manager.dispose); + + final previous = RideSession( + dbId: 901, + state: RideState.riding, + healthStatus: RecordingHealthStatus.recordingActive, + ); + final completed = previous.copyWith( + state: RideState.finished, + healthStatus: RecordingHealthStatus.recordingComplete, + ); + + observer.onRideSessionChanged(previous, completed); + await Future.delayed(Duration.zero); + + final item = outbox.getItem(901, 'firebase'); + expect(item, isNotNull); + expect(item?.localRideId, 901); + expect(item?.providerId, 'firebase'); + expect( + prefs.getString(outbox.storageKey), + contains('"localRideId":901'), + ); + }); + + test('Stage 2B: automatic completion does not invoke legacy queue path', () { + final rideStateMachineSource = + File('lib/features/rides/ride_state_machine.dart').readAsStringSync(); + final legacyServiceSource = + File('lib/services/github/github_sync_service.dart').readAsStringSync(); + + expect(rideStateMachineSource, isNot(contains('queueRideForUpload'))); + expect( + rideStateMachineSource, + isNot(contains('services/github/github_sync_service.dart')), + ); + expect(legacyServiceSource, contains('queueRideForUpload')); + expect(legacyServiceSource, contains('uploadRideToGitHub')); + }); + + test('Stage 2B: duplicate completed emissions keep one active outbox item', () async { + final outbox = SyncOutbox(prefs); + final manager = SyncManager( + outbox, + initialProviders: [ + FakeSyncProvider( + providerId: 'firebase', + behavior: FakeSyncBehavior.authRequired, + ), + ], + defaultProviderId: 'firebase', + ); + final observer = SyncObserver(manager); + addTearDown(manager.dispose); + + final riding = RideSession( + dbId: 902, + state: RideState.riding, + healthStatus: RecordingHealthStatus.recordingActive, + ); + final finished = riding.copyWith( + state: RideState.finished, + healthStatus: RecordingHealthStatus.recordingComplete, + ); + + observer.onRideSessionChanged(riding, finished); + observer.onRideSessionChanged(riding, finished); + observer.onRideSessionChanged(finished, finished); + await Future.delayed(Duration.zero); + + expect(outbox.count, 1); + expect( + outbox.getAllItems().where((item) => item.localRideId == 902), + hasLength(1), + ); + expect(outbox.getItem(902, 'firebase'), isNotNull); + }); }); }