chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)
Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -4,7 +4,12 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gametime/application/application.dart';
|
||||
import 'package:gametime/domain/domain.dart';
|
||||
import 'package:gametime/infrastructure/infrastructure.dart'
|
||||
hide WorkoutHistory, WorkoutHistorySetResult, WorkoutHistoryStepResult;
|
||||
hide
|
||||
WorkoutHistory,
|
||||
WorkoutHistorySetResult,
|
||||
WorkoutHistoryStepResult,
|
||||
WorkoutTelemetryAggregate,
|
||||
WorkoutTelemetrySample;
|
||||
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||||
|
||||
void main() {
|
||||
@ -221,6 +226,48 @@ void main() {
|
||||
await adapter.stop();
|
||||
await sensorUseCases.dispose();
|
||||
});
|
||||
|
||||
test('persists telemetry samples from the native stream', () async {
|
||||
final native = _FakeWatchBridgeNativeChannel();
|
||||
final source = _FakeProjectionSource(_runningProjection(revision: 1));
|
||||
final telemetryRepository = _FakeWorkoutTelemetryRepository();
|
||||
final adapter = _adapter(
|
||||
native: native,
|
||||
source: source,
|
||||
telemetryUseCases: WorkoutTelemetryUseCases(
|
||||
repository: telemetryRepository,
|
||||
clock: _FakeClock(_now),
|
||||
ids: _FakeIds(),
|
||||
),
|
||||
);
|
||||
await adapter.start();
|
||||
|
||||
native.emitSensorSample(
|
||||
WatchSensorSample(
|
||||
sampleId: 'sample-1',
|
||||
sessionId: 'session-1',
|
||||
recordedAtEpochMs: _now.millisecondsSinceEpoch,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
stepIndex: 0,
|
||||
heartRateBpm: 120,
|
||||
distanceMeters: 500,
|
||||
caloriesKcal: 42,
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(telemetryRepository.samples.single.id, 'sample-1');
|
||||
final aggregate = telemetryRepository.aggregates.singleWhere(
|
||||
(aggregate) => aggregate.scope == WorkoutTelemetryAggregateScope.session,
|
||||
);
|
||||
expect(aggregate.sampleCount, 1);
|
||||
expect(aggregate.minHeartRateBpm, 120);
|
||||
expect(aggregate.totalDistanceMeters, 500);
|
||||
expect(aggregate.totalCaloriesKcal, 42);
|
||||
await adapter.stop();
|
||||
});
|
||||
}
|
||||
|
||||
WatchWearDataLayerAdapter _adapter({
|
||||
@ -229,6 +276,7 @@ WatchWearDataLayerAdapter _adapter({
|
||||
required _FakeProjectionSource source,
|
||||
WorkoutHistoryUseCases? historyUseCases,
|
||||
ActiveWorkoutSensorUseCases? sensorUseCases,
|
||||
WorkoutTelemetryUseCases? telemetryUseCases,
|
||||
Duration heartbeatInterval = const Duration(seconds: 5),
|
||||
}) {
|
||||
return WatchWearDataLayerAdapter(
|
||||
@ -237,6 +285,7 @@ WatchWearDataLayerAdapter _adapter({
|
||||
projectionSource: source,
|
||||
workoutHistoryUseCases: historyUseCases,
|
||||
activeWorkoutSensorUseCases: sensorUseCases,
|
||||
workoutTelemetryUseCases: telemetryUseCases,
|
||||
projectionRefreshInterval: heartbeatInterval,
|
||||
);
|
||||
}
|
||||
@ -312,6 +361,16 @@ final class _FakeClock implements Clock {
|
||||
DateTime now() => value;
|
||||
}
|
||||
|
||||
final class _FakeIds implements IdGenerator {
|
||||
var _next = 0;
|
||||
|
||||
@override
|
||||
String newId() {
|
||||
_next += 1;
|
||||
return 'id-$_next';
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeProjectionSource implements WatchProjectionSource {
|
||||
_FakeProjectionSource(this.current);
|
||||
|
||||
@ -466,8 +525,11 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
@override
|
||||
Future<void> patchHeartRateSummary({
|
||||
required String historyId,
|
||||
int? minHeartRateBpm,
|
||||
required double averageHeartRateBpm,
|
||||
required int maxHeartRateBpm,
|
||||
double? totalDistanceMeters,
|
||||
double? totalCaloriesKcal,
|
||||
required DateTime patchedAt,
|
||||
}) async {
|
||||
final index = histories.indexWhere(
|
||||
@ -497,6 +559,72 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
Future<void> delete(String id, DateTime deletedAt) async {}
|
||||
}
|
||||
|
||||
final class _FakeWorkoutTelemetryRepository
|
||||
implements WorkoutTelemetryRepository {
|
||||
final samples = <WorkoutTelemetrySample>[];
|
||||
final aggregates = <WorkoutTelemetryAggregate>[];
|
||||
|
||||
@override
|
||||
Future<bool> saveSample(WorkoutTelemetrySample sample) async {
|
||||
if (samples.any((existing) => existing.id == sample.id)) {
|
||||
return false;
|
||||
}
|
||||
samples.add(sample);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<WorkoutTelemetrySample>> listSamples(String sessionId) async {
|
||||
return samples
|
||||
.where((sample) => sample.sessionId == sessionId)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> replaceAggregatesForSession({
|
||||
required String sessionId,
|
||||
required List<WorkoutTelemetryAggregate> aggregates,
|
||||
}) async {
|
||||
this.aggregates.removeWhere(
|
||||
(aggregate) => aggregate.sessionId == sessionId,
|
||||
);
|
||||
this.aggregates.addAll(aggregates);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<WorkoutTelemetryAggregate>> listAggregates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return aggregates
|
||||
.where((aggregate) => aggregate.sessionId == sessionId)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WorkoutTelemetryAggregate?> findAggregate({
|
||||
required String sessionId,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
int? programIndex,
|
||||
int? exerciseIndex,
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) async {
|
||||
return aggregates
|
||||
.where(
|
||||
(aggregate) =>
|
||||
aggregate.sessionId == sessionId &&
|
||||
aggregate.scope == scope &&
|
||||
aggregate.programIndex == programIndex &&
|
||||
aggregate.exerciseIndex == exerciseIndex &&
|
||||
aggregate.setIndex == setIndex &&
|
||||
aggregate.passageIndex == passageIndex &&
|
||||
aggregate.stepIndex == stepIndex,
|
||||
)
|
||||
.firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
final class _SentAck {
|
||||
const _SentAck(this.command, this.ack, this.revisionAtAck);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user