fix(qa): corrections issues de la passe QA fonctionnelle

Corrige les bugs identifiés lors de la QA finale sur use_cases.dart et
drift_repositories.dart, ajoute la couverture de tests d'infrastructure
manquante (test/infrastructure/drift_repositories_test.dart).
flutter analyze propre, 21/21 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:28:38 +02:00
parent 9d91d66820
commit 7b6d21450b
4 changed files with 399 additions and 1 deletions

View File

@ -458,11 +458,17 @@ final class WorkoutTemplateUseCases {
}
final templateOverrides = <WorkoutTemplateExerciseOverride>[];
for (final input in overrides) {
final programInput = programs.firstWhere(
(program) => program.clientKey == input.workoutTemplateProgramClientKey,
orElse: () =>
throw const DomainException('Workout template program not found.'),
);
final workoutTemplateProgramId =
programIdsByClientKey[input.workoutTemplateProgramClientKey];
if (workoutTemplateProgramId == null) {
throw const DomainException('Workout template program not found.');
}
_validateOverrideTargets(programInput.programSnapshotJson, input);
templateOverrides.add(
WorkoutTemplateExerciseOverride(
metadata:
@ -833,6 +839,14 @@ final class CloseWorkoutSessionUseCase {
final now = clock.now();
final results = await sessionRepository.listSetResults(sessionId);
final historyId = ids.newId();
final historyResults = _historyResultsFromActiveResults(
historyId: historyId,
results: results,
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
now: now,
ids: ids,
originDeviceId: originDeviceId,
);
final history = WorkoutHistory(
metadata: EntityMetadata(
id: historyId,
@ -867,6 +881,7 @@ final class CloseWorkoutSessionUseCase {
)
.toList(),
}),
results: historyResults,
);
await historyRepository.save(history);
return history;
@ -886,6 +901,142 @@ final class WorkoutHistoryUseCases {
Future<void> delete(String id) => repository.delete(id, clock.now());
}
void _validateOverrideTargets(
String programSnapshotJson,
WorkoutTemplateExerciseOverrideConfig input,
) {
final snapshot = jsonDecode(programSnapshotJson) as Map<String, dynamic>;
final exercises = (snapshot['exercises'] as List<dynamic>? ?? const [])
.cast<Map<String, dynamic>>();
final exercise = exercises.cast<Map<String, dynamic>?>().firstWhere(
(item) =>
item?['id'] == input.snapshotProgramExerciseId ||
item?['snapshotProgramExerciseId'] == input.snapshotProgramExerciseId,
orElse: () => null,
);
if (exercise == null) {
throw const DomainException('Snapshot exercise not found.');
}
if (input.targetTimeSecondsOverride != null &&
exercise['timeEnabled'] != true) {
throw const DomainException('Cannot override inactive time target.');
}
if (input.targetRepsOverride != null && exercise['repsEnabled'] != true) {
throw const DomainException('Cannot override inactive reps target.');
}
if (input.targetScoreOverride != null && exercise['scoreEnabled'] != true) {
throw const DomainException('Cannot override inactive score target.');
}
}
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
required String historyId,
required List<ActiveSetResult> results,
required String resolvedTemplateSnapshotJson,
required DateTime now,
required IdGenerator ids,
required String originDeviceId,
}) {
final snapshots = _exerciseSnapshotsById(resolvedTemplateSnapshotJson);
return results.map((result) {
final snapshot = snapshots[result.exerciseSnapshotId];
return WorkoutHistorySetResult(
metadata: _newMetadata(ids, originDeviceId, now),
workoutHistoryId: historyId,
programSnapshotId: result.programSnapshotId,
exerciseSnapshotId: result.exerciseSnapshotId,
programIndex: result.programIndex,
exerciseIndex: result.exerciseIndex,
setIndex: result.setIndex,
programNameSnapshot:
snapshot?.programNameSnapshot ?? result.programSnapshotId,
exerciseNameSnapshot:
snapshot?.exerciseNameSnapshot ?? result.exerciseSnapshotId,
timeEnabledSnapshot: snapshot?.timeEnabled ?? result.actualTimeMs != null,
repsEnabledSnapshot: snapshot?.repsEnabled ?? result.actualReps != null,
scoreEnabledSnapshot:
snapshot?.scoreEnabled ?? result.actualScore != null,
targetTimeSecondsSnapshot: snapshot?.targetTimeSeconds,
targetRepsSnapshot: snapshot?.targetReps,
targetScoreSnapshot: snapshot?.targetScore,
actualTimeMs: result.actualTimeMs,
actualReps: result.actualReps,
actualScore: result.actualScore,
scoreLabelSnapshot:
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
scoreUnitSnapshot:
result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot,
startedAt: result.startedAt,
completedAt: result.completedAt,
);
}).toList();
}
Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
String resolvedTemplateSnapshotJson,
) {
final decoded =
jsonDecode(resolvedTemplateSnapshotJson) as Map<String, dynamic>;
final programs = (decoded['programs'] as List<dynamic>? ?? const []);
final snapshots = <String, _ResolvedExerciseSnapshot>{};
for (final program in programs.cast<Map<String, dynamic>>()) {
final programName = program['programNameSnapshot'] as String? ?? '';
final programSnapshotJson = program['programSnapshotJson'] as String?;
if (programSnapshotJson == null) {
continue;
}
final programSnapshot =
jsonDecode(programSnapshotJson) as Map<String, dynamic>;
final exercises =
(programSnapshot['exercises'] as List<dynamic>? ?? const []);
for (final exercise in exercises.cast<Map<String, dynamic>>()) {
final id = exercise['id'] as String?;
if (id == null) {
continue;
}
snapshots[id] = _ResolvedExerciseSnapshot(
programNameSnapshot: programName,
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
timeEnabled: exercise['timeEnabled'] == true,
repsEnabled: exercise['repsEnabled'] == true,
scoreEnabled: exercise['scoreEnabled'] == true,
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
targetReps: exercise['targetReps'] as int?,
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
);
}
}
return snapshots;
}
final class _ResolvedExerciseSnapshot {
const _ResolvedExerciseSnapshot({
required this.programNameSnapshot,
required this.exerciseNameSnapshot,
required this.timeEnabled,
required this.repsEnabled,
required this.scoreEnabled,
this.targetTimeSeconds,
this.targetReps,
this.targetScore,
this.scoreLabelSnapshot,
this.scoreUnitSnapshot,
});
final String programNameSnapshot;
final String exerciseNameSnapshot;
final bool timeEnabled;
final bool repsEnabled;
final bool scoreEnabled;
final int? targetTimeSeconds;
final int? targetReps;
final double? targetScore;
final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot;
}
EntityMetadata _newMetadata(
IdGenerator ids,
String originDeviceId,

View File

@ -530,7 +530,7 @@ domain.EntityMetadata _metadataFromRow(dynamic row) {
);
}
List<Value<Object?>> _metadataValues(domain.EntityMetadata metadata) => [
List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
Value(metadata.id),
Value(metadata.createdAt),
Value(metadata.updatedAt),