fix: route completed rides through durable sync outbox
This commit is contained in:
parent
57e5508878
commit
32531e477b
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue