fix: assign stable cloud ride identity at creation

This commit is contained in:
Developer 2026-09-03 00:25:00 -04:00
parent 32531e477b
commit 68c0bda344
4 changed files with 124 additions and 13 deletions

View File

@ -142,10 +142,23 @@ class AppDatabase extends _$AppDatabase {
return created; return created;
} }
Future<int> startRide() async { Future<int> startRide({
final id = await into(rides).insert(RidesCompanion.insert(startTime: DateTime.now())); DateTime? startTime,
await ensureCloudRideId(id); int? motorcycleId,
return id; int? motorcycleYear,
String? motorcycleMake,
String? motorcycleModel,
String? motorcycleVariant,
}) async {
return into(rides).insert(RidesCompanion.insert(
cloudRideId: Value(_newCloudRideId()),
startTime: startTime ?? DateTime.now(),
motorcycleId: Value(motorcycleId),
motorcycleYear: Value(motorcycleYear),
motorcycleMake: Value(motorcycleMake),
motorcycleModel: Value(motorcycleModel),
motorcycleVariant: Value(motorcycleVariant),
));
} }
Future<void> endRide(int rideId, RidesCompanion data) async { Future<void> endRide(int rideId, RidesCompanion data) async {

View File

@ -211,20 +211,21 @@ class RideStateMachine extends StateNotifier<RideSession> {
} catch (_) {} } catch (_) {}
final defaultBike = ref.read(defaultMotorcycleProvider); final defaultBike = ref.read(defaultMotorcycleProvider);
final id = await db.into(db.rides).insert(RidesCompanion.insert( final startTime = DateTime.now();
startTime: DateTime.now(), final id = await db.startRide(
motorcycleId: drift.Value(defaultBike?.id), startTime: startTime,
motorcycleYear: drift.Value(defaultBike?.year), motorcycleId: defaultBike?.id,
motorcycleMake: drift.Value(defaultBike?.makeName), motorcycleYear: defaultBike?.year,
motorcycleModel: drift.Value(defaultBike?.modelName), motorcycleMake: defaultBike?.makeName,
motorcycleVariant: drift.Value(defaultBike?.variantName), motorcycleModel: defaultBike?.modelName,
)); motorcycleVariant: defaultBike?.variantName,
);
state = state.copyWith( state = state.copyWith(
dbId: id, dbId: id,
state: RideState.riding, state: RideState.riding,
healthStatus: RecordingHealthStatus.recordingActive, healthStatus: RecordingHealthStatus.recordingActive,
startTime: DateTime.now(), startTime: startTime,
tripDistanceMiles: 0, tripDistanceMiles: 0,
topSpeedMph: 0, topSpeedMph: 0,
maxLeanLeft: 0, maxLeanLeft: 0,

View File

@ -35,6 +35,22 @@ void main() {
expect(second, first); expect(second, first);
}); });
test('ensureCloudRideId repairs legacy null ride identity', () async {
final db = AppDatabase.forTesting(NativeDatabase.memory());
addTearDown(db.close);
final legacyRideId = await db.into(db.rides).insert(
RidesCompanion.insert(startTime: DateTime.utc(2026, 9, 1, 10)),
);
expect(await db.getCloudRideId(legacyRideId), isNull);
final repaired = await db.ensureCloudRideId(legacyRideId);
expect(repaired, startsWith('ride_'));
expect(await db.getCloudRideId(legacyRideId), repaired);
expect(await db.ensureCloudRideId(legacyRideId), repaired);
});
test('real v5 rides migrate to their legacy MotorCloud identity', () async { test('real v5 rides migrate to their legacy MotorCloud identity', () async {
final connection = NativeDatabase.memory(setup: (rawDb) { final connection = NativeDatabase.memory(setup: (rawDb) {
rawDb.execute(''' rawDb.execute('''

View File

@ -16,6 +16,7 @@ import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart'; import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/features/garage/garage_providers.dart';
class MockAudioAlertService extends Fake implements AudioAlertService { class MockAudioAlertService extends Fake implements AudioAlertService {
final List<String> spokenMessages = []; final List<String> spokenMessages = [];
@ -177,6 +178,86 @@ void main() {
expect(events.any((e) => e.eventType.contains('START') || e.eventType.contains('RIDE')), isTrue); expect(events.any((e) => e.eventType.contains('START') || e.eventType.contains('RIDE')), isTrue);
}); });
test('RideStateMachine assigns stable cloud identity at ride creation', () async {
final bike = GarageMotorcycle(
id: 7,
year: 2024,
makeId: 'triumph',
makeName: 'Triumph',
modelId: 'street-triple',
modelName: 'Street Triple',
variantName: 'RS',
isDefault: true,
dateAdded: DateTime.utc(2026, 9, 1),
lastUpdated: DateTime.utc(2026, 9, 1),
);
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
telemetryServiceProvider.overrideWithValue(fakeTelemetry),
audioAlertServiceProvider.overrideWithValue(mockAudio),
fieldTestLoggerProvider.overrideWithValue(logger),
defaultMotorcycleProvider.overrideWithValue(bike),
],
);
addTearDown(container.dispose);
final notifier = container.read(rideStateMachineProvider.notifier);
await notifier.startRide();
final session = container.read(rideStateMachineProvider);
expect(session.dbId, isNotNull);
final ride = await (db.select(db.rides)..where((r) => r.id.equals(session.dbId!))).getSingle();
expect(ride.cloudRideId, isNotNull);
expect(ride.cloudRideId, startsWith('ride_'));
expect(ride.motorcycleId, bike.id);
expect(ride.motorcycleYear, bike.year);
expect(ride.motorcycleMake, bike.makeName);
expect(ride.motorcycleModel, bike.modelName);
expect(ride.motorcycleVariant, bike.variantName);
final ensured = await db.ensureCloudRideId(ride.id);
expect(ensured, ride.cloudRideId);
expect(await db.getCloudRideId(ride.id), ride.cloudRideId);
});
test('Interrupted ride recovery preserves existing cloud identity', () async {
final rideId = await db.startRide(startTime: DateTime.utc(2026, 9, 2, 12));
final cloudRideId = await db.getCloudRideId(rideId);
expect(cloudRideId, isNotNull);
await db.into(db.telemetrySamples).insert(
TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: DateTime.utc(2026, 9, 2, 12, 5),
),
);
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
telemetryServiceProvider.overrideWithValue(fakeTelemetry),
audioAlertServiceProvider.overrideWithValue(mockAudio),
fieldTestLoggerProvider.overrideWithValue(logger),
],
);
addTearDown(container.dispose);
container.read(rideStateMachineProvider.notifier);
await Future<void>.delayed(const Duration(milliseconds: 50));
final recovered = await (db.select(db.rides)..where((r) => r.id.equals(rideId))).getSingle();
expect(
recovered.endTime!.isAtSameMomentAs(DateTime.utc(2026, 9, 2, 12, 5)),
isTrue,
);
expect(recovered.cloudRideId, cloudRideId);
expect(await db.ensureCloudRideId(rideId), cloudRideId);
});
test('Motion Safeguard: Moving vehicle auto-promotes READY state to active recording', () async { test('Motion Safeguard: Moving vehicle auto-promotes READY state to active recording', () async {
final container = ProviderContainer( final container = ProviderContainer(
overrides: [ overrides: [