merge(main): validation auto-enchaînement configurable des chronos d'étapes (ticket #79)

Fusionne feature/step-chaining-79-qa — analyze propre (mêmes infos
préexistantes), 154/154 tests verts, build APK debug validé. Aucun
bug de production trouvé en QA, couverture de test supplémentaire
uniquement (reprise après kill d'app, enchaînement désactivé).
Clôture le chantier "auto-enchaînement des chronos d'étapes" (#73,
tickets #75 à #79).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 18:39:31 +02:00
4 changed files with 79 additions and 9 deletions

View File

@ -1,6 +1,6 @@
--- ---
issueRef: "#78" issueRef: "#78"
version: 1 version: 2
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784547239512 updatedAt: 1784549136530
--- ---

View File

@ -2,15 +2,15 @@
id: "e7769413-3d3d-48af-b55e-361c69aef4b2" id: "e7769413-3d3d-48af-b55e-361c69aef4b2"
number: 78 number: 78
title: "[DevFrontend] État d'exécution Chrono suivant prêt" title: "[DevFrontend] État d'exécution Chrono suivant prêt"
status: "open" status: "qa"
priority: "medium" priority: "medium"
sprint: null sprint: null
links: [{"target":"#73","kind":"relatesTo"},{"target":"#76","kind":"dependsOn"},{"target":"#77","kind":"dependsOn"}] links: [{"target":"#73","kind":"relatesTo"},{"target":"#76","kind":"dependsOn"},{"target":"#77","kind":"dependsOn"}]
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}] agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784547239512 createdAt: 1784547239512
updatedAt: 1784547239512 updatedAt: 1784549136530
version: 1 version: 2
--- ---
Adapter l'écran d'exécution des étapes pour afficher `Chrono suivant prêt` quand une étape Temps est prête après une étape Temps et que l'auto-enchaînement effectif est désactivé. Réutiliser `stoppedTimer` côté domaine; l'UI dérive le libellé/bouton depuis l'étape courante, le résultat précédent et la valeur effective. Bouton `Démarrer le chrono`, pas de pause séance, pas de rouge/erreur. Après kill/reprise, l'écran doit revenir à cet état sans démarrage automatique. Adapter l'écran d'exécution des étapes pour afficher `Chrono suivant prêt` quand une étape Temps est prête après une étape Temps et que l'auto-enchaînement effectif est désactivé. Réutiliser `stoppedTimer` côté domaine; l'UI dérive le libellé/bouton depuis l'étape courante, le résultat précédent et la valeur effective. Bouton `Démarrer le chrono`, pas de pause séance, pas de rouge/erreur. Après kill/reprise, l'écran doit revenir à cet état sans démarrage automatique.

View File

@ -883,13 +883,13 @@
"issueRef": "#78", "issueRef": "#78",
"path": "78", "path": "78",
"title": "[DevFrontend] État d'exécution Chrono suivant prêt", "title": "[DevFrontend] État d'exécution Chrono suivant prêt",
"status": "open", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": null, "sprint": null,
"assignedAgentIds": [ "assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d" "9933c93a-b8a1-4164-a3bb-7063fdad747d"
], ],
"updatedAt": 1784547239512 "updatedAt": 1784549136530
}, },
{ {
"issueRef": "#79", "issueRef": "#79",

View File

@ -1281,6 +1281,76 @@ void main() {
}, },
); );
test(
'step chaining disabled stops at first elapsed timed boundary after kill',
() async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
setsCount: 1,
targetReps: 1,
autoStartNextTimedStepSnapshot: false,
exerciseSteps: [
..._twoTimedSteps(),
_step(
id: 'step-3',
position: 2,
name: 'Centre',
type: ExerciseStepType.time,
defaultTargetValue: 1,
),
],
);
final repository = _FakeActiveSessionRepository()..session = session;
await ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
).startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
await ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
).startTimer(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
clock.value = clock.value.add(const Duration(milliseconds: 4500));
final reconstructed = ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final view = await reconstructed.startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
expect(view.state.status, ActiveExerciseStepProgressStatus.stoppedTimer);
expect(view.state.currentStepIndex, 1);
expect(view.state.startedAt, isNull);
expect(view.state.accumulatedMs, 0);
expect(repository.stepResults, hasLength(1));
expect(repository.stepResults.single.stepIndex, 0);
},
);
test( test(
'step chaining resolution uses program override before exercise snapshot', 'step chaining resolution uses program override before exercise snapshot',
() async { () async {