Fix red CI: coverage 7.16.2, split workout_screen.dart and modes test
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run

- coverage 7.16.1 -> 7.16.2 (dependency freshness).
- workout_screen.dart (253) -> _allSetsCompleted/_isPaused move into
  workout_screen_breaks.dart next to _stateOf.
- storage_service_modes_test.dart (289) -> setExerciseSettings and
  per-mode progression groups move to storage_service_settings_test.dart.

Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-27 19:06:15 +02:00
parent d35edfc10c
commit cf2d917b33
5 changed files with 150 additions and 132 deletions

View File

@ -2,7 +2,7 @@
# Runtime: tkinter/subprocess/socket/sqlite3 (stdlib) plus gatelock (below).
bandit==1.9.4
codespell==2.4.3
coverage==7.16.1
coverage==7.16.2
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock

View File

@ -172,22 +172,6 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
// ── Helpers ────────────────────────────────────────────────────────────────
/// Every set recorded, not counting paused exercises: those have no sets
/// to tap and are recorded as failed at Finish.
bool get _allSetsCompleted {
for (var i = 0; i < _tapped.length; i++) {
if (!_isPaused(i) && !_tapped[i].every((t) => t)) return false;
}
return true;
}
/// Whether exercise [exIdx] is on an injury pause right now.
bool _isPaused(int exIdx) =>
_exerciseStates[widget.exercises[exIdx].name]?.isPausedAt(
DateTime.now(),
) ??
false;
/// Runs [fn] inside `setState` on behalf of this library's extensions.
///
/// `setState` is `@protected`, so an extension cannot call it directly. This

View File

@ -1,4 +1,4 @@
// Rest-period timers and per-exercise settings edits.
// Rest-period timers, per-exercise state lookups and settings edits.
//
// See workout_screen_session.dart for why this is a `part`. `setState` is
// `@protected` and unreachable from an extension, so these mutate through the
@ -12,6 +12,22 @@ const _expiredBreakGraceSecs = 120;
/// Starting, ticking, cancelling and finishing the rest period between sets.
extension _WorkoutScreenBreaks on _WorkoutScreenState {
/// Every set recorded, not counting paused exercises: those have no sets
/// to tap and are recorded as failed at Finish.
bool get _allSetsCompleted {
for (var i = 0; i < _tapped.length; i++) {
if (!_isPaused(i) && !_tapped[i].every((t) => t)) return false;
}
return true;
}
/// Whether exercise [exIdx] is on an injury pause right now.
bool _isPaused(int exIdx) =>
_exerciseStates[widget.exercises[exIdx].name]?.isPausedAt(
DateTime.now(),
) ??
false;
/// Exercise [exIdx]'s stored state, or its defaults while the states are
/// still loading: that is where its rest lengths live.
ExerciseState _stateOf(int exIdx) {

View File

@ -11,8 +11,6 @@ import 'package:workout_app/services/storage_service.dart';
StorageService get _svc => StorageService.instance;
final _yesterday = DateTime.now().subtract(const Duration(days: 1));
/// Builds a database exactly as a v3 install left it: the v3 schema, a
/// finished workout from today, and a workout in progress.
Future<void> _writeV3Database(String path) async {
@ -122,12 +120,10 @@ void main() {
test('keeps progression and gives every row weight mode', () async {
for (final ex in [...workoutA, ...workoutB]) {
final s = (await _svc.getExerciseState(ex.name))!;
expect((s.weight, s.reps, s.successStreak, s.failStreak), (
17.5,
9,
2,
1,
));
expect(
(s.weight, s.reps, s.successStreak, s.failStreak),
(17.5, 9, 2, 1),
);
expect((s.successThreshold, s.failThreshold), (5, 4));
expect(s.mode, ProgressionMode.weight);
expect((s.repsHigh, s.repsLow), (kDefaultRepsHigh, kDefaultRepsLow));
@ -180,110 +176,4 @@ void main() {
expect(situp.pausedUntil, isNull);
expect(await _svc.getLastWorkoutDate(), DateTime(2026, 9, 27));
});
group('setExerciseSettings', () {
test('persists mode, rep range, warmup and thresholds', () async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(
s.copyWith(
mode: ProgressionMode.doubleProgression,
repsHigh: 15,
repsLow: 8,
hasWarmup: false,
successThreshold: 1,
failThreshold: 4,
),
);
final after = (await _svc.getExerciseState(name))!;
expect(after.mode, ProgressionMode.doubleProgression);
expect((after.repsHigh, after.repsLow), (15, 8));
expect(after.hasWarmup, isFalse);
expect((after.successThreshold, after.failThreshold), (1, 4));
// The target itself is untouched until the next finished workout.
expect((after.weight, after.reps), (s.weight, s.reps));
});
test('persists a pause and its resume', () async {
final s = (await _svc.getExerciseState('Situp'))!;
await _svc.setExerciseSettings(
s.copyWith(pausedUntil: DateTime(2026, 10, 11)),
);
expect(
(await _svc.getExerciseState('Situp'))!.pausedUntil,
DateTime(2026, 10, 11),
);
await _svc.setExerciseSettings(s.copyWith(resume: true));
expect((await _svc.getExerciseState('Situp'))!.pausedUntil, isNull);
});
test('reaches the workout through getCurrentExercises', () async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(s.copyWith(hasWarmup: false));
final exercises = await _svc.getCurrentExercises('A');
expect(exercises.first.hasWarmup, isFalse);
expect(exercises[1].hasWarmup, isTrue);
});
});
group('applyProgression per mode', () {
Future<ExerciseState> run(
ProgressionMode mode, {
required bool success,
required int reps,
DateTime? last,
}) async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(
s.copyWith(mode: mode, successThreshold: 1, failThreshold: 1),
);
await _svc.setExerciseReps(name, reps);
await _svc.setExerciseWeight(name, 10);
await _svc.applyProgression(
succeededExercises: {name: success},
lastWorkoutDate: last ?? _yesterday,
);
return (await _svc.getExerciseState(name))!;
}
test('reps: success adds a rep', () async {
final s = await run(ProgressionMode.reps, success: true, reps: 10);
expect((s.weight, s.reps, s.successStreak), (10, 11, 0));
});
test('reps: failure removes a rep', () async {
final s = await run(ProgressionMode.reps, success: false, reps: 10);
expect((s.weight, s.reps, s.failStreak), (10, 9, 0));
});
test('double: success at n moves up in weight and back to m', () async {
final s = await run(
ProgressionMode.doubleProgression,
success: true,
reps: 12,
);
expect((s.weight, s.reps), (12.5, 6));
});
test('double: failure at m moves down in weight and up to n', () async {
final s = await run(
ProgressionMode.doubleProgression,
success: false,
reps: 6,
);
expect((s.weight, s.reps), (7.5, 12));
});
test('a week off is one regression step in the active mode', () async {
final s = await run(
ProgressionMode.reps,
success: true,
reps: 10,
last: DateTime.now().subtract(const Duration(days: 9)),
);
expect((s.weight, s.reps, s.successStreak, s.failStreak), (10, 9, 0, 0));
});
});
}

View File

@ -0,0 +1,128 @@
// Per-exercise settings writes and progression per mode, through storage.
// Split from storage_service_modes_test.dart for the 250-line cap.
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:workout_app/models/workout_plan.dart';
import 'package:workout_app/services/storage_service.dart';
StorageService get _svc => StorageService.instance;
final _yesterday = DateTime.now().subtract(const Duration(days: 1));
void main() {
setUpAll(() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
});
setUp(() async {
StorageService.resetForTesting();
await StorageService.init();
});
group('setExerciseSettings', () {
test('persists mode, rep range, warmup and thresholds', () async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(
s.copyWith(
mode: ProgressionMode.doubleProgression,
repsHigh: 15,
repsLow: 8,
hasWarmup: false,
successThreshold: 1,
failThreshold: 4,
),
);
final after = (await _svc.getExerciseState(name))!;
expect(after.mode, ProgressionMode.doubleProgression);
expect((after.repsHigh, after.repsLow), (15, 8));
expect(after.hasWarmup, isFalse);
expect((after.successThreshold, after.failThreshold), (1, 4));
// The target itself is untouched until the next finished workout.
expect((after.weight, after.reps), (s.weight, s.reps));
});
test('persists a pause and its resume', () async {
final s = (await _svc.getExerciseState('Situp'))!;
await _svc.setExerciseSettings(
s.copyWith(pausedUntil: DateTime(2026, 10, 11)),
);
expect(
(await _svc.getExerciseState('Situp'))!.pausedUntil,
DateTime(2026, 10, 11),
);
await _svc.setExerciseSettings(s.copyWith(resume: true));
expect((await _svc.getExerciseState('Situp'))!.pausedUntil, isNull);
});
test('reaches the workout through getCurrentExercises', () async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(s.copyWith(hasWarmup: false));
final exercises = await _svc.getCurrentExercises('A');
expect(exercises.first.hasWarmup, isFalse);
expect(exercises[1].hasWarmup, isTrue);
});
});
group('applyProgression per mode', () {
Future<ExerciseState> run(
ProgressionMode mode, {
required bool success,
required int reps,
DateTime? last,
}) async {
final name = workoutA.first.name;
final s = (await _svc.getExerciseState(name))!;
await _svc.setExerciseSettings(
s.copyWith(mode: mode, successThreshold: 1, failThreshold: 1),
);
await _svc.setExerciseReps(name, reps);
await _svc.setExerciseWeight(name, 10);
await _svc.applyProgression(
succeededExercises: {name: success},
lastWorkoutDate: last ?? _yesterday,
);
return (await _svc.getExerciseState(name))!;
}
test('reps: success adds a rep', () async {
final s = await run(ProgressionMode.reps, success: true, reps: 10);
expect((s.weight, s.reps, s.successStreak), (10, 11, 0));
});
test('reps: failure removes a rep', () async {
final s = await run(ProgressionMode.reps, success: false, reps: 10);
expect((s.weight, s.reps, s.failStreak), (10, 9, 0));
});
test('double: success at n moves up in weight and back to m', () async {
final s = await run(
ProgressionMode.doubleProgression,
success: true,
reps: 12,
);
expect((s.weight, s.reps), (12.5, 6));
});
test('double: failure at m moves down in weight and up to n', () async {
final s = await run(
ProgressionMode.doubleProgression,
success: false,
reps: 6,
);
expect((s.weight, s.reps), (7.5, 12));
});
test('a week off is one regression step in the active mode', () async {
final s = await run(
ProgressionMode.reps,
success: true,
reps: 10,
last: DateTime.now().subtract(const Duration(days: 9)),
);
expect((s.weight, s.reps, s.successStreak, s.failStreak), (10, 9, 0, 0));
});
});
}