fix(core): fusionne la strategie Health Services au niveau seance et nullifie sourceExerciseId au pull si absent
Complement #183 : fusion de la strategie Health Services au niveau seance pour eviter que le premier exercice fige tout le choix de strategie sur la seance. Fix #185 : nullification defensive de sourceExerciseId au pull quand l'exercice source est absent, pour ne plus casser le chargement des programmes apres suppression d'un exercice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -54,28 +54,27 @@ 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());
|
||||
test('projects session-level Health Services exercise strategy', () async {
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = _session(
|
||||
healthServicesExerciseTypeStrategy: const ['WORKOUT'],
|
||||
secondExerciseName: 'Dribble',
|
||||
secondExerciseHealthServicesExerciseTypeStrategy: const [
|
||||
'RUNNING',
|
||||
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||
'WORKOUT',
|
||||
],
|
||||
);
|
||||
final projector = _projector(repository, _clock());
|
||||
|
||||
final projection = await projector.project(revision: 1);
|
||||
final projection = await projector.project(revision: 1);
|
||||
|
||||
expect(projection.healthServicesExerciseTypeStrategy, [
|
||||
'RUNNING',
|
||||
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||
'WORKOUT',
|
||||
]);
|
||||
},
|
||||
);
|
||||
expect(projection.healthServicesExerciseTypeStrategy, [
|
||||
'WORKOUT',
|
||||
'RUNNING',
|
||||
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||
]);
|
||||
});
|
||||
|
||||
test('projects running with step timer before stopwatch score', () async {
|
||||
final now = DateTime.utc(2026, 7, 25, 12);
|
||||
@ -611,6 +610,7 @@ ActiveWorkoutSession _session({
|
||||
List<ExerciseStep> steps = const [],
|
||||
String? secondExerciseName,
|
||||
List<String> healthServicesExerciseTypeStrategy = const [],
|
||||
List<String> secondExerciseHealthServicesExerciseTypeStrategy = const [],
|
||||
}) {
|
||||
final exerciseSnapshot = {
|
||||
'id': 'exercise-snapshot-1',
|
||||
@ -642,6 +642,8 @@ ActiveWorkoutSession _session({
|
||||
'scoreInputModeSnapshot': ScoreInputMode.manual.name,
|
||||
'exerciseStepsSnapshot': const [],
|
||||
'autoStartNextTimedStepSnapshot': true,
|
||||
'healthServicesExerciseTypeStrategy':
|
||||
secondExerciseHealthServicesExerciseTypeStrategy,
|
||||
};
|
||||
return ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
|
||||
@ -951,6 +951,60 @@ CREATE TABLE pending_share_actions (
|
||||
expect(template!.tags, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'local sync pull keeps program loadable when source exercise is absent',
|
||||
() async {
|
||||
final now = DateTime.utc(2026, 7, 23, 11);
|
||||
|
||||
final applied = await syncChangeRepository.applyRemoteItem(
|
||||
RemoteSyncedItem(
|
||||
resourceType: SyncResourceType.program,
|
||||
clientId: 'remote-program-missing-exercise',
|
||||
serverId: 'server-program-missing-exercise',
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: now,
|
||||
serverUpdatedAt: now,
|
||||
deletedAt: null,
|
||||
payload: const {
|
||||
'id': 'remote-program-missing-exercise',
|
||||
'name': 'Remote program',
|
||||
'defaultRestSeconds': 30,
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'remote-program-exercise-missing-source',
|
||||
'sourceExerciseId': 'remote-exercise-deleted',
|
||||
'position': 0,
|
||||
'exerciseNameSnapshot': 'Remote deleted exercise',
|
||||
'availableTimeSnapshot': false,
|
||||
'availableRepsSnapshot': true,
|
||||
'availableScoreSnapshot': false,
|
||||
'setsCount': 2,
|
||||
'timeEnabled': false,
|
||||
'repsEnabled': true,
|
||||
'scoreEnabled': false,
|
||||
'targetReps': 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final program = await programRepository.findById(
|
||||
'remote-program-missing-exercise',
|
||||
);
|
||||
|
||||
expect(applied, isTrue);
|
||||
expect(program, isNotNull);
|
||||
expect(program!.exercises, hasLength(1));
|
||||
expect(program.exercises.single.sourceExerciseId, isNull);
|
||||
expect(
|
||||
program.exercises.single.exerciseNameSnapshot,
|
||||
'Remote deleted exercise',
|
||||
);
|
||||
expect(program.exercises.single.targetReps, 15);
|
||||
},
|
||||
);
|
||||
|
||||
test('local backup export includes tags and full workout history', () async {
|
||||
final now = DateTime.utc(2026, 7, 22, 10);
|
||||
await exerciseRepository.save(
|
||||
@ -1644,6 +1698,58 @@ CREATE TABLE pending_share_actions (
|
||||
expect(await exerciseRepository.findById(exercise.metadata.id), isNotNull);
|
||||
});
|
||||
|
||||
test('program remains loadable after source exercise deletion', () async {
|
||||
final now = DateTime.utc(2026, 7, 23, 9);
|
||||
final exercise = Exercise(
|
||||
metadata: _metadata('exercise-deleted-source', now),
|
||||
name: 'Tirs en course',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 8,
|
||||
);
|
||||
await exerciseRepository.save(exercise);
|
||||
await programRepository.save(
|
||||
Program(
|
||||
metadata: _metadata('program-deleted-source', now),
|
||||
name: 'Programme source supprimée',
|
||||
defaultRestSeconds: 30,
|
||||
exercises: [
|
||||
ProgramExercise.snapshotFromExercise(
|
||||
metadata: _metadata('program-exercise-deleted-source', now),
|
||||
programId: 'program-deleted-source',
|
||||
exercise: exercise,
|
||||
position: 0,
|
||||
setsCount: 3,
|
||||
enabledMeasures: const {WorkoutMeasure.reps},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await exerciseRepository.save(
|
||||
exercise.copyWith(
|
||||
metadata: exercise.metadata.markDeleted(
|
||||
now.add(const Duration(minutes: 1)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final programs = await programRepository.listActive();
|
||||
|
||||
expect(programs, hasLength(1));
|
||||
expect(programs.single.name, 'Programme source supprimée');
|
||||
expect(
|
||||
programs.single.exercises.single.sourceExerciseId,
|
||||
exercise.metadata.id,
|
||||
);
|
||||
expect(
|
||||
programs.single.exercises.single.exerciseNameSnapshot,
|
||||
'Tirs en course',
|
||||
);
|
||||
expect(programs.single.exercises.single.targetReps, 8);
|
||||
});
|
||||
|
||||
test(
|
||||
'starter program and workout template snapshot exercise steps',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user