fix: route completed rides through durable sync outbox

This commit is contained in:
Developer 2026-09-02 23:39:49 -04:00
parent 57e5508878
commit 32531e477b
3 changed files with 103 additions and 13 deletions

View File

@ -12,7 +12,6 @@ import '../../services/audio_alert_service.dart';
import '../records/records_service.dart'; import '../records/records_service.dart';
import '../garage/garage_providers.dart'; import '../garage/garage_providers.dart';
import '../../services/field_test/field_test_logger.dart'; import '../../services/field_test/field_test_logger.dart';
import '../../services/github/github_sync_service.dart';
enum RideState { idle, ready, riding, paused, finished } enum RideState { idle, ready, riding, paused, finished }
@ -377,14 +376,6 @@ class RideStateMachine extends StateNotifier<RideSession> {
} }
} catch (_) {} } 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 = state.copyWith(
state: RideState.finished, state: RideState.finished,
healthStatus: RecordingHealthStatus.recordingComplete, healthStatus: RecordingHealthStatus.recordingComplete,

View File

@ -8,7 +8,7 @@ import 'firebase_options.dart';
import 'core/theme/theme.dart'; import 'core/theme/theme.dart';
import 'core/router.dart'; import 'core/router.dart';
import 'features/records/records_service.dart'; import 'features/records/records_service.dart';
import 'services/sync/sync_providers.dart'; import 'services/sync/sync_observer.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -47,9 +47,9 @@ class _MotorcycleDashAppState extends ConsumerState<MotorcycleDashApp> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Eagerly construct the sync manager so persisted outbox work is // Eagerly construct the sync observer so completed rides enter the durable
// reconciled immediately at app startup, not only after visiting a sync UI. // outbox at startup scope, not only after visiting a sync UI.
ref.read(syncManagerProvider); ref.read(syncObserverProvider);
_initDeepLinks(); _initDeepLinks();
} }

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
@ -428,5 +430,102 @@ void main() {
observer.onRideSessionChanged(prevSession, nextSession); observer.onRideSessionChanged(prevSession, nextSession);
expect(outbox.count, equals(1)); 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<void>.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<void>.delayed(Duration.zero);
expect(outbox.count, 1);
expect(
outbox.getAllItems().where((item) => item.localRideId == 902),
hasLength(1),
);
expect(outbox.getItem(902, 'firebase'), isNotNull);
});
}); });
} }