fix(persistence): correctifs post-QA sur les repositories (ticket #13)
Corrige les ports, use cases, entités et l'implémentation Drift des repositories suite aux retours de QA. flutter analyze propre, 24/24 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -80,6 +80,7 @@ abstract interface class ActiveSessionRepository {
|
||||
Future<void> save(ActiveWorkoutSession session);
|
||||
Future<void> saveSetResult(ActiveSetResult result);
|
||||
Future<void> saveRestState(ActiveRestState restState);
|
||||
Future<ActiveRestState?> findRestStateById(String id);
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId);
|
||||
}
|
||||
|
||||
@ -803,6 +803,50 @@ final class ActiveWorkoutSessionUseCases {
|
||||
return rest;
|
||||
}
|
||||
|
||||
Future<ActiveRestState> adjustRestSeconds({
|
||||
required String restStateId,
|
||||
required int deltaSeconds,
|
||||
}) async {
|
||||
final rest = await sessionRepository.findRestStateById(restStateId);
|
||||
if (rest == null) {
|
||||
throw const DomainException('Active rest state not found.');
|
||||
}
|
||||
final now = clock.now();
|
||||
final adjusted = rest.copyWith(
|
||||
metadata: rest.metadata.touch(now),
|
||||
adjustedRestSeconds: (rest.adjustedRestSeconds + deltaSeconds).clamp(
|
||||
0,
|
||||
1 << 31,
|
||||
),
|
||||
);
|
||||
await sessionRepository.saveRestState(adjusted);
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
Future<ActiveRestState> skipRest({required String restStateId}) async {
|
||||
final rest = await sessionRepository.findRestStateById(restStateId);
|
||||
if (rest == null) {
|
||||
throw const DomainException('Active rest state not found.');
|
||||
}
|
||||
final now = clock.now();
|
||||
final skipped = rest.copyWith(
|
||||
metadata: rest.metadata.touch(now),
|
||||
skippedAt: now,
|
||||
);
|
||||
await sessionRepository.saveRestState(skipped);
|
||||
return skipped;
|
||||
}
|
||||
|
||||
Future<ActiveRestState?> findActiveRest({required String sessionId}) async {
|
||||
final restStates = await sessionRepository.listRestStates(sessionId);
|
||||
final active =
|
||||
restStates
|
||||
.where((rest) => rest.endedAt == null && rest.skippedAt == null)
|
||||
.toList()
|
||||
..sort((left, right) => right.startedAt.compareTo(left.startedAt));
|
||||
return active.isEmpty ? null : active.first;
|
||||
}
|
||||
|
||||
Future<ActiveWorkoutSession> _requiredSession(String sessionId) async {
|
||||
final session = await sessionRepository.findById(sessionId);
|
||||
if (session == null) {
|
||||
|
||||
@ -631,6 +631,28 @@ final class ActiveRestState {
|
||||
final DateTime startedAt;
|
||||
final DateTime? endedAt;
|
||||
final DateTime? skippedAt;
|
||||
|
||||
ActiveRestState copyWith({
|
||||
EntityMetadata? metadata,
|
||||
int? adjustedRestSeconds,
|
||||
Object? endedAt = _unchanged,
|
||||
Object? skippedAt = _unchanged,
|
||||
}) {
|
||||
return ActiveRestState(
|
||||
metadata: metadata ?? this.metadata,
|
||||
activeWorkoutSessionId: activeWorkoutSessionId,
|
||||
afterProgramIndex: afterProgramIndex,
|
||||
afterExerciseIndex: afterExerciseIndex,
|
||||
afterSetIndex: afterSetIndex,
|
||||
plannedRestSeconds: plannedRestSeconds,
|
||||
adjustedRestSeconds: adjustedRestSeconds ?? this.adjustedRestSeconds,
|
||||
startedAt: startedAt,
|
||||
endedAt: endedAt == _unchanged ? this.endedAt : endedAt as DateTime?,
|
||||
skippedAt: skippedAt == _unchanged
|
||||
? this.skippedAt
|
||||
: skippedAt as DateTime?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkoutHistory {
|
||||
|
||||
@ -398,6 +398,14 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
.insertOnConflictUpdate(_activeRestStateCompanion(restState));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveRestState?> findRestStateById(String id) async {
|
||||
final row = await (database.select(
|
||||
database.activeRestStates,
|
||||
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _activeRestStateFromRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
final rows =
|
||||
|
||||
@ -111,6 +111,104 @@ void main() {
|
||||
clock.value = clock.value.add(const Duration(seconds: 15));
|
||||
expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 45000);
|
||||
});
|
||||
|
||||
test('adjustRestSeconds persists adjusted rest duration', () async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..restStates['rest-1'] = ActiveRestState(
|
||||
metadata: _metadata('rest-1'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 0,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now(),
|
||||
);
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
final adjusted = await useCase.adjustRestSeconds(
|
||||
restStateId: 'rest-1',
|
||||
deltaSeconds: -75,
|
||||
);
|
||||
|
||||
expect(adjusted.adjustedRestSeconds, 0);
|
||||
expect(repository.restStates['rest-1']!.adjustedRestSeconds, 0);
|
||||
});
|
||||
|
||||
test('skipRest persists skippedAt timestamp', () async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..restStates['rest-1'] = ActiveRestState(
|
||||
metadata: _metadata('rest-1'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 0,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now(),
|
||||
);
|
||||
clock.value = clock.value.add(const Duration(seconds: 10));
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
final skipped = await useCase.skipRest(restStateId: 'rest-1');
|
||||
|
||||
expect(skipped.skippedAt, clock.now());
|
||||
expect(repository.restStates['rest-1']!.skippedAt, clock.now());
|
||||
});
|
||||
|
||||
test('findActiveRest returns latest non-ended non-skipped rest', () async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..restStates['ended'] = ActiveRestState(
|
||||
metadata: _metadata('ended'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 0,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now(),
|
||||
endedAt: clock.now().add(const Duration(seconds: 60)),
|
||||
)
|
||||
..restStates['older-active'] = ActiveRestState(
|
||||
metadata: _metadata('older-active'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 1,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now().add(const Duration(seconds: 30)),
|
||||
)
|
||||
..restStates['latest-active'] = ActiveRestState(
|
||||
metadata: _metadata('latest-active'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 2,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now().add(const Duration(seconds: 90)),
|
||||
)
|
||||
..restStates['skipped'] = ActiveRestState(
|
||||
metadata: _metadata('skipped'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
afterProgramIndex: 0,
|
||||
afterExerciseIndex: 3,
|
||||
afterSetIndex: 0,
|
||||
plannedRestSeconds: 60,
|
||||
adjustedRestSeconds: 60,
|
||||
startedAt: clock.now().add(const Duration(seconds: 120)),
|
||||
skippedAt: clock.now().add(const Duration(seconds: 125)),
|
||||
);
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
final active = await useCase.findActiveRest(sessionId: 'session-1');
|
||||
|
||||
expect(active?.metadata.id, 'latest-active');
|
||||
});
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id) {
|
||||
@ -181,3 +279,51 @@ final class _FakeProgramRepository implements ProgramRepository {
|
||||
@override
|
||||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||
}
|
||||
|
||||
ActiveWorkoutSessionUseCases _activeUseCase(
|
||||
_FakeActiveSessionRepository repository,
|
||||
_FakeClock clock,
|
||||
) {
|
||||
return ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: repository,
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
final restStates = <String, ActiveRestState>{};
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return restStates.values
|
||||
.where((rest) => rest.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async =>
|
||||
const [];
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {
|
||||
restStates[restState.metadata.id] = restState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
}
|
||||
|
||||
@ -284,6 +284,9 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
|
||||
@ -230,6 +230,9 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||||
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
|
||||
Reference in New Issue
Block a user