feat(core): consolide le chainage type metier d'exercice -> strategie Health Services -> payload montre -> consommation Kotlin

Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 16:17:31 +02:00
parent 30e657a67f
commit 2ae716206a
10 changed files with 444 additions and 22 deletions

View File

@ -54,6 +54,29 @@ void main() {
expect(projection.dominantTimer, isNull);
});
test(
'projects Health Services exercise type strategy from snapshot',
() async {
final repository = _FakeActiveSessionRepository()
..session = _session(
healthServicesExerciseTypeStrategy: const [
'RUNNING',
'HIGH_INTENSITY_INTERVAL_TRAINING',
'WORKOUT',
],
);
final projector = _projector(repository, _clock());
final projection = await projector.project(revision: 1);
expect(projection.healthServicesExerciseTypeStrategy, [
'RUNNING',
'HIGH_INTENSITY_INTERVAL_TRAINING',
'WORKOUT',
]);
},
);
test('projects running with step timer before stopwatch score', () async {
final now = DateTime.utc(2026, 7, 25, 12);
final session = _session(
@ -587,6 +610,7 @@ ActiveWorkoutSession _session({
bool? autoStartNextTimedStepSnapshot = true,
List<ExerciseStep> steps = const [],
String? secondExerciseName,
List<String> healthServicesExerciseTypeStrategy = const [],
}) {
final exerciseSnapshot = {
'id': 'exercise-snapshot-1',
@ -603,6 +627,7 @@ ActiveWorkoutSession _session({
.map((step) => step.toSnapshotJson())
.toList(),
'autoStartNextTimedStepSnapshot': ?autoStartNextTimedStepSnapshot,
'healthServicesExerciseTypeStrategy': healthServicesExerciseTypeStrategy,
};
final secondExerciseSnapshot = secondExerciseName == null
? null

View File

@ -125,6 +125,10 @@ void main() {
expect(await columnNames('exercises'), contains('tags_json'));
expect(await columnNames('exercises'), contains('business_types_json'));
expect(
await columnNames('program_exercises'),
contains('health_services_exercise_type_strategy_snapshot_json'),
);
expect(await columnNames('programs'), contains('tags_json'));
expect(await columnNames('workout_templates'), contains('tags_json'));
expect(
@ -175,6 +179,48 @@ void main() {
BusinessExerciseType.dribble,
BusinessExerciseType.highIntensity,
]);
expect(restoredExercise?.healthServicesExerciseTypeStrategy, [
HealthServicesExerciseType.running,
HealthServicesExerciseType.highIntensityIntervalTraining,
HealthServicesExerciseType.workout,
]);
final program = Program(
metadata: _metadata('program-business-types', now),
name: 'Programme types',
defaultRestSeconds: 30,
exercises: [
ProgramExercise.snapshotFromExercise(
metadata: _metadata('program-exercise-business-types', now),
programId: 'program-business-types',
exercise: restoredExercise!,
position: 0,
setsCount: 1,
enabledMeasures: const {WorkoutMeasure.time},
),
],
);
await programRepository.save(program);
final restoredProgram = await programRepository.findById(
program.metadata.id,
);
expect(
restoredProgram
?.exercises
.single
.healthServicesExerciseTypeStrategySnapshot,
[
HealthServicesExerciseType.running,
HealthServicesExerciseType.highIntensityIntervalTraining,
HealthServicesExerciseType.workout,
],
);
expect(
restoredProgram?.exercises.single
.toSnapshotJson()['healthServicesExerciseTypeStrategy'],
['RUNNING', 'HIGH_INTENSITY_INTERVAL_TRAINING', 'WORKOUT'],
);
final legacyExercise = Exercise(
metadata: _metadata('exercise-business-types-legacy', now),
@ -193,6 +239,12 @@ void main() {
expect(restoredLegacy?.effectiveBusinessTypes, [
BusinessExerciseType.shoot,
]);
expect(restoredLegacy?.healthServicesExerciseTypeStrategy, [
HealthServicesExerciseType.highIntensityIntervalTraining,
HealthServicesExerciseType.running,
HealthServicesExerciseType.walking,
HealthServicesExerciseType.workout,
]);
});
test(
@ -595,6 +647,23 @@ CREATE TABLE pending_share_actions (
],
);
await exerciseRepository.save(updatedExercise);
await programRepository.save(
Program(
metadata: _metadata('program-sync-business-types', now),
name: 'Program business types',
defaultRestSeconds: 45,
exercises: [
ProgramExercise.snapshotFromExercise(
metadata: _metadata('program-exercise-sync-business-types', now),
programId: 'program-sync-business-types',
exercise: updatedExercise,
position: 0,
setsCount: 1,
enabledMeasures: const {WorkoutMeasure.time},
),
],
),
);
final changes = await syncChangeRepository.listPendingChanges();
final payloadsById = {
@ -605,6 +674,13 @@ CREATE TABLE pending_share_actions (
'dribble',
'finishing',
]);
final programExercises =
payloadsById['program-sync-business-types']!['exercises'] as List;
expect(programExercises.single['healthServicesExerciseTypeStrategy'], [
'RUNNING',
'HIGH_INTENSITY_INTERVAL_TRAINING',
'WORKOUT',
]);
});
test('local sync payload includes full workout history aggregate', () async {