feat(sync): préparation à la synchronisation cloud (ticket #12)

Ajuste les ports et le bootstrap applicatif, ainsi que le mapping
Drift des repositories, pour préparer une future synchronisation cloud
(invariants documentés dans docs/sync-invariants.md). Corrige au
passage un bug de fuseau horaire UTC dans le mapping Drift. flutter
analyze propre, 35/35 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 00:53:58 +02:00
parent d13a3eb8de
commit e38dd75f69
5 changed files with 603 additions and 122 deletions

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
@ -8,12 +9,16 @@ import 'package:gametime/infrastructure/local/local.dart' as local;
void main() {
late local.AppDatabase database;
late local.DriftExerciseRepository exerciseRepository;
late local.DriftProgramRepository programRepository;
late local.DriftActiveSessionRepository activeRepository;
late local.DriftWorkoutTemplateRepository templateRepository;
late local.DriftWorkoutHistoryRepository historyRepository;
setUp(() {
database = local.AppDatabase(NativeDatabase.memory());
exerciseRepository = local.DriftExerciseRepository(database);
programRepository = local.DriftProgramRepository(database);
activeRepository = local.DriftActiveSessionRepository(database);
templateRepository = local.DriftWorkoutTemplateRepository(database);
historyRepository = local.DriftWorkoutHistoryRepository(database);
@ -23,6 +28,102 @@ void main() {
await database.close();
});
test('exercise mutations are written to change log', () async {
final now = DateTime.utc(2026, 7, 17, 12);
final exercise = Exercise(
metadata: _metadata('exercise-1', now),
name: 'Squat',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
);
await exerciseRepository.save(exercise);
await exerciseRepository.save(
exercise.copyWith(
metadata: _metadata(
'exercise-1',
now.add(const Duration(seconds: 1)),
1,
),
name: 'Front squat',
),
);
await exerciseRepository.save(
exercise
.copyWith(
metadata: _metadata(
'exercise-1',
now.add(const Duration(seconds: 2)),
2,
),
name: 'Front squat',
)
.archive(now.add(const Duration(seconds: 2))),
);
final changes =
await (database.select(database.changeLogEntries)
..where((table) => table.entityId.equals('exercise-1'))
..orderBy([
(table) => drift.OrderingTerm.asc(table.localRevision),
]))
.get();
expect(changes.map((change) => change.operation), [
'insert',
'update',
'update',
]);
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
});
test(
'removing a program exercise writes a soft delete change log entry',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final first = _programExercise('program-exercise-1', now, position: 0);
final second = _programExercise('program-exercise-2', now, position: 1);
await programRepository.save(
Program(
metadata: _metadata('program-1', now),
name: 'Jambes',
defaultRestSeconds: 60,
exercises: [first, second],
),
);
final deletedAt = now.add(const Duration(minutes: 1));
await programRepository.replaceExercises(
Program(
metadata: _metadata('program-1', deletedAt, 1),
name: 'Jambes',
defaultRestSeconds: 60,
exercises: [first],
),
deletedAt,
);
final deletedRow = await (database.select(
database.programExercises,
)..where((table) => table.id.equals('program-exercise-2'))).getSingle();
final change =
await (database.select(database.changeLogEntries)..where(
(table) =>
table.entityType.equals('ProgramExercise') &
table.entityId.equals('program-exercise-2') &
table.operation.equals('softDelete'),
))
.getSingleOrNull();
expect(deletedRow.deletedAt?.toUtc(), deletedAt);
expect(deletedRow.syncState, 'deleted');
expect(deletedRow.localRevision, 1);
expect(change, isNotNull);
expect(change!.localRevision, 1);
},
);
test(
'running session elapsed time survives repository reconstruction',
() async {
@ -167,12 +268,33 @@ String _resolvedSnapshot() {
});
}
EntityMetadata _metadata(String id, DateTime now) {
EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
return EntityMetadata(
id: id,
createdAt: now,
updatedAt: now,
originDeviceId: 'device-1',
localRevision: localRevision,
);
}
ProgramExercise _programExercise(
String id,
DateTime now, {
required int position,
}) {
return ProgramExercise(
metadata: _metadata(id, now),
programId: 'program-1',
position: position,
exerciseNameSnapshot: 'Exercise $position',
availableTimeSnapshot: false,
availableRepsSnapshot: true,
availableScoreSnapshot: false,
setsCount: 1,
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
);
}