feat(bibliothèque): migration Drift v16, seed basket et seeding au démarrage (ticket #80)
Ajoute la migration Drift v16 (enum ExerciseCategory, champ isExample sur exercises/programs/workout_templates, table local_seed_metadata), le contenu seed basket déterministe (21 exercices, 1 programme, 1 séance- modèle) dans lib/application/starter_content/, les ports StarterSeedStateRepository/StarterContentRepository et le use case SeedStarterContentUseCase branché dans AppBootstrap.create(). Validé GO par QA/Main : dart analyze propre, suite complète 167 tests (4 échecs préexistants sans rapport avec ce lot), 4 nouveaux tests seed tous verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -98,6 +98,151 @@ void main() {
|
||||
expect(restored!.defaultTargetScore, 0);
|
||||
});
|
||||
|
||||
test('starter seed populates a fresh database once', () async {
|
||||
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||
final result = await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
|
||||
expect(result.status, StarterSeedStatus.inserted);
|
||||
expect(await seedRepository.readAppliedStarterSeedVersion(), 1);
|
||||
|
||||
final exercises = await exerciseRepository.listActive();
|
||||
final programs = await programRepository.listActive();
|
||||
final templates = await templateRepository.listActive();
|
||||
expect(exercises, hasLength(21));
|
||||
expect(programs, hasLength(1));
|
||||
expect(templates, hasLength(1));
|
||||
expect(exercises.every((exercise) => exercise.isExample), isTrue);
|
||||
expect(programs.single.isExample, isTrue);
|
||||
expect(templates.single.isExample, isTrue);
|
||||
expect(exercises.map((exercise) => exercise.category).toSet(), {
|
||||
ExerciseCategory.shoot,
|
||||
ExerciseCategory.freeThrows,
|
||||
ExerciseCategory.dribble,
|
||||
ExerciseCategory.finishing,
|
||||
ExerciseCategory.conditioning,
|
||||
ExerciseCategory.defense,
|
||||
ExerciseCategory.mobility,
|
||||
});
|
||||
|
||||
final secondRun = await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 21, 9)),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
expect(secondRun.status, StarterSeedStatus.skippedAlreadyApplied);
|
||||
expect(await exerciseRepository.listActive(), hasLength(21));
|
||||
});
|
||||
|
||||
test(
|
||||
'starter seed marks non-empty database without inserting examples',
|
||||
() async {
|
||||
final now = DateTime.utc(2026, 7, 21, 8);
|
||||
await exerciseRepository.save(
|
||||
Exercise(
|
||||
metadata: _metadata('user-exercise-1', now),
|
||||
name: 'Exercice utilisateur',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
),
|
||||
);
|
||||
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||
|
||||
final result = await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(now),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
|
||||
expect(result.status, StarterSeedStatus.skippedNotEmpty);
|
||||
expect(await seedRepository.readAppliedStarterSeedVersion(), 1);
|
||||
expect(await exerciseRepository.listActive(), hasLength(1));
|
||||
expect(await programRepository.listActive(), isEmpty);
|
||||
expect(await templateRepository.listActive(), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('starter seed does not reappear after example deletion', () async {
|
||||
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||
await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
|
||||
final exercise = (await exerciseRepository.listActive()).first;
|
||||
await exerciseRepository.save(
|
||||
exercise.copyWith(
|
||||
metadata: exercise.metadata.markDeleted(DateTime.utc(2026, 7, 21, 9)),
|
||||
),
|
||||
);
|
||||
await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 21, 10)),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
|
||||
expect(await exerciseRepository.listActive(), hasLength(20));
|
||||
expect(await exerciseRepository.findById(exercise.metadata.id), isNotNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'starter program and workout template snapshot exercise steps',
|
||||
() async {
|
||||
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||
await SeedStarterContentUseCase(
|
||||
seedStateRepository: seedRepository,
|
||||
contentRepository: seedRepository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||
originDeviceId: 'local-device',
|
||||
).run();
|
||||
|
||||
final program = (await programRepository.listActive()).single;
|
||||
expect(program.name, 'Fondations basket - 45 min');
|
||||
expect(program.exercises, hasLength(8));
|
||||
expect(
|
||||
program.exercises
|
||||
.map((exercise) => exercise.setsCount)
|
||||
.reduce((total, count) => total + count),
|
||||
22,
|
||||
);
|
||||
final spotShooting = program.exercises.singleWhere(
|
||||
(exercise) =>
|
||||
exercise.exerciseNameSnapshot == 'Spot shooting 5 positions',
|
||||
);
|
||||
expect(spotShooting.exerciseStepsSnapshot, hasLength(5));
|
||||
expect(spotShooting.exerciseStepsSnapshot.map((step) => step.name), [
|
||||
'Coin droit',
|
||||
'Aile droite',
|
||||
'Face cercle',
|
||||
'Aile gauche',
|
||||
'Coin gauche',
|
||||
]);
|
||||
|
||||
final template = (await templateRepository.listActive()).single;
|
||||
expect(template.name, 'Séance exemple - Fondations basket');
|
||||
expect(template.programs, hasLength(1));
|
||||
expect(template.programs.single.sourceProgramId, program.metadata.id);
|
||||
final snapshot =
|
||||
jsonDecode(template.programs.single.programSnapshotJson)
|
||||
as Map<String, Object?>;
|
||||
expect(snapshot['programId'], program.metadata.id);
|
||||
expect(snapshot['name'], program.name);
|
||||
expect(snapshot['exercises'], isA<List<Object?>>());
|
||||
expect(snapshot['exercises'] as List<Object?>, hasLength(8));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'step chaining settings round-trip through drift repositories',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user