feat(core): consolide modele donnees stats par scope, types metier d'exercice et correctif contrainte active_exercise_step_progress_states
Couche domaine/application/infra locale partagee par #179 (stockage stats par etape/serie/exercice/seance), #182 (correctif contrainte UNIQUE active_exercise_step_progress_states) et #183 (modele/persistance types metier d'exercice). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -3324,22 +3324,6 @@ void main() {
|
||||
),
|
||||
);
|
||||
final duplicate = await useCase.recordTelemetrySample(
|
||||
WatchTelemetrySample(
|
||||
sampleId: 'sample-1',
|
||||
sessionId: 'session-1',
|
||||
capturedAtEpochMs: DateTime.utc(
|
||||
2026,
|
||||
7,
|
||||
28,
|
||||
10,
|
||||
1,
|
||||
).millisecondsSinceEpoch,
|
||||
heartRateBpm: 160,
|
||||
distanceMeters: 300,
|
||||
caloriesKcal: 10,
|
||||
),
|
||||
);
|
||||
final second = await useCase.recordTelemetrySample(
|
||||
WatchTelemetrySample(
|
||||
sampleId: 'sample-2',
|
||||
sessionId: 'session-1',
|
||||
@ -3348,7 +3332,42 @@ void main() {
|
||||
7,
|
||||
28,
|
||||
10,
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
).millisecondsSinceEpoch,
|
||||
heartRateBpm: 160,
|
||||
distanceMeters: 530,
|
||||
caloriesKcal: 45,
|
||||
),
|
||||
);
|
||||
final olderInSameBucket = await useCase.recordTelemetrySample(
|
||||
WatchTelemetrySample(
|
||||
sampleId: 'sample-older',
|
||||
sessionId: 'session-1',
|
||||
capturedAtEpochMs: DateTime.utc(
|
||||
2026,
|
||||
7,
|
||||
28,
|
||||
10,
|
||||
0,
|
||||
5,
|
||||
).millisecondsSinceEpoch,
|
||||
heartRateBpm: 130,
|
||||
distanceMeters: 510,
|
||||
caloriesKcal: 43,
|
||||
),
|
||||
);
|
||||
final second = await useCase.recordTelemetrySample(
|
||||
WatchTelemetrySample(
|
||||
sampleId: 'sample-3',
|
||||
sessionId: 'session-1',
|
||||
capturedAtEpochMs: DateTime.utc(
|
||||
2026,
|
||||
7,
|
||||
28,
|
||||
10,
|
||||
0,
|
||||
20,
|
||||
).millisecondsSinceEpoch,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
@ -3367,17 +3386,19 @@ void main() {
|
||||
WorkoutTelemetryAggregateScope.set,
|
||||
WorkoutTelemetryAggregateScope.step,
|
||||
]);
|
||||
expect(duplicate, isEmpty);
|
||||
expect(duplicate, isNotEmpty);
|
||||
expect(olderInSameBucket, isEmpty);
|
||||
expect(repository.samples, hasLength(2));
|
||||
expect(repository.samples.first.heartRateBpm, 160);
|
||||
|
||||
final sessionAggregate = second.singleWhere(
|
||||
(aggregate) =>
|
||||
aggregate.scope == WorkoutTelemetryAggregateScope.session,
|
||||
);
|
||||
expect(sessionAggregate.sampleCount, 2);
|
||||
expect(sessionAggregate.minHeartRateBpm, 120);
|
||||
expect(sessionAggregate.averageHeartRateBpm, 135);
|
||||
expect(sessionAggregate.maxHeartRateBpm, 150);
|
||||
expect(sessionAggregate.minHeartRateBpm, 150);
|
||||
expect(sessionAggregate.averageHeartRateBpm, 155);
|
||||
expect(sessionAggregate.maxHeartRateBpm, 160);
|
||||
expect(sessionAggregate.totalDistanceMeters, 620);
|
||||
expect(sessionAggregate.totalCaloriesKcal, 48);
|
||||
expect(
|
||||
@ -3394,6 +3415,79 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'WorkoutTelemetryUseCases reads graph samples by scope with relative cumulative metrics',
|
||||
() async {
|
||||
final repository = _FakeWorkoutTelemetryRepository();
|
||||
final useCase = WorkoutTelemetryUseCases(
|
||||
repository: repository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 28, 10)),
|
||||
ids: _FakeIds(),
|
||||
);
|
||||
final history = WorkoutHistory(
|
||||
metadata: _metadata('history-telemetry'),
|
||||
nameSnapshot: 'Séance',
|
||||
startedAt: DateTime.utc(2026, 7, 28, 10),
|
||||
endedAt: DateTime.utc(2026, 7, 28, 11),
|
||||
totalActiveMs: 3600000,
|
||||
completed: true,
|
||||
historySnapshotJson: jsonEncode({
|
||||
'telemetrySamples': [
|
||||
{
|
||||
'id': 'sample-1',
|
||||
'sessionId': 'session-1',
|
||||
'capturedAt': DateTime.utc(2026, 7, 28, 10).toIso8601String(),
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'stepIndex': 0,
|
||||
'heartRateBpm': 120,
|
||||
'distanceMeters': 500,
|
||||
'caloriesKcal': 42,
|
||||
},
|
||||
{
|
||||
'id': 'sample-2',
|
||||
'sessionId': 'session-1',
|
||||
'capturedAt': DateTime.utc(2026, 7, 28, 10, 3).toIso8601String(),
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'stepIndex': 0,
|
||||
'heartRateBpm': 150,
|
||||
'distanceMeters': 620,
|
||||
'caloriesKcal': 48,
|
||||
},
|
||||
{
|
||||
'id': 'sample-other',
|
||||
'sessionId': 'session-1',
|
||||
'capturedAt': DateTime.utc(2026, 7, 28, 10, 4).toIso8601String(),
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 1,
|
||||
'heartRateBpm': 90,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
final series = await useCase.readGraphSeriesForHistory(
|
||||
history: history,
|
||||
scope: WorkoutTelemetryAggregateScope.step,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
stepIndex: 0,
|
||||
);
|
||||
|
||||
expect(series.points, hasLength(2));
|
||||
expect(series.minHeartRateBpm, 120);
|
||||
expect(series.maxHeartRateBpm, 150);
|
||||
expect(series.points.first.distanceMeters, 0);
|
||||
expect(series.points.last.distanceMeters, 120);
|
||||
expect(series.points.last.caloriesKcal, 6);
|
||||
expect(series.points.last.startsAfterGap, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'WorkoutHistoryUseCases ignores insufficient heart rate summary',
|
||||
() async {
|
||||
@ -5309,10 +5403,15 @@ final class _FakeWorkoutTelemetryRepository
|
||||
|
||||
@override
|
||||
Future<bool> saveSample(WorkoutTelemetrySample sample) async {
|
||||
if (samples.any((existing) => existing.id == sample.id)) {
|
||||
final index = samples.indexWhere((existing) => existing.id == sample.id);
|
||||
if (index != -1 && !sample.capturedAt.isAfter(samples[index].capturedAt)) {
|
||||
return false;
|
||||
}
|
||||
samples.add(sample);
|
||||
if (index == -1) {
|
||||
samples.add(sample);
|
||||
} else {
|
||||
samples[index] = sample;
|
||||
}
|
||||
samples.sort((left, right) => left.capturedAt.compareTo(right.capturedAt));
|
||||
return true;
|
||||
}
|
||||
@ -5324,6 +5423,33 @@ final class _FakeWorkoutTelemetryRepository
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<WorkoutTelemetrySample>> listSamplesForScope({
|
||||
required String sessionId,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
int? programIndex,
|
||||
int? exerciseIndex,
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) async {
|
||||
return samples
|
||||
.where(
|
||||
(sample) =>
|
||||
sample.sessionId == sessionId &&
|
||||
_fakeTelemetrySampleMatchesScope(
|
||||
sample,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> replaceAggregatesForSession({
|
||||
required String sessionId,
|
||||
@ -5369,6 +5495,33 @@ final class _FakeWorkoutTelemetryRepository
|
||||
}
|
||||
}
|
||||
|
||||
bool _fakeTelemetrySampleMatchesScope(
|
||||
WorkoutTelemetrySample sample, {
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
int? programIndex,
|
||||
int? exerciseIndex,
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) {
|
||||
return switch (scope) {
|
||||
WorkoutTelemetryAggregateScope.session => true,
|
||||
WorkoutTelemetryAggregateScope.exercise =>
|
||||
sample.programIndex == programIndex &&
|
||||
sample.exerciseIndex == exerciseIndex,
|
||||
WorkoutTelemetryAggregateScope.set =>
|
||||
sample.programIndex == programIndex &&
|
||||
sample.exerciseIndex == exerciseIndex &&
|
||||
sample.setIndex == setIndex,
|
||||
WorkoutTelemetryAggregateScope.step =>
|
||||
sample.programIndex == programIndex &&
|
||||
sample.exerciseIndex == exerciseIndex &&
|
||||
sample.setIndex == setIndex &&
|
||||
(passageIndex == null || sample.passageIndex == passageIndex) &&
|
||||
sample.stepIndex == stepIndex,
|
||||
};
|
||||
}
|
||||
|
||||
ActiveWorkoutSessionUseCases _activeUseCase(
|
||||
_FakeActiveSessionRepository repository,
|
||||
_FakeClock clock,
|
||||
|
||||
Reference in New Issue
Block a user