Apply Progress: app-quality-and-native-alarms
Mode: Strict TDD (test runner: flutter test)
Artifact store: openspec (Engram unavailable this session)
Delivery: auto-chain, local apply — no commits, no PRs (user commits at own cadence)
Last updated: 2026-06-11 (Batch 6)
Batch log
| Batch |
Slice |
Status |
Date |
| 1 |
S1 — Alarm native reliability |
COMPLETE (Dart verified; Kotlin/manifest on-device verification deferred to user) |
2026-06-11 |
| 2 |
S2a + S2b — Snooze correctness end-to-end + Alarm UX parity |
COMPLETE (Dart verified; Kotlin on-device verification deferred to user) |
2026-06-11 |
| 3 |
S3a + S3b — Test seams (statics/prefs/cache/mutex/dirty-guard/bounded set) + audio_session |
COMPLETE (Dart-only batch; call-pause on-device verification deferred to user) |
2026-06-11 |
| 4 |
S7 — Streaming resilience (buffer config, reconnect state machine, UI wiring) |
COMPLETE (Dart-only batch; stream-drop on-device verification deferred to user) |
2026-06-11 |
| 5 |
S4a — ServicioExportImport + EstadoEcualizador extraction + compat getters |
COMPLETE (Dart-only batch) |
2026-06-11 |
| 6 |
S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds + compat-getter removal |
COMPLETE (Dart-only batch) |
2026-06-11 |
Task status (cumulative)
Slice S1 — Alarm native reliability — 17/17 complete
| Task |
Status |
Notes |
| T-S1-01 |
[x] |
RED test: programar() payload carries fallbackStationName/Url, fadeInSegundos, fallbackSound |
| T-S1-02 |
[x] |
RED test: solicitarExencionBateria() invokes requestIgnoreBatteryOptimizations |
| T-S1-03 |
[x] |
Manifest FGS type + permission. DEVIATION (see below) |
| T-S1-04 |
[x] |
API ≥ 34 3-arg startForeground with type bitmask. DEVIATION (see below) |
| T-S1-05 |
[x] |
Receiver showFireNotification + ensureFireChannel removed; service notification (id 92841) is sole FSI owner; fireNotificationIdForAlarm kept for cancel-migration safety |
| T-S1-06 |
[x] |
setFullScreenIntent was ALREADY present in service buildNotification; stopAlarm already cancels legacy fire id + stopForeground(STOP_FOREGROUND_REMOVE); FSI-before-audio ordering documented with comment |
| T-S1-07 |
[x] |
Channel pluriwave_alarm_fire_v2 (IMPORTANCE_HIGH, DEFAULT_ALARM_ALERT_URI + USAGE_ALARM attrs, vibration); one-time deletion of pluriwave_alarm_native + pluriwave_alarm_fire guarded by channels_migrated_v2 flag in device-protected prefs pluriwave_alarm_channels; channel count consolidated to 2 (fire_v2 + pre-notice) |
| T-S1-08 |
[x] |
NativeAlarmSpec + fallbackStationName/Url, fadeInSegundos; schemaVersion 2→3; fromJson backward-compatible (null/0 defaults); wired through scheduleAlarm, MainActivity handler, EXTRA_* consts, fireIntent extras |
| T-S1-09 |
[x] |
Three-stage chain primary(15s) → fallback station(15s) → bundled WAV via continuation lambdas (onStageFailed); scheduleStationFallback per stage with independent timeout windows |
| T-S1-10 |
[x] |
startFadeIn ramp: start 0.05×target, 250 ms steps over fadeInSegundos; applied to station and bundled-WAV players; cancelFadeIn() in stopAlarm (snooze path goes through stopAlarm) |
| T-S1-11 |
[x] |
requestIgnoreBatteryOptimizations MethodChannel handler + private method in MainActivity.kt mirroring requestExactAlarmPermission |
| T-S1-12 |
[x] |
GREEN: payload fields added to programar() args map |
| T-S1-13 |
[x] |
GREEN: solicitarExencionBateria() on PuertoAlarmasAndroid + impl |
| T-S1-14 |
[x] |
GREEN: asked-once guard in _solicitarPermisosNecesariosParaAlarma — calls only when !diag.ignoraOptimizacionBateria AND bateria_exencion_solicitada unset; optional SharedPreferences? prefs ctor param added to EstadoAlarmas (forward-compatible with S3 injection) |
| T-S1-15 |
[x] |
flutter test — full suite 54 tests, all passing (5 new) |
| T-S1-16 |
[x] |
flutter analyze — No issues found! (baseline before S1 was also clean) |
| T-S1-17 |
[x] |
dart format applied to the 4 touched Dart files |
Slice S2a — Snooze correctness — 20/20 complete
| Task |
Status |
Notes |
| T-S2a-01 |
[x] |
RED: test/estado/estado_alarmas_snooze_test.dart — anchor, snoozed-event, recalc-preserve + extras (cold-start import, stop-cancels-snooze, finalizar clears) |
| T-S2a-02 |
[x] |
RED: test/servicios/servicio_alarmas_snooze_test.dart — anchor future/clamped/custom(7), payload snoozeUntilMillis+snoozeOriginMillis, getNativeSnoozeState parse/empty. Test C (finalizar) lives in the estado file |
| T-S2a-03 |
[x] |
RED: synchronous list update after posponerAlarma (no poll) |
| T-S2a-04 |
[x] |
Kotlin: ACTION_SNOOZE reports back via MainActivity.notifyAlarmEvent with alarmAction="snoozed" (PluriWaveAlarmService.kt:56-80). On-device verify |
| T-S2a-05 |
[x] |
Kotlin: MainActivity.notifyAlarmEvent companion (lines ~610-635), @Volatile activeInstance set in configureFlutterEngine, cleared in onDestroy; main-thread post; no-op when engine dead. On-device verify |
| T-S2a-06 |
[x] |
Kotlin: AlarmScheduler.snooze() (lines 266-292) unified to occurrenceAt + minutes clamped to now + minutes; persists snoozeMinutes; returns NativeSnoozeResult(until, origin, title). On-device verify |
| T-S2a-07 |
[x] |
Kotlin: AlarmScheduler.nativeSnoozeStates() (lines 366-385) + getNativeSnoozeState handler (MainActivity.kt:192). On-device verify |
| T-S2a-08 |
[x] |
GREEN: EventoAlarmaAndroid.snoozeUntilMillis + accionSnoozed; app.dart ignores snoozed events in _abrirAlarmaSonando |
| T-S2a-09 |
[x] |
snoozeUntilMillis was ALREADY in the programar() payload — locked by new test, no code change |
| T-S2a-10 |
[x] |
GREEN: _alRecibirEventoNativo (estado_alarmas.dart:266) — posponerEjecucionHasta + _aplicar + notifyListeners, NO second programar. Subscribed in the CONSTRUCTOR (deviation, see below); cancelled in dispose |
| T-S2a-11 |
[x] |
GREEN: _importarSnoozesNativosActivos (estado_alarmas.dart:312) called at the end of _sincronizarEjecucionesGestionadasPorAndroid; imports active future snoozes for active alarms when they differ |
| T-S2a-12 |
[x] |
GREEN: obtenerEstadoSnoozeNativo() on PuertoAlarmasAndroid + impl + EstadoSnoozeNativo model |
| T-S2a-13 |
[x] |
GREEN: _recalcular snoozeActivo now requires alarma.activa (servicio_alarmas.dart:395) — disabling clears the snooze; finalizar path already cleared + re-programs without snooze (bridge cancels natively when inactive) |
| T-S2a-14 |
[x] |
RED: test/pantallas/pantalla_alarma_sonando_test.dart — buttons 3/5/10(+7), no-dup, tap-5 behavior |
| T-S2a-15 |
[x] |
GREEN: _liberarAudioLocal() + _posponer(int) + _detener refactor (pantalla_alarma_sonando.dart:138,161). _estadoSub.cancel() is fire-and-forget (deviation, see below) |
| T-S2a-16 |
[x] |
GREEN: snooze button row via _opcionesSnooze() (sorted {3,5,10,custom}); NEW l10n keys in ALL 13 .arb files: alarmSnoozeOptionLabel, snoozeAction, alarmSnoozeDurationTitle, alarmFallbackStationLabel, alarmStationPickerSearchHint (+ flutter gen-l10n regenerated) |
| T-S2a-17 |
[x] |
Targeted snooze tests green |
| T-S2a-18 |
[x] |
Full suite 77/77 |
| T-S2a-19 |
[x] |
flutter analyze — No issues found |
| T-S2a-20 |
[x] |
dart format on touched files |
Slice S2b — Editor + visual redesign — 12/12 complete
| Task |
Status |
Notes |
| T-S2b-01 |
[x] |
RED: scaffold test — PluriWaveScaffold present, no Color(0xFF061722) Scaffold, Animate present / absent under disableAnimations |
| T-S2b-02 |
[x] |
RED: 5 editor tests (preview + live update, primary picker + filtering, fallback picker, snooze duration persists, volume floor 0.0) |
| T-S2b-03 |
[x] |
GREEN: ringing screen on PluriWaveScaffold; 0xFFFFB86B → tokens.warmCoral; blurSigma: 10 + cold-GPU comment (Design 2.4) |
| T-S2b-04 |
[x] |
GREEN: lib/tema/pluri_animate.dart — pluriFadeIn/pluriScaleIn honoring MediaQuery.maybeDisableAnimationsOf |
| T-S2b-05 |
[x] |
GREEN: glass surface wrapped in .pluriFadeIn(context) |
| T-S2b-06 |
[x] |
GREEN: _vistaProximaEjecucion (draft → calcularProxima, respects vacations/exceptions; recomputed on every setState) |
| T-S2b-07 |
[x] |
GREEN: _CampoSelectorEmisora + _SelectorEmisoraSheet (SearchBar picker) for primary AND fallback station; copyWith clear-flags added (see deviations) |
| T-S2b-08 |
[x] |
GREEN: snooze duration SegmentedButton wired to snoozeMinutos (editor used to hardcode 5); volume slider min 0.25 → 0.0 (divisions 20) |
| T-S2b-09 |
[x] |
S2b targeted tests 7/7 green |
| T-S2b-10 |
[x] |
Full suite 77/77 |
| T-S2b-11 |
[x] |
flutter analyze — No issues found |
| T-S2b-12 |
[x] |
dart format applied |
Slice S3a — Test seams — 15/15 complete
| Task |
Status |
Notes |
| T-S3a-01 |
[x] |
RED: servicio_alarmas_android_instance_test.dart — two channels, simulated alarmFired via handlePlatformMessage, isolation asserted both ways |
| T-S3a-02 |
[x] |
RED: servicio_alarmas_cache_test.dart — _PrefsEspia implements SharedPreferences (setString/getString counters); no-write-when-clean, exactly-one-write-when-dirty, concurrent-no-lost-write (+ single cache hydration) |
| T-S3a-03 |
[x] |
RED: estado_alarmas_ejecuciones_test.dart — 100 stale entries pruned (1 fresh survives) + 250-entry cap test |
| T-S3a-04 |
[x] |
RED: mini_reproductor_configurar_test.dart — 10 rebuilds → 1 configurar; locale es→en → 2 |
| T-S3a-05 |
[x] |
GREEN: ServicioAlarmasAndroid statics → instance fields; handler installed per instance in ctor. DEVIATION: no deprecated static shim (Dart name clash + only call site rewired in same change); configurarLocalizaciones added to PuertoAlarmasAndroid interface instead |
| T-S3a-06 |
[x] |
GREEN: MiniReproductor → StatefulWidget, locale-guarded didChangeDependencies; alarm-bridge l10n hoisted to app.dart _PaginaPrincipalState.didChangeDependencies (design 3.3 alternative), before the early-return |
| T-S3a-07 |
[x] |
GREEN: main.dart resolves prefs ONCE; PluriWaveApp(prefs:) → EstadoRadio/EstadoAlarmas(→ServicioAlarmas)/EstadoIdioma |
| T-S3a-08 |
[x] |
GREEN: injected-with-fallback _resolverPrefs() in estado_radio (10 sites), servicio_ecualizador (6), servicio_grabacion_radio (4), servicio_contenido_app (3). rg check: only main.dart + one fallback per class remain |
| T-S3a-09 |
[x] |
GREEN: recalcularTodas dirty-guard — serialized comparison vs _cacheRaw, skips write when identical |
| T-S3a-10 |
[x] |
GREEN: _cache/_cacheRaw + _enCola writer queue; all 8 mutation methods queued over _configActual(). DEVIATION: public cargar() still re-reads prefs (queued cache reset) because EstadoRadio.importarConfig writes the raw alarms key directly — a fully cached cargar would hide imports until restart |
| T-S3a-11 |
[x] |
GREEN: bounded _ejecucionesEmitidas — cap 200 + 24 h retention, pruned on every add and each _vigilarAlarmasVencidas pass; @visibleForTesting length getter |
| T-S3a-12 |
[x] |
Targeted S3a tests green (RED first: 1 passed / 6 failed across the batch) |
| T-S3a-13 |
[x] |
Full suite 89/89 (77 baseline + 12 new) |
| T-S3a-14 |
[x] |
flutter analyze — No issues found |
| T-S3a-15 |
[x] |
dart format on 19 touched files |
Slice S3b — audio_session + intent flag — 7/7 complete
| Task |
Status |
Notes |
| T-S3b-01 |
[x] |
RED: servicio_audio_session_test.dart — 5 tests (pause-begin, resume-end, no-resume-without-prior-pause, duck begin/end, becoming-noisy hard pause) over fake ObjetivoAudioInterrumpible |
| T-S3b-02 |
[x] |
GREEN: lib/servicios/servicio_audio_session.dart — music().copyWith(androidWillPauseWhenDucked: true); interruption + becoming-noisy subscriptions; _pausadoPorInterrupcion gate for auto-resume; defines ObjetivoAudioInterrumpible (test seam) |
| T-S3b-03 |
[x] |
GREEN: PluriWaveAudioHandler implements ObjetivoAudioInterrumpible; _intencionReproducir true in play()/playMediaItem(), false in pause()/stop() (S7 seam); duck via setAtenuado ×0.3 (_volumenEfectivo); configurar() wired in main.dart |
| T-S3b-04 |
[x] |
Targeted run 5/5 green (RED first: load failure) |
| T-S3b-05 |
[x] |
Full suite 89/89 |
| T-S3b-06 |
[x] |
flutter analyze — No issues found |
| T-S3b-07 |
[x] |
dart format applied |
Slice S7 — Streaming resilience — 13/13 complete
| Task |
Status |
Notes |
| T-S7-01 |
[x] |
RED: servicio_audio_reconnect_test.dart — 8 tests over ControladorReconexion (backoff sequence + cap, retry scheduled with intent=true, NO retry with intent=false, exhaustion → agotado, restablecer resets counter + backoff base, cancelar kills pending timer) via injectable fake-timer factory |
| T-S7-02 |
[x] |
RED: buffer-config test asserts PluriWaveAudioHandler.configuracionCargaAndroid values (15s/50s/2.5s/5s, prioritizeTime=true) — construction wiring not unit-testable without platform channels (see deviations) |
| T-S7-03 |
[x] |
RED: reconnect_ui_test.dart — reconectando shows spinner + "Reconectando..." label, NO AlertDialog/SnackBar, NO manual-retry button; second test locks retry button to error state only |
| T-S7-04 |
[x] |
GREEN: _crearPlayer passes audioLoadConfiguration: configuracionCargaAndroid; named static const durations. just_audio 0.9.46 API verified in pub-cache source — all design params exist, NO deviation |
| T-S7-05 |
[x] |
GREEN: EstadoReproduccion.reconectando added; estadoStream maps the handler's reconectando flag (error wins, then reconectando, then cargando) |
| T-S7-06 |
[x] |
GREEN: NEW lib/servicios/controlador_reconexion.dart (pure logic: maxReintentos=5, base=1s, cap=30s); handler enters reconnect on network-class errors (PlayerException 2xxx OR TimeoutException) with intent=play; retry re-issues source via revision-guarded _cambiarFuente; ready+playing resets; pause/stop/playMediaItem cancel/reset; exhaustion → single terminal error; _cambiarFuente returns normally when reconnect engaged so EstadoRadio.reproducir doesn't snackbar mid-retry |
| T-S7-07 |
[x] |
GREEN: mini player (spinner + playbackStatusReconnecting in _labelEstado), full player (_WaveHero/_Controles: reconectando = loading, not error), visualizer stays active; l10n key in ALL 13 .arb locales + gen-l10n |
| T-S7-08 |
[x] |
GREEN: S7-R4 boundary comment at _estadoSub listener — only reproduciendo cancels the alarm's 12s fallback timer; reconectando never counts as playing (code already correct, now documented + locked by enum distinctness) |
| T-S7-09 |
[x] |
GREEN: ServicioGrabacionRadio untouched except S7-R5 invariant comment above _fallar |
| T-S7-10 |
[x] |
Targeted run 10/10 green (RED first: +0 -2 load failures) |
| T-S7-11 |
[x] |
Full suite 99/99 (89 baseline + 10 new) |
| T-S7-12 |
[x] |
flutter analyze — No issues found |
| T-S7-13 |
[x] |
dart format on 9 touched files (2 reflowed); re-ran suite + analyze after format |
Slice S4a — ServicioExportImport + EstadoEcualizador — 10/10 complete
| Task |
Status |
Notes |
| T-S4a-01 |
[x] |
RED: servicio_export_import_test.dart — v2 round-trip deep-equal (favorites/groups/EQ/alarms/vacations, alarmas raw passthrough, "sin asignar" excluded) + malformed JSON → null (invalid/empty/non-object) |
| T-S4a-02 |
[x] |
RED: estado_ecualizador_test.dart — cambiarPreset notifies EQ listeners; EQ change does NOT notify EstadoRadio listeners (radio = 0) |
| T-S4a-03 |
[x] |
GREEN: lib/servicios/servicio_export_import.dart — construirExportacion (v2 envelope, exact legacy key set), exportar (pretty JSON), importar (graceful Map?). pantalla_ajustes lost dart:convert; EstadoRadio gained exportarConfigJson/parsearConfigJson |
| T-S4a-04 |
[x] |
GREEN: lib/estado/estado_ecualizador.dart — full EQ state + persistence + audio application; emisoraActualUuid callback seam; ListenableProvider registration in app.dart (owned/disposed by EstadoRadio) |
| T-S4a-05 |
[x] |
GREEN: EstadoRadio keeps 15 delegating members, all tagged // TODO(S4b): remove getter; EQ fields and private helpers removed |
| T-S4a-06 |
[x] |
GREEN: _SeccionEcualizador → Consumer2<EstadoRadio, EstadoEcualizador>; ecualizador_widget.dart already presentational (no change); pantalla_reproductor EQ toggle also rewired (deviation 3) |
| T-S4a-07 |
[x] |
Targeted run 4/4 green (RED first: +0 -2 load failures) |
| T-S4a-08 |
[x] |
Full suite 103/103 (99 baseline + 4 new) |
| T-S4a-09 |
[x] |
flutter analyze — No issues found |
| T-S4a-10 |
[x] |
dart format on 8 touched files (4 reflowed); analyze + suite re-run after format |
Slice S4b — EstadoGrabacion + EstadoBusqueda + scoped rebuilds — 13/13 complete
| Task |
Status |
Notes |
| T-S4b-01 |
[x] |
RED: estado_grabacion_test.dart — 4 tests (notify on state change, iniciar delegates with current station, no-station → alError without service call, error state → alError) over a controlled ServicioGrabacionRadio fake |
| T-S4b-02 |
[x] |
RED: estado_busqueda_test.dart — 3 tests (notify on buscar, pagination/memory cap MOVED from estado_radio_test, identity-stable resultados getter) |
| T-S4b-03 |
[x] |
RED: pantalla_inicio_rebuild_test.dart — EQ preset change does NOT rebuild PantallaInicio (S4-R5-A), debugPrintRebuildDirtyWidgets probe + positive control (cargarPopulares DOES rebuild) |
| T-S4b-04 |
[x] |
GREEN: lib/estado/estado_grabacion.dart — owns service, subscription, dir/maxBytes/open actions, pluriwave/file_actions channel; emisoraActual+alError seams; ListenableProvider in app.dart |
| T-S4b-05 |
[x] |
GREEN: lib/estado/estado_busqueda.dart — search + nearby (cercanas) + min-bitrate filter; ordenListas/textos/alError seams; ListenableProvider in app.dart |
| T-S4b-06 |
[x] |
GREEN: pantalla_inicio — no root watch; selects over identity-memoized getters (NEW lib/estado/orden_emisoras.dart: enum + sorter + MemoLista); cercanas/genre sections on EstadoBusqueda |
| T-S4b-07 |
[x] |
GREEN: pantalla_ajustes 6 watch sites — Grabaciones → watch; Timer/Orden/Grupos/Preferida/Emisoras → context.select; _SeccionInfo keeps scoped Consumer |
| T-S4b-08 |
[x] |
GREEN: pantalla_favoritos → selects; ALSO pantalla_buscar root watch → EstadoBusqueda and pantalla_reproductor _GrabacionWidget → EstadoGrabacion (mandatory: EstadoRadio no longer notifies on recording/search) |
| T-S4b-09 |
[x] |
GREEN: estado_radio.dart — 15 compat members removed (zero TODO(S4b) in lib/), recording + search state extracted; 1121 (pre-split) → 753 lines |
| T-S4b-10 |
[x] |
Targeted run 8/8 green (RED first: +0 -3 load failures) |
| T-S4b-11 |
[x] |
Full suite 110/110 (103 baseline − 1 moved test + 8 new) |
| T-S4b-12 |
[x] |
flutter analyze — No issues found |
| T-S4b-13 |
[x] |
dart format on 15 touched files (10 reflowed); analyze + suite re-run after |
Remaining slices (not started)
S5, S6, cross-cutting (T-CC-01, T-CC-02) — all pending.
Snooze defect fixes (design audit D1–D5 / S1–S5)
| Defect |
Fix |
Where |
| D1 (audit S1) — native snooze never notifies Flutter |
ACTION_SNOOZE → MainActivity.notifyAlarmEvent("snoozed", origin, until); Flutter records via posponerEjecucionHasta WITHOUT re-programming; engine-dead case covered by getNativeSnoozeState cold-start import |
PluriWaveAlarmService.kt:56-80, MainActivity.kt:627, estado_alarmas.dart:266,312 |
| D2 (audit S2) — two snooze anchors |
Unified everywhere to occurrence + minutes clamped to now + minutes: native snooze() adopts postponeNext logic; Dart posponerEjecucion re-anchored from now+min to ejecucion+min |
AlarmScheduler.kt:266-292, servicio_alarmas.dart:256-274 |
| D3 (audit S3) — no snooze on ringing screen |
3/5/10 + custom buttons → _posponer → shared _liberarAudioLocal teardown → canonical EstadoAlarmas.posponerAlarma (Flutter-first; hides native notification = same stop path) |
pantalla_alarma_sonando.dart:138-176,242-256 |
| D4 (audit S4) — recalc erases native-only snooze |
Resolved transitively by D1: Flutter now RECORDS every native snooze, so _recalcular sees snoozeActivo and preserves it; regression-guard test added; additionally snooze cleared when alarm disabled (S2-R5) |
servicio_alarmas.dart:392-401, test estado_alarmas_snooze_test.dart |
| D5 (audit S5) — preserveNativeSnooze origin mismatch |
Resolved transitively by D1/D2: Flutter always sends snoozeUntilMillis when snoozed, so the preservation net is no longer load-bearing; kept as belt-and-suspenders per design |
AlarmScheduler.kt:preserveNativeSnooze (unchanged) |
TDD Cycle Evidence (Strict TDD hard gate)
| Task |
RED (test written first, failing) |
GREEN (implementation passes) |
REFACTOR |
| T-S1-01/T-S1-12 |
servicio_alarmas_android_test.dart written first; load failure + payload keys absent |
Payload fields added; 3 tests pass |
dart format |
| T-S1-02/T-S1-13 |
Same RED run: solicitarExencionBateria undefined → compile failure |
Interface + impl added; test passes |
None needed |
| T-S1-14 |
"solicita exencion una sola vez" FAILED (Expected 1, Actual 0) |
Asked-once guard; both tests pass |
Fake made configurable |
| T-S1-03..11 (Kotlin) |
N/A — on-device items |
N/A |
Surgical diffs |
| T-S2a-01..03 / T-S2a-08..13 |
All 3 new test files failed to LOAD (missing EstadoSnoozeNativo, accionSnoozed, obtenerEstadoSnoozeNativo, alarmSnoozeOptionLabel) — captured before any implementation. Anchor test would fail under old now+min semantics (verified by design: old code returned 7:05 vs expected 7:35) |
All bridge/state/service changes added; targeted run 23/23 green |
Shared FakePuertoAlarmasAndroid extracted to test/helpers/fakes_alarmas.dart; existing estado_alarmas_test.dart deduplicated |
| T-S2a-14..16 |
Widget test load failure (l10n key missing) then tap-test failure (snoozeHasta null) |
Buttons + _posponer implemented; 3/3 green |
Debug prints removed; unawaited cancel documented |
| T-S2b-01..02 / T-S2b-03..08 |
Scaffold tests failed (no PluriWaveScaffold/Animate); all 5 editor tests failed (key not found) — captured in dedicated RED run (0 passed / 7 failed) |
pluri_animate.dart, scaffold migration, editor preview/pickers/snooze/volume; 7/7 green |
Material(transparency) wrappers; initState-l10n fix; dart format |
RED run evidence: first run +0 -3 (loading failures, three files); ringing tap test Expected: DateTime:<07:35> Actual: <null>; S2b run +0 -7 before implementation. GREEN: targeted 23/23 then 7/7; full suite 00:24 +77: All tests passed!.
Batch 3 TDD Cycle Evidence (S3a + S3b)
| Task |
RED (test written first, failing) |
GREEN (implementation passes) |
REFACTOR |
| T-S3a-01/T-S3a-05 |
Static controller shared events: expect(eventosB, isEmpty) FAILED ('solo-a' leaked into B) |
Statics → instance fields; isolation test passes |
Comment documenting handler re-bind semantics |
| T-S3a-02-A/T-S3a-09 |
recalcularTodas always wrote: Expected: <1> Actual: <2> writes |
Dirty-guard skips clean writes |
_serializar extracted, shared with _guardar |
| T-S3a-02-B |
Passed pre-fix (exactly-once lock-in guard) |
Still passes (regression lock) |
— |
| T-S3a-02-C/T-S3a-10 |
Lost write: final config had 1 of 2 alarms; 2 hydration reads |
Cache + _enCola queue: both alarms persisted, 1 hydration read |
Mutation bodies kept verbatim, only wrapped |
| T-S3a-03/T-S3a-11 |
Load failure: ejecucionesEmitidasLength/maxEjecucionesEmitidas undefined |
Bounded set: 1 survivor of 101, cap respected |
Prune helper shared by add-path and watch-pass |
| T-S3a-04/T-S3a-06 |
Expected: <1> Actual: <11> (configurar on every rebuild) |
StatefulWidget + locale guard: 1 then 2 |
FakeServicioAudio gained l10n override (assert fix) |
| T-S3b-01/02/03 |
Load failure: servicio_audio_session.dart missing |
5/5 green against fake objetivo |
— |
RED run evidence (Batch 3): 00:06 +1 -6 before implementation (the single pass is the exactly-once write lock-in). GREEN: targeted 12/12; full suite 00:12 +89: All tests passed!.
Batch 4 TDD Cycle Evidence (S7)
| Task |
RED (test written first, failing) |
GREEN (implementation passes) |
REFACTOR |
| T-S7-01/T-S7-06 |
Load failure: controlador_reconexion.dart missing (+0 -2 run) |
ControladorReconexion created; 6 decision/backoff tests pass |
Doc comments tying defaults to the ~60-90s design window |
| T-S7-02/T-S7-04 |
Same RED run: configuracionCargaAndroid undefined |
Const config + _crearPlayer wiring; values test passes |
Durations extracted as named static const |
| T-S7-03/T-S7-05/T-S7-07 |
Compile failure: EstadoReproduccion.reconectando missing |
Enum + stream mapping + UI wiring; both widget tests pass |
Test fixed to double-pump (stream event delivery + frame); diag run proved impl correct before the fix |
RED run evidence (Batch 4): 00:00 +0 -2 (both files fail to load). GREEN: targeted 00:01 +10: All tests passed!; full suite 00:08 +99: All tests passed! (89 baseline + 10 new).
Batch 5 TDD Cycle Evidence (S4a)
| Task |
RED (test written first, failing) |
GREEN (implementation passes) |
REFACTOR |
| T-S4a-01/T-S4a-03 |
Load failure: servicio_export_import.dart missing (+0 -2 run) |
Service created; round-trip + malformed tests pass |
Envelope comments tied to legacy format compatibility |
| T-S4a-02/T-S4a-04/05 |
Same RED run: estado_ecualizador.dart missing, estado.ecualizador undefined |
EQ notifier + EstadoRadio delegation; both tests pass |
dart format reflow; delegation kept expression-bodied |
RED run evidence (Batch 5): 00:00 +0 -2 (both files fail to load — captured before any lib code). GREEN: targeted 00:00 +4: All tests passed!; full suite 00:12 +103: All tests passed! (99 baseline + 4 new); analyze + suite re-run after format.
Batch 6 TDD Cycle Evidence (S4b)
| Task |
RED (test written first, failing) |
GREEN (implementation passes) |
REFACTOR |
| T-S4b-01/T-S4b-04 |
Load failure: estado_grabacion.dart missing (+0 -3 run) |
EstadoGrabacion created; 4 tests pass |
Comment ties callbacks to the S4a seam pattern |
| T-S4b-02/T-S4b-05 |
Same RED run: estado_busqueda.dart missing |
EstadoBusqueda created; 3 tests pass |
Pagination test deduplicated out of estado_radio_test |
| T-S4b-03/T-S4b-06..09 |
Same RED run: estado.busqueda undefined; then first GREEN attempt FAILED honestly (Expected: true Actual: <false>) because the element.dirty probe cannot observe provider's deferred dependent notification |
Probe rewritten over debugPrintRebuildDirtyWidgets; EQ change → screen NOT in rebuild log; cargarPopulares control → screen IS in log |
Memo identity test added to estado_busqueda_test locking the select-enabler invariant |
RED run evidence (Batch 6): 00:00 +0 -3 (all three files fail to load — captured before any lib code). GREEN: targeted 8/8; full suite 00:11 +110: All tests passed! (103 baseline − 1 moved + 8 new); analyze + suite re-run after format.
Files changed (Batch 2)
| File |
Action |
~Lines |
android/.../AlarmScheduler.kt |
Modified |
+57/-12 (unified snooze + NativeSnoozeResult + nativeSnoozeStates) |
android/.../MainActivity.kt |
Modified |
+36/-1 (notifyAlarmEvent companion, activeInstance, getNativeSnoozeState) |
android/.../PluriWaveAlarmService.kt |
Modified |
+19/-1 (snooze report-back) |
lib/servicios/servicio_alarmas_android.dart |
Modified |
+60 (snoozed event, EstadoSnoozeNativo, bridge method) |
lib/servicios/servicio_alarmas.dart |
Modified |
+20/-3 (anchor, activa-aware snooze clearing) |
lib/estado/estado_alarmas.dart |
Modified |
+95/-15 (event subscription, snooze recording, cold-start import) |
lib/pantallas/pantalla_alarma_sonando.dart |
Modified |
+70/-15 (snooze buttons, teardown, PluriWaveScaffold, tokens, fade-in) |
lib/pantallas/pantalla_alarmas.dart |
Modified |
~+330/-60 net (preview, pickers, snooze field, volume floor, initState fix, Material wrappers; large diff partly dart-format reflow) |
lib/modelos/alarma_musical.dart |
Modified |
+10/-2 (limpiarEmisora/limpiarEmisoraFallback) |
lib/app.dart |
Modified |
+7 (ignore snoozed events) |
lib/tema/pluri_animate.dart |
Created |
+39 |
lib/l10n/app_*.arb (13 files) |
Modified |
+12 each (5 keys + metadata) |
lib/l10n/gen/* (15 files) |
Regenerated |
by flutter gen-l10n |
test/helpers/fakes_alarmas.dart |
Created |
+120 |
test/helpers/fakes.dart |
Modified |
+13 (pausar/setVolumen on FakeServicioAudio) |
test/estado/estado_alarmas_snooze_test.dart |
Created |
+250 |
test/servicios/servicio_alarmas_snooze_test.dart |
Created |
+155 |
test/pantallas/pantalla_alarma_sonando_test.dart |
Created |
+165 |
test/pantallas/pantalla_alarma_sonando_scaffold_test.dart |
Created |
+150 |
test/pantallas/pantalla_alarmas_editor_test.dart |
Created |
+210 |
test/estado/estado_alarmas_test.dart |
Modified |
-78/+8 (fake deduplicated to helper; anchor expectations 7:36:00 → 7:36:02) |
Files changed (Batch 3)
| File |
Action |
~Lines |
lib/servicios/servicio_audio_session.dart |
Created |
+115 (session config, interruption/noisy handling, ObjetivoAudioInterrumpible) |
lib/servicios/servicio_alarmas.dart |
Modified |
+106/-64 (cache, _enCola queue, dirty-guard, _parsear/_serializar) |
lib/estado/estado_alarmas.dart |
Modified |
+55/-2 (bounded set, configurarLocalizaciones, prefs→servicio default) |
lib/servicios/servicio_alarmas_android.dart |
Modified |
+18/-11 (statics → instance, interface method) |
lib/servicios/servicio_audio.dart |
Modified |
+52/-3 (intent flag, ObjetivoAudioInterrumpible impl, duck volume) |
lib/estado/estado_radio.dart |
Modified |
+16/-15 (prefs param + _resolverPrefs, static alarm-l10n call removed) |
lib/widgets/mini_reproductor.dart |
Modified |
+22/-2 (StatefulWidget, locale-guarded didChangeDependencies) |
lib/main.dart |
Modified |
+14/-2 (prefs once, ServicioAudioSession wiring) |
lib/app.dart |
Modified |
+22/-3 (PluriWaveApp.prefs, alarm l10n locale guard) |
lib/servicios/servicio_ecualizador.dart |
Modified |
+14/-6 (prefs injection) |
lib/servicios/servicio_grabacion_radio.dart |
Modified |
+13/-4 (prefs injection) |
lib/servicios/servicio_contenido_app.dart |
Modified |
+11/-3 (prefs injection) |
test/helpers/fakes.dart |
Modified |
+8 (configurarLocalizaciones override on FakeServicioAudio) |
test/helpers/fakes_alarmas.dart |
Modified |
+4 (interface no-op) |
test/servicios/servicio_alarmas_android_instance_test.dart |
Created |
+53 |
test/servicios/servicio_alarmas_cache_test.dart |
Created |
+105 |
test/estado/estado_alarmas_ejecuciones_test.dart |
Created |
+85 |
test/widgets/mini_reproductor_configurar_test.dart |
Created |
+85 |
test/servicios/servicio_audio_session_test.dart |
Created |
+130 |
Total Batch 3 diff: ~362 insertions / ~122 deletions in lib + helpers, plus ~458 lines of new tests.
Files changed (Batch 4)
| File |
Action |
~Lines |
lib/servicios/controlador_reconexion.dart |
Created |
+100 (decision enum + backoff controller, injectable timer factory) |
lib/servicios/servicio_audio.dart |
Modified |
+148/-7 (enum reconectando, buffer config consts, reconnect integration, TimeoutException routing, pause/stop/play resets) |
lib/widgets/mini_reproductor.dart |
Modified |
+6/-2 (spinner for reconectando, label case) |
lib/pantallas/pantalla_reproductor.dart |
Modified |
+8/-2 (reconectando = loading in _WaveHero + _Controles) |
lib/widgets/visualizador_audio.dart |
Modified |
+2/-1 (reconectando keeps visualizer active) |
lib/pantallas/pantalla_alarma_sonando.dart |
Modified |
+5 (S7-R4 boundary comment) |
lib/servicios/servicio_grabacion_radio.dart |
Modified |
+4 (S7-R5 invariant comment) |
lib/l10n/app_*.arb (13 files) |
Modified |
+1 each (playbackStatusReconnecting) |
lib/l10n/gen/* (14 files) |
Regenerated |
by flutter gen-l10n |
test/servicios/servicio_audio_reconnect_test.dart |
Created |
+210 (8 tests) |
test/widgets/reconnect_ui_test.dart |
Created |
+100 (2 tests) |
Total Batch 4 diff: ~235 insertions / ~13 deletions in lib (incl. ARB/gen), plus ~310 lines of new tests. Within the ~285-line slice estimate. No Kotlin/native files touched.
Files changed (Batch 5)
| File |
Action |
~Lines |
lib/servicios/servicio_export_import.dart |
Created |
+80 (v2 envelope builder, pretty export, graceful parse) |
lib/estado/estado_ecualizador.dart |
Created |
+205 (EQ ChangeNotifier: presets, per-station map, activo, persistence, audio application, import path) |
lib/estado/estado_radio.dart |
Modified |
+122/-208 (EQ state/methods extracted; 15 // TODO(S4b) delegating members; exportarConfig delegates envelope; importarConfig delegates EQ; exportarConfigJson/parsearConfigJson; ecualizador owned + disposed) |
lib/pantallas/pantalla_ajustes.dart |
Modified |
+33/-23 (backup section delegates JSON to service, dart:convert removed; _SeccionEcualizador → Consumer2 with EstadoEcualizador) |
lib/pantallas/pantalla_reproductor.dart |
Modified |
+8/-11 (EQ toggle watches EstadoEcualizador) |
lib/app.dart |
Modified |
+7 (ListenableProvider exposing EstadoRadio's instance) |
test/servicios/servicio_export_import_test.dart |
Created |
+85 (2 tests) |
test/estado/estado_ecualizador_test.dart |
Created |
+52 (2 tests) |
Total Batch 5 diff: ~455 insertions / ~242 deletions in lib, plus ~137 lines of new tests. Slightly over the ~350-line slice estimate because the EQ method bodies moved (not duplicated) into the new notifier — net lib growth is ~+213. No Kotlin/native files touched.
Files changed (Batch 6)
| File |
Action |
~Lines |
lib/estado/orden_emisoras.dart |
Created |
+55 (OrdenEmisoras enum moved here + ordenarEmisoras + MemoLista identity memo; estado_radio re-exports the enum so existing imports keep compiling) |
lib/estado/estado_grabacion.dart |
Created |
+144 (recording notifier: service ownership, subscription, dir/maxBytes, open-file/dir actions, file_actions channel) |
lib/estado/estado_busqueda.dart |
Created |
+222 (search notifier: query/filters/pagination, cercanas + geolocation, min-bitrate filter, memoized sorted views) |
lib/estado/estado_radio.dart |
Modified |
+154/-375 net (recording/search/EQ-compat removed; memoized list getters; creates+disposes the 3 notifiers; custom-station mutations now reassign for memo identity) — 753 lines final (was ~1121 pre-split) |
lib/app.dart |
Modified |
+10/-4 (ListenableProviders for EstadoGrabacion + EstadoBusqueda) |
lib/pantallas/pantalla_inicio.dart |
Modified |
~+55/-41 (root watch removed; selects + EstadoBusqueda sections) |
lib/pantallas/pantalla_buscar.dart |
Modified |
~+30/-27 (root watch → watch; renamed members) |
lib/pantallas/pantalla_favoritos.dart |
Modified |
~+12/-3 (root watch → selects) |
lib/pantallas/pantalla_ajustes.dart |
Modified |
~+50/-32 (Grabaciones → EstadoGrabacion; 5 sections → selects) |
lib/pantallas/pantalla_reproductor.dart |
Modified |
~+18/-10 (_GrabacionWidget → watch) |
test/estado/estado_grabacion_test.dart |
Created |
+122 (4 tests) |
test/estado/estado_busqueda_test.dart |
Created |
+67 (3 tests) |
test/pantallas/pantalla_inicio_rebuild_test.dart |
Created |
+97 (1 test, S4-R5-A) |
test/estado/estado_radio_test.dart |
Modified |
EQ call sites → estado.ecualizador.*; pagination test moved out |
test/pantallas/pantalla_inicio_test.dart |
Modified |
_conProviders helper mirrors app.dart wiring (3 pump sites) |
Total Batch 6 lib diff: ~386 insertions / ~625 deletions across 9 pre-existing files plus 3 new lib files (+421) and 3 new test files (+286). Net lib growth ≈ +180; EstadoRadio shrank by ~260 lines this batch. No Kotlin/native, .arb or gen/ files touched.
Deviations from design (Batch 6)
- Provider ownership NOT inverted — documented as accepted. Design 217 allows "pass the shared service instances at construction"; EstadoEcualizador/EstadoGrabacion/EstadoBusqueda need EstadoRadio's services AND callbacks (
emisoraActual, alError, ordenListas, textos) at construction, so EstadoRadio creates and disposes all three and the ListenableProviders only expose the instances (S4a deviation 2 pattern, now final). Inverting would require lifting ServicioAudio/ServicioRadio creation into app.dart — out of slice budget and blast radius.
- NEW
lib/estado/orden_emisoras.dart (not in task text). Two reasons: (a) the OrdenEmisoras enum is needed by both EstadoRadio and EstadoBusqueda without a circular import (estado_radio re-exports it, so consumers compile unchanged); (b) MemoLista — derived-list getters used to return a fresh copy per read, which would make every context.select degrade to watch behavior (lists compare by identity). Identity-memoized getters are the enabler that makes S4-R5's "stop rebuilding on buffer events" REAL, not just formal.
- EstadoBusqueda also owns the nearby-stations (cercanas) flow (task text only said query/results/loading). cercanas shares the min-bitrate filter and
radio.buscar plumbing with search; leaving it in EstadoRadio would have kept a search-state remnant there against S4-R3's intent.
pantalla_buscar and pantalla_reproductor rewired beyond the task list (tasks named inicio/ajustes/favoritos). Mandatory, not optional: EstadoRadio no longer notifies on search or recording changes, so any screen still reading them through EstadoRadio would go permanently stale. Buscar now watches EstadoBusqueda; the player's _GrabacionWidget watches EstadoGrabacion.
- Custom-station mutations reassign the backing list instead of mutating in place — required so the identity memo (and therefore
select) sees the change. Behavior identical.
element.dirty is NOT a valid rebuild probe with provider — provider defers dependent notification to the next build phase (markNeedsNotifyDependents → inherited element rebuild → dependents marked during build). The widget test uses debugPrintRebuildDirtyWidgets log capture with a positive control instead. Worth remembering for future rebuild-scope tests.
emisorasDisponiblesPreferencia staleness window (minor, accepted): the preferred-station dropdown's option list now refreshes when favoritos/custom/populares/tendencias change identity, but a pure search/cercanas update no longer rebuilds the section (EstadoRadio does not notify on those anymore). The options re-derive on the section's next rebuild; preferred-station resolution itself prefers favorites, so impact is cosmetic.
Deviations from design (Batch 5)
importar() returns Map<String, dynamic>?, not a ConfiguracionCompleta model (task text suggested one). EstadoRadio's importarConfig(Map) is the existing application API with v1/v2 branching and a localized version-guard error; introducing a typed model would force re-validating/re-mapping every section twice in a slice that must stay under budget. The service's contract (graceful null on malformed, version inside the map) covers S4-R4; a typed model can land with S4b/S6 if wanted.
ListenableProvider instead of ProxyProvider for EstadoEcualizador registration. The notifier needs ServicioAudio from EstadoRadio at CONSTRUCTION; EstadoRadio therefore constructs and disposes it (transition ownership), and the provider only exposes the instance (create: ctx.read<EstadoRadio>().ecualizador, no dispose callback — avoids double-dispose). In S4b, when EstadoRadio sheds the remaining EQ surface, ownership can be inverted if desired.
pantalla_reproductor.dart EQ toggle rewired in S4a (task listed only ecualizador_widget + pantalla_ajustes). EstadoRadio no longer notifies on EQ changes (required by S4-R1-A test B), so any screen still reading EQ through EstadoRadio's compat getters under watch<EstadoRadio> would go STALE on toggle. The reproductor button was the only such site; 8-line fix beats shipping a known visual bug until S4b.
ecualizador_widget.dart needed NO change: both widgets in it are presentational (preset/onCambio props, no provider reads), so T-S4a-06's intent (scoped consumption) is satisfied at the call sites in pantalla_ajustes.
- Compat getters do NOT relay EQ notifications to EstadoRadio listeners — intentional and spec-mandated (S4-R1-A scenario). EQ-displaying UI was rewired in this same slice precisely because of this; S4b removes the getters entirely.
emisoraActualUuid callback seam on EstadoEcualizador (not in task text): per-station preset decisions need the currently playing station; a String? Function() injected by EstadoRadio keeps the notifier free of station-list coupling and trivially testable.
Deviations from design (Batch 3)
- No deprecated static shim for
ServicioAlarmasAndroid.configurarLocalizaciones (task text asked for one). Dart forbids a static and an instance member with the same name in one class, and the ONLY external call site (estado_radio.dart:74) was rewired in this same slice — a renamed shim would be unreachable dead code. Instead configurarLocalizaciones joined the PuertoAlarmasAndroid interface (fakes no-op it).
- Alarm-bridge l10n configured from
app.dart, not from MiniReproductor — design 3.3 offered both options; the mini player now only configures its real dependency (EstadoRadio), and _PaginaPrincipalState.didChangeDependencies (locale-guarded, placed BEFORE the existing early-return) forwards l10n to EstadoAlarmas.
- Public
ServicioAlarmas.cargar() re-reads from prefs instead of serving the cache. EstadoRadio.importarConfig writes the raw alarmas_musicales_v1 key directly to SharedPreferences; a fully cached cargar() would make a settings import invisible until app restart. Mutations DO use the cache (_configActual), which is what S3-R7's race fix and "one cargar per mutation burst" require. The re-read is queued, so it cannot interleave with a mutation.
- Duck handling added beyond the task text:
setAtenuado on the handler scales effective volume ×0.3 (restored on interruption end). With androidWillPauseWhenDucked: true Android delivers duck as pause, so this is mostly the iOS/edge path; kept minimal.
_PrefsEspia implements SharedPreferences via noSuchMethod rather than pulling shared_preferences_platform_interface into the tests — avoids a depend_on_referenced_packages lint on a transitive dep.
servicio_contenido_app.dart also migrated (3 getInstance sites; not named in the task). Its only construction site is static final in pluri_onboarding_dialog.dart, which keeps the fallback path at runtime — acceptable under the injected-with-fallback compat net; full injection there would require a dialog refactor out of S3 scope.
- Two-instances-same-channel semantics documented, not prevented: with instance handlers, constructing a second
ServicioAlarmasAndroid over the SAME MethodChannel re-binds the platform handler to the newest instance. Production creates exactly one per channel (provider singleton); tests use distinct channels.
Deviations from design (Batch 4)
- Reconnect logic lives in a NEW file
lib/servicios/controlador_reconexion.dart, not inline in servicio_audio.dart (task text said "edit servicio_audio.dart"). Pure decision/backoff logic must be testable without platform channels (S7-R7); the handler cannot be constructed in tests (AudioPlayer hits MethodChannels at construction). The handler keeps only the integration glue.
- Buffer-config test asserts the config VALUES, not the construction call. Asserting that
AudioPlayer(...) received the config would require constructing the real player (platform channels). The config is a static const on the handler; _crearPlayer passes it (one-line wiring, verified by review + on-device item 9). Honest scope of S7-R1's [flutter test] portion.
TimeoutException treated as network-class in addition to the spec'd PlayerException 2xxx range. A real network drop usually surfaces as the existing 12s source-change timeout, NOT as a 2xxx PlayerException — without this, the most common stall would bypass reconnect entirely. The generic-Exception terminal path is otherwise unchanged.
- Stall detection is error-driven only (per task T-S7-06 text); the design's optional "buffering > 8-10s watchdog" was NOT implemented. ExoPlayer/just_audio surfaces dead live streams as errors or our timeout; a buffering watchdog would add a timer racing the buffer config for marginal gain. Flagged for on-device validation (item 9): if a silent endless-buffering hang is observed, add the watchdog in a follow-up.
_cambiarFuente completes normally when reconnect is engaged (returns instead of rethrowing). Previously every failure rejected the playMediaItem future and EstadoRadio.reproducir's catch pushed an error snackbar — that would show an error on the FIRST failure even while reconnecting, violating S7-R3. User-facing rejection still happens when reconnect does NOT engage (non-network error, no intent, exhausted).
restablecer() on playMediaItem (fresh user play/source switch restarts the backoff budget). Not explicit in the task text but required so the retry path (which goes through _cambiarFuente internally, not playMediaItem) can exhaust while user-initiated switches always get a full budget.
- Widget tests need a double
tester.pump() after a broadcast-stream emission (one pump delivers the event, the second rebuilds). Verified with a diagnostic harness that the implementation was correct and only the test needed the fix.
Deviations from design (Batch 2)
- Event subscription lives in the
EstadoAlarmas CONSTRUCTOR, not inicializar (task text said inicializar). Rationale: snoozed events must be recorded even before/without full init, and tests with iniciarAutomaticamente: false stay light. Cancelled in dispose.
posponerEjecucion clamps minutes to 1..120 instead of coercing to 3/5/10. The old calcularSnooze coercion would have made the custom snooze button (e.g. "7 min", S2-R1-C) silently snooze 5. The native notification path still sanitizes to 3/5/10 (sanitizeSnoozeMinutes), unchanged. calcularSnooze kept (unused by this path) for API compatibility.
- Existing test expectation updated (
estado_alarmas_test.dart): unified anchor makes snooze land at proximaEjecucion(+inminencia normalization) + 5min = 7:36:02, not now+5 = 7:36:00. This is the spec'd behavior change (S2-R6), documented inline.
_recalcular now clears snooze for INACTIVE alarms — required by S2-R5-A ("snoozeHasta is null in persistent storage" after disable); previously a disabled alarm kept a stale snoozeHasta forever.
AlarmaMusical.copyWith gained limpiarEmisora/limpiarEmisoraFallback (not in task text). Without them the picker's "no station" could never clear an existing station (latent pre-existing bug: copyWith null-coalesced).
- Pre-existing debug crash fixed:
_EditorAlarmaSheetState.initState called AppLocalizations.of(context) → dependOnInheritedWidgetOfExactType assert in debug builds. Name controller now created lazily in didChangeDependencies. (The sibling _EditorVacacionesSheet has the same latent issue — NOT fixed here, out of S2 scope; flag for S5/S6.)
Material(type: transparency) wrappers added inside PluriGlassSurface for the editor sheet and the station picker — ListTiles inside a DecoratedBox trigger a debug assert and invisible ink splashes otherwise.
_liberarAudioLocal does NOT await _estadoSub.cancel() — a broadcast-subscription cancel future may not resolve until the stream closes (observed in tests); cancellation of delivery is synchronous, so fire-and-forget (unawaited) is correct and prevents the snooze tap from stalling.
AlarmScheduler.snooze returns NativeSnoozeResult (until/origin/title) so the service can build the report-back payload; postponeNext untouched (already had the unified anchor).
- Ringing screen
blurSigma capped to 10 (PluriGlassSurface default 18) as the Design 2.4 cold-GPU mitigation, plus reduced-motion users skip the entry animation entirely.
Issues found
flutter test does NOT auto-run gen-l10n in this setup despite generate: true; flutter gen-l10n must be run manually after editing .arb files (gen files are committed).
tester.tap + an awaited broadcast-subscription cancel() deadlocks the gesture handler chain in widget tests (see deviation 8) — worth remembering for S3/S7 work.
On-device verification checklist for the user
From tasks.md Section 11 — S1 items still pending from Batch 1, plus new S2 items:
- Kotlin compiles — native layer edited again without compilation (HIGH risk): build FIRST.
- Alarm fires app-killed on Android 14+ (S1-R1/R2); channel v2 on alarm stream (S1-R3); 3-stage fallback (S1-R4); native fade (S1-R6); battery dialog once (S1-R5); reboot persistence.
- Snooze from ringing screen (S2-R1, S2-R4): ring → tap "5 min" → notification dismissed, list shows snoozeHasta immediately, re-fires at that time.
- Snooze from notification while app killed (S2-R3): tap "Posponer" on the fire notification with the app killed → system alarm icon persists → reopen the app → list shows the snooze WITHOUT waiting for the 60 s poll (cold-start
getNativeSnoozeState import).
- Snooze from notification while app foregrounded (S2-R3): same, but the list updates within the same frame via the
snoozed MethodChannel event.
- Stop cancels pending snooze (S2-R5): snooze → disable the alarm from the list → does NOT re-fire.
- Ringing screen visuals (S2-R7): PluriWaveScaffold gradient + entry fade; verify no first-frame stutter on screen-off FSI wake (blur capped); with "remove animations" accessibility setting the entry is instant.
- Editor (S2-R8..R11): next-trigger preview updates live; searchable pickers for primary AND backup station; snooze duration control; volume slider reaches 0%.
Verification summary (Batch 2)
flutter test: 77/77 passing (54 pre-batch + 23 new)
flutter analyze: No issues found (identical to baseline)
dart format: applied to all touched Dart files only (gen/ untouched by hand)
flutter gen-l10n: run once after .arb edits
flutter build: NOT run (forbidden)
Verification summary (Batch 3)
flutter test: 89/89 passing (77 baseline + 12 new across 5 files)
flutter analyze: No issues found (identical to baseline)
dart format: applied to all 19 touched Dart files (5 reflowed)
rg 'SharedPreferences.getInstance()' lib/: only main.dart startup resolution + one injected-with-fallback expression per class (estado_alarmas, estado_idioma, estado_radio, servicio_alarmas, servicio_ecualizador, servicio_grabacion_radio, servicio_contenido_app)
flutter build: NOT run (forbidden)
- No Kotlin/native files touched in this batch
On-device verification items added by Batch 3 (user — Android device)
- Phone call pauses radio (S3-R1, checklist item 10): while the radio plays, receive a call → radio pauses (or ducks); after the call ends it resumes automatically (transient loss).
- Headphones unplugged pauses radio (S3-R1): unplug wired headphones / disconnect BT while playing → radio pauses and does NOT auto-resume.
- Another media app takes focus: start playback in another app → PluriWave pauses; it must not resume on its own when focus is permanent loss.
- Locale switch sanity: change app language in Ajustes → alarm titles/station names sent to new native schedules use the new language (l10n now configured per locale change, not per rebuild).
- Settings import still reflects alarms immediately (cache bypass in
cargar()): import a backup with alarms → the alarms list shows them without restarting the app.
Verification summary (Batch 4)
flutter test: 99/99 passing (89 baseline + 10 new across 2 files); re-run after dart format
flutter analyze: No issues found (identical to baseline); re-run after format
dart format: applied to all 9 touched Dart files (2 reflowed); gen/ untouched by hand
flutter gen-l10n: run once after the 13 .arb edits
flutter build: NOT run (forbidden)
- No Kotlin/native files touched in this batch (S7-R4: native alarm audio path untouched by construction)
What the buffer actually buys (honest expectations, Design 7.1)
- Configured: ExoPlayer keeps a 15-50s forward buffer; playback (re)starts after 2.5s buffered (5s after a rebuffer); time prioritized over byte thresholds.
- Real drop ≲ buffered cushion (typically a few seconds up to ~15-30s depending on bitrate and how full the buffer was): audio keeps playing through the cushion, no UI change.
- Drop longer than the cushion: playback stalls → "Reconectando..." spinner state (no error dialog/snackbar) → up to 5 backoff retries (1/2/4/8/16s delays + 12s attempt timeout each, total window ≈ up to ~90s) → on recovery the player rejoins the LIVE edge (live radio has no rewind — the missed audio is gone, not replayed) → on exhaustion, the single existing friendly error with manual retry.
On-device verification items added by Batch 4 (user — Android device)
- Short drop plays through (S7-R1, checklist item 9): while the radio plays (let it run ~1 min so the buffer fills), disable WiFi/LTE for ~10s → audio continues without interruption and no UI state change.
- Long drop reconnects (S7-R2/R3, checklist item 9): disable connectivity ~45s → mini player and full player show "Reconectando..." with spinner (NO error dialog/snackbar); re-enable within ~90s → playback resumes at the live edge automatically.
- Exhaustion surfaces one error (S7-R2-C): leave connectivity off >2 min → exactly ONE error state with the manual retry button appears after retries exhaust; no error spam during the retry window.
- User pause/stop during reconnect (S7-R6): trigger a drop, then tap pause/stop while "Reconectando..." → playback stays stopped; it must NOT restart on its own when connectivity returns.
- Alarm fallback not delayed (S7-R4, checklist item 11): alarm with a non-responding station URL → bundled WAV fires within the existing ~12-15s window, NOT extended by reconnect attempts.
- Recording during a drop (S7-R5): record while forcing a drop → recording fails/stops cleanly per existing behavior; a subsequent recording start works.
- Sleep timer during a drop (S7-R6): sleep timer expiring during "Reconectando..." stops audio for good.
Verification summary (Batch 5)
flutter test: 103/103 passing (99 baseline + 4 new across 2 files); re-run after dart format
flutter analyze: No issues found (identical to baseline); re-run after format
dart format: applied to all 8 touched Dart files (4 reflowed)
flutter build: NOT run (forbidden)
- No Kotlin/native, .arb or gen/ files touched in this batch
Manual verification items added by Batch 5 (user)
- Backup round-trip on device (S4-R4): export a backup from Ajustes, wipe/reinstall (or import on a second device), import the file → favorites, groups, custom stations, EQ presets, alarms (incl. vacations) and preferences all restored. Old (pre-S4a) backup files must import identically — the v2 envelope is byte-compatible.
- EQ controls still live-update (S4-R1): toggle EQ from the player screen and from Ajustes; chip/switch/preset selector reflect changes immediately (these now rebuild from EstadoEcualizador, not EstadoRadio).
- Per-station preset on playback switch: play a station with its own preset, switch to one without → main preset re-applies (path now goes through EstadoEcualizador).
Verification summary (Batch 6)
flutter test: 110/110 passing (103 baseline − 1 pagination test moved to estado_busqueda_test + 8 new across 3 files); re-run after dart format
flutter analyze: No issues found (identical to baseline) — used as the safety net for missed call sites after removing the 15 compat members; re-run after format
dart format: applied to all 15 touched Dart files (10 reflowed)
rg 'TODO\(S4b\)' lib/: ZERO occurrences (only historical mentions in tasks.md/apply-progress.md remain)
- EstadoRadio final size: 753 lines (was ~1121 pre-split, ~1010 after S4a)
flutter build: NOT run (forbidden)
- No Kotlin/native, .arb or gen/ files touched in this batch
Manual verification items added by Batch 6 (user)
- Search screen (S4-R3): search by name/country/language/quality, infinite scroll, genre chips on home — results and spinners behave as before (now driven by EstadoBusqueda).
- Nearby stations (S4-R3): "Detect" on home requests location and fills the nearby strip; error text when undetectable.
- Recording (S4-R2): start/stop from the player (indefinite, fixed and custom durations), live duration/bytes counter updates, open-folder/open-last-file buttons, recordings settings section (change/restore dir, max size) — all now via EstadoGrabacion.
- Scoped rebuilds (S4-R5): while audio plays/buffers, home/favorites/settings should feel identical (no visual change expected — the win is fewer rebuilds); list reordering in Ajustes still re-sorts home, search results and favorites.
- Stop recording on pause/stop/station switch: unchanged orchestration in EstadoRadio — verify recording stops when playback pauses/stops or station changes.
Workload / boundary
- Mode: auto-chain local slices (no PRs)
- Current work units: S1, S2a, S2b, S3a, S3b, S7, S4a (committed, latest
0416b30), S4b (complete, in working tree)
- Boundary (Batch 6): starts from the clean post-0416b30 tree; ends with S4b fully checked off, suite green (110/110). Rollback = revert the 9 modified lib/test files + delete the 6 new files (Dart-only; no native edits).
- Next batch: S5 (design system / a11y / i18n — unblocked since S2b) then S6 (quality gates — now unblocked: depends on S4b + S5).