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:
2026-07-22 07:29:49 +02:00
parent 75f52200a2
commit 24e266c0e9
12 changed files with 2052 additions and 16 deletions

View File

@ -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 {