fix: assign stable cloud ride identity at creation
This commit is contained in:
parent
32531e477b
commit
68c0bda344
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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('''
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue