Persist the exploration, proposal, spec, design, tasks, and verify/archive reports produced during the multi-device EQ, alarm-countdown, and notification-visual-polish SDD cycles.
18 KiB
18 KiB
Tasks: snooze-reschedule-fix
Strict TDD active. Every behavioral task = RED (failing test) -> GREEN (minimal fix) -> REFACTOR (cleanup, still green). Tasks are numbered hierarchically; "Parallel" tasks have no file overlap with concurrently-listed siblings and may be done in any order or by different people; "Sequential" tasks depend on a prior task's output and must follow it.
Affected files: lib/estado/estado_alarmas.dart, lib/pantallas/pantalla_alarma_sonando.dart, lib/app.dart, test/helpers/fakes_alarmas.dart, test/estado/estado_alarmas_snooze_test.dart, test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart (or new _failure_test.dart).
Phase 1 — Test infrastructure (failure-path enablement)
1.1 [x] Add fallaProgramar switch to FakePuertoAlarmasAndroid (Sequential — blocks all RED tasks below)
- Satisfies: Design D7; Spec Requirement "Native Scheduling Failure Must Not Corrupt UI State".
- File:
test/helpers/fakes_alarmas.dart. - Add a
bool fallaProgramar = falsefield toFakePuertoAlarmasAndroid. - In
programar(), iffallaProgramaris true,throw StateError('fake programar failure')before appending toprogramadas. If false, behave exactly as today (append and return). - No test asserts on the fake itself (it's a test double) — verify by compiling and running the existing snooze test suite unchanged (must stay green, zero behavior change for
fallaProgramar == false). - Cannot run RED for 1.2-1.5 without this; this task ships first and alone.
Phase 2 — posponerAlarma() reliability (RED -> GREEN -> REFACTOR)
2.1 [x] [RED] Failure path does not throw, calls notifyListeners, records _error (Sequential — depends on 1.1)
- Satisfies: Spec "Native Scheduling Failure Must Not Corrupt UI State" / Scenario "Native scheduling throws (failure path)".
- File:
test/estado/estado_alarmas_snooze_test.dart. - Add test: build
EstadoAlarmaswithandroid.fallaProgramar = trueafterguardarAlarma, callawait estado.posponerAlarma(alarma, minutos). - Assert: the
awaitdoes NOT throw;estado.erroris non-null after the call; anotifyListeners()fired (use anaddListenercounter, expect>= 1);estado.alarmas.single.snoozeHastais still committed to the new value (proves_aplicar(config)ran before the native call, per design — in-memory state commits first). - Run: test MUST fail (current code has no try/catch — exception propagates uncaught, test fails on the
awaitline, or_errorstays null).
2.2 [x] [GREEN] Implement try/catch + permission pre-check + unconditional notifyListeners in posponerAlarma() (Sequential — depends on 2.1)
- Satisfies: Spec "Permission Pre-Check Before Snooze Scheduling" + "Native Scheduling Failure Must Not Corrupt UI State"; Design D1, D2, D3, D5.
- File:
lib/estado/estado_alarmas.dart, methodposponerAlarma()(current L194-212). - Add
_error = null;at the start of the method (Design D5 — clear stale failure before a fresh attempt). - Wrap
_solicitarPermisosNecesariosParaAlarma()(new call, Design's permission pre-check) +await android.programar(actualizada)intry { ... } catch (e) { _error = '...'; }— untypedcatch (e), NOTon StateError(Design D2). - Keep the catch block non-rethrowing (swallow into
_error, mirrorguardarAlarma()exactly). - Move
notifyListeners()so it executes unconditionally after the try/catch (NOT inside try, NOT in finally — trailing call per Design D1). - Run: 2.1 test MUST pass. Run full
estado_alarmas_snooze_test.dartsuite — all prior tests (success path, sync state, native-event paths) MUST stay green with zero behavior change whenfallaProgramar == false.
2.3 [x] [RED] Success path leaves _error null (regression guard for D5) (Sequential — depends on 2.2)
- Satisfies: Design D5 (stale-error clearing); Spec Scenario "Native scheduling succeeds (happy path)".
- File:
test/estado/estado_alarmas_snooze_test.dart. - Add test: trigger a failed
posponerAlarmafirst (fallaProgramar = true, confirmestado.error != null), then setandroid.fallaProgramar = falseand callposponerAlarmaagain on the same alarm. - Assert:
estado.errorisnullafter the second (successful) call. - This MUST already pass after 2.2 (the
_error = nullreset at method start covers it) — write it as a deliberate regression guard, run it to confirm GREEN immediately; if it fails, the_error = nullplacement in 2.2 was wrong and must be fixed before continuing (loop back into 2.2, do not proceed to Phase 3).
2.4 [x] [REFACTOR] Cleanup pass on posponerAlarma() (Sequential — depends on 2.2, 2.3 green)
- Satisfies: code quality, no spec/design behavior change.
- File:
lib/estado/estado_alarmas.dart. - Re-read the method top to bottom: confirm debugPrint placement still makes sense, confirm variable naming, confirm the catch comment matches
guardarAlarma()'s phrasing style (e.g. "Alarma pospuesta, pero Android no pudo reprogramarla todavía: $e") for consistency with the existing error message inguardarAlarma(). - No new test required — full suite must remain green (this step touches no observable behavior).
Phase 3 — posponerProximaDesdePreaviso() reliability (mirror of Phase 2)
3.1 [x] [RED] Failure path does not throw, calls notifyListeners, records _error (Parallel with Phase 2 once 1.1 lands — different test cases, same file; sequence within this phase is fixed)
- Satisfies: Spec Scenario "Pre-notice snooze failure (variant path)".
- File:
test/estado/estado_alarmas_snooze_test.dart. - Add test mirroring 2.1 but calling
await estado.posponerProximaDesdePreaviso(alarma, minutos, ejecucion)withandroid.fallaProgramar = true. - Assert: no throw;
estado.errornon-null;notifyListeners()fired;snoozeHasta/snoozeOrigenstill committed (state mutation happens before native call, same asposponerAlarma). - Run: test MUST fail against current code (no try/catch in this method either).
3.2 [x] [GREEN] Implement identical try/catch + permission pre-check + unconditional notifyListeners in posponerProximaDesdePreaviso() (Sequential — depends on 3.1)
- Satisfies: Spec "Permission Pre-Check Before Snooze Scheduling" + "Native Scheduling Failure Must Not Corrupt UI State"; Design D1-D5 (same shape as 2.2).
- File:
lib/estado/estado_alarmas.dart, methodposponerProximaDesdePreaviso()(current L214-236). - Same changes as 2.2:
_error = null;at start; guard_solicitarPermisosNecesariosParaAlarma()+android.programar(actualizada)in try/catch(e); unconditionalnotifyListeners()after. - Keep
_snoozeSeguro/snoozeHastacomputation andocultarNotificacionAlarmacall untouched (outside the guard, per Design). - Run: 3.1 test MUST pass. Full suite stays green.
3.3 [x] [RED] Success path leaves _error null for preaviso method (Sequential — depends on 3.2)
- Satisfies: Design D5 regression guard, preaviso variant.
- File:
test/estado/estado_alarmas_snooze_test.dart. - Mirror 2.3: fail once, then succeed, assert
estado.error == nullafter the successful call. - MUST pass immediately after 3.2; if not, fix
_error = nullplacement before proceeding.
3.4 [x] [REFACTOR] Cleanup pass on posponerProximaDesdePreaviso() (Sequential — depends on 3.2, 3.3 green)
- Satisfies: code quality, no behavior change.
- File:
lib/estado/estado_alarmas.dart. - Align error message phrasing with 2.4's, confirm both snooze methods now read as structural twins of
guardarAlarma(). - Full suite must remain green.
Phase 4 — UI failure feedback: ringing-screen _posponer()
4.1 [x] [RED] Failure surfaces a SnackBar and screen still dismisses (Sequential — depends on 2.2 GREEN, since posponerAlarma no longer throws)
- Satisfies: Spec "User-Facing Failure Feedback" / Scenario "Snooze fails and ringing screen dismisses".
- File:
test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart(add to existing group, or new filepantalla_alarma_sonando_failure_test.dartif the existing group's_buildEnv()helper needs a failure-mode variant — prefer extending the existing file's helpers with an optionalfallaProgramarparam to avoid duplicating_buildEnv/_Env/_montarConHistorial). - Add test: build env with
android.fallaProgramar = true(set afterguardarAlarma, since the initial save must succeed for setup), mount with history (_montarConHistorial), tap the snooze button. - Assert:
find.byType(PantallaAlarmaSonando)is gone (screen still dismissed — existing dismiss-by-design behavior preserved); aSnackBarwidget is found with text content (use the existingl10nlookup; failure message comes fromestado.error, which is theandroidExactAlarmScheduleError-sourced string per Design's l10n decision). - Run: test MUST fail against current code (today
_posponer()onlydebugPrints on catch, no SnackBar is shown — also today's code DOES throw so this scenario currently relies on the try/catch in_posponer()itself, not on a SnackBar at all).
4.2 [x] [GREEN] Wire SnackBar feedback in _posponer() (Sequential — depends on 4.1)
- Satisfies: Spec "User-Facing Failure Feedback"; Design D4 ("capture root/app-level ScaffoldMessenger BEFORE dismiss"), UI Wiring section.
- File:
lib/pantallas/pantalla_alarma_sonando.dart, method_posponer()(current L173-188). - Before the
await alarmas.posponerAlarma(...)call, capturefinal messenger = ScaffoldMessenger.of(context);(BEFORE await, per Design D4, so it survives screen dismissal). - After the await completes (inside or after the existing try block —
posponerAlarmano longer throws after 2.2, so the existingcatch (e)becomes a dead/safety-net branch per Design "method no longer throws on native failure" — keep it as a defensive net per Design's explicit instruction, do not delete it), readalarmas.error(theEstadoAlarmasinstance captured earlier asalarmas). If non-null, callmessenger.showSnackBar(SnackBar(content: Text(alarmas.error!))). - Update the stale comment at L177-180 (currently claims
posponerAlarmacan throw and that's why dismiss is infinally) to reflect the new reality: dismiss-in-finally is now a structural safety net, not a workaround for an expected throw; failure is now reported via_error/SnackBar, not via an exception. - Keep
if (mounted) _dismissScreen();infinallyunchanged (dismiss-by-design preserved, per spec scenario). - Run: 4.1 test MUST pass. Full
pantalla_alarma_sonando_dismiss_guard_test.dartsuite stays green (success-path snooze tests must show NO SnackBar — verify no new SnackBar finder conflicts with existing assertions).
4.3 [x] [RED] Success path shows no failure SnackBar (Sequential — depends on 4.2)
- Satisfies: Spec Scenario "Snooze succeeds" (no failure message shown).
- File: same test file as 4.1.
- Add/confirm test: default env (
fallaProgramarstaysfalse), tap snooze, assert noSnackBarwith the failure text is present (orfind.byType(SnackBar)is absent, depending on whether other snackbars exist on this screen — check current widget tree first via the existing dismiss-guard tests, which assert no SnackBar today). - Should already be GREEN after 4.2 (only shows SnackBar when
alarmas.error != null) — run to confirm; if it fails, fix the conditional in 4.2 before proceeding.
4.4 [x] [REFACTOR] Cleanup _posponer() (Sequential — depends on 4.2, 4.3 green)
- Satisfies: code quality, no behavior change.
- File:
lib/pantallas/pantalla_alarma_sonando.dart. - Re-read full method, confirm
messengervariable naming/placement is unambiguous, confirm comment clarity, confirm no duplicateScaffoldMessenger.of(context)lookups remain. - Full suite stays green.
Phase 5 — UI failure feedback: pre-notice POSTPONE_NEXT action in app.dart
5.1 [BLOCKED — see note] [RED] Failure branches to a failure SnackBar instead of the success message (Sequential — depends on 3.2 GREEN)
- Satisfies: Spec Scenario "Pre-notice snooze failure (variant path)" combined with "User-Facing Failure Feedback".
- File: new or existing widget test covering
app.dart'sPOSTPONE_NEXThandling (locate existing test coverage for this action first — searchtest/forPOSTPONE_NEXToralarmPostponedCurrentExecutionbefore creating a new file, to extend rather than duplicate harness setup). - Add test: simulate the
POSTPONE_NEXTnative event withandroid.fallaProgramar = true, assert the resulting SnackBar showsestado.errortext, NOTalarmPostponedCurrentExecution. - Run: test MUST fail against current code (today's code unconditionally shows the success SnackBar after the await, regardless of failure, since
posponerProximaDesdePreavisodoesn't yet set_erroruntil 3.2 — and even after 3.2,app.dartdoesn't yet branch on it). - APPLY NOTE (documented exception, not silently skipped): confirmed no existing test mounts
PluriWaveApp/_PaginaPrincipal. Investigated building one:PluriWaveApp.build()hardcodesEstadoAlarmas(prefs: prefs)andEstadoRadio(prefs:..., dispositivoAudio: ServicioDispositivoAudioReal())with zero DI seam. Worse,EstadoRadio's defaultServicioAudio()assertsregistrarHandler()(audio_service init frommain.dart) was called — mountingPluriWaveAppin a widget test without fullAudioService.init()+ platform-channel mocking throws/asserts immediately. This is a pre-existing architectural gap inapp.dart, not something this change's spec/design authorized fixing (no DI-seam task in scope). Building the required test infra (10+ MethodChannel mocks forServicioAlarmasAndroid, plusaudio_service/just_audiomocking) is disproportionate to this change's ~40-60 line estimate and risks brittle, unmaintainable scaffolding. Flagging for sdd-verify judgment.
5.2 [x] [GREEN] Branch on estado.error in app.dart's POSTPONE_NEXT handler (Sequential — depends on 5.1)
- Satisfies: Spec "User-Facing Failure Feedback"; Design "UI Wiring" section (app.dart POSTPONE_NEXT L291-311).
- File:
lib/app.dart, current L291-311. - After
await estado.posponerProximaDesdePreaviso(alarma, evento.snoozeMinutes, ejecucion);, branch: ifestado.error != null, show a failure SnackBar with that text; else keep the existingalarmPostponedCurrentExecutionsuccess SnackBar unchanged. - Keep
setState(() => _indice = 3);and the earlyif (!mounted) return;guard unchanged — only the SnackBar content/branch changes. - Implemented directly (no preceding RED widget test per 5.1's documented exception) since the change is a mechanical mirror of the already fully-tested Phase 4 SnackBar branch pattern.
flutter analyzeclean, fullflutter testsuite (238 tests) green, no regression.
5.3 [x] [REFACTOR] Cleanup app.dart POSTPONE_NEXT handler (Sequential — depends on 5.2 green)
- Satisfies: code quality, no behavior change.
- File:
lib/app.dart. - Confirm branch readability, confirm no duplicated
ScaffoldMessenger.of(context)calls, confirm comment if needed to explain the error/success branch. - Full suite stays green.
Phase 6 — Full regression sweep (Sequential — depends on all of Phase 2-5 green)
6.1 [x] Run full flutter test suite
- Satisfies: Proposal Success Criteria — "Dart regression tests cover success+failure for both paths, pass under flutter test".
- Run the entire test suite (not just touched files) to catch any cross-file regression (e.g. other tests relying on
posponerAlarma/posponerProximaDesdePreavisothrowing, or on the old_posponer()catch/debugPrint behavior). - Fix any incidental breakage found; do not skip or weaken unrelated tests to make this pass.
- Result: 238 tests passed, 0 failures.
flutter analyze: No issues found.
6.2 [x] Manual/device verification note (optional, non-blocking)
- Satisfies: Proposal Risks R1/R2 — root cause is permission-revocation, confirmable via device logcat.
- Not a coded task — record in the PR description that device-level confirmation (adb logcat tag PluriWave during a real snooze) is recommended but not required to merge, since the fix targets the confirmed structural defect regardless of root cause confirmation.
Review Workload Forecast
- Files touched:
lib/estado/estado_alarmas.dart(~30-40 changed lines across two methods),lib/pantallas/pantalla_alarma_sonando.dart(~15-20 changed lines),lib/app.dart(~10-15 changed lines),test/helpers/fakes_alarmas.dart(~5 lines),test/estado/estado_alarmas_snooze_test.dart(~80-100 new lines, 4 new tests),test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dartor new file (~40-60 new lines, 2 new tests), possible new/extendedapp.dartwidget test (~40-60 new lines). - Estimated total changed/added lines: roughly 220-300 lines (well under the 400-line single-PR budget).
- Chained PRs recommended: No — single PR is appropriate; change is Dart-only, narrowly scoped, fully covered by Strict TDD, no Kotlin/native edits, no schema or persisted-state changes (per Proposal Rollback Plan).
- 400-line budget risk: Low.
- Decision needed before apply: No — proceed as a single PR under
delivery_strategy: ask-on-riskwithout triggering the chained-PR conversation, since none of the risk thresholds are met. - Ownership/dependency note: Phase 1 (1.1) is a hard sequential blocker for every RED task in Phases 2-5 — it must land first and alone. Within Phase 2 and Phase 3, tasks are strictly sequential (RED before GREEN before REFACTOR). Phase 2 and Phase 3 touch the same file (
estado_alarmas.dart) but different methods — a single implementer should do both phases serially to avoid merge conflicts inside one file; do not parallelize across two people without coordinating hunks. Phase 4 depends on Phase 2's GREEN state (posponerAlarma no longer throwing) and Phase 5 depends on Phase 3's GREEN state — both UI phases can proceed in parallel with each other (different files:pantalla_alarma_sonando.dartvsapp.dart) once their respective state-layer phase is green.