fix(alarmas): heal alarms already poisoned by the old Detener anchor
The anchor fix stops NEW damage, but devices that ran the buggy build still carry a future occurrence in ultimaEjecucionGestionada in SharedPreferences. _esValida rejects any candidate matching it, so the affected alarm would keep skipping that day with nothing in the UI to explain it -- which reads as "still broken" rather than "fixed". _recalcular now drops an ultimaEjecucionGestionada that is meaningfully in the future. An occurrence cannot have been handled before it happens, so such a value is corrupt by definition, and dropping it can only ever restore a real future ring: the double-fire guard it also feeds needs a PAST occurrence to do its job, and those are untouched. Placed in the recalculation that every load and every mutation already funnels through, so an affected alarm heals on the next app open with no user action -- no delete-and-recreate. Tests: 1122 -> 1124, including one proving a genuine past occurrence is still preserved.
This commit is contained in:
@@ -568,13 +568,34 @@ class ServicioAlarmas {
|
||||
alarma.activa &&
|
||||
alarma.snoozeHasta != null &&
|
||||
alarma.snoozeHasta!.isAfter(ahora);
|
||||
// Self-heal for state poisoned before the Detener anchor fix: a stop
|
||||
// that closed a FUTURE occurrence wrote it into
|
||||
// ultimaEjecucionGestionada, and _esValida rejects any candidate
|
||||
// matching it -- so the alarm silently skips that day forever after,
|
||||
// with nothing in the UI to explain it. An occurrence cannot have been
|
||||
// handled before it happens, so a value meaningfully in the future is
|
||||
// corrupt by definition and safe to drop: it can only ever suppress a
|
||||
// real future ring, never prevent a double-fire (which needs a PAST
|
||||
// occurrence to guard). Placed here, in the recalculation every load and
|
||||
// every mutation already funnels through, so an affected alarm heals on
|
||||
// the next app open with no user action.
|
||||
final gestionada = alarma.ultimaEjecucionGestionada;
|
||||
final gestionadaCorrupta =
|
||||
gestionada != null &&
|
||||
gestionada.isAfter(
|
||||
ahora.add(ServicioProgramacionAlarmas.toleranciaDisparoInminente),
|
||||
);
|
||||
final saneada =
|
||||
gestionadaCorrupta
|
||||
? alarma.copyWith(limpiarUltimaEjecucionGestionada: true)
|
||||
: alarma;
|
||||
final proxima = _programacion.calcularProxima(
|
||||
alarma: alarma,
|
||||
alarma: saneada,
|
||||
desde: ahora,
|
||||
vacaciones: vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
return alarma.copyWith(
|
||||
return saneada.copyWith(
|
||||
proximaEjecucion: proxima,
|
||||
limpiarProximaEjecucion: true,
|
||||
limpiarSnooze: !snoozeActivo,
|
||||
|
||||
@@ -96,6 +96,53 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('estado ya envenenado se cura solo: una ocurrencia futura marcada '
|
||||
'como gestionada se descarta al recalcular', () async {
|
||||
// Devices that ran the buggy build carry the poisoned value in
|
||||
// SharedPreferences. Without this, the fix would still leave the
|
||||
// affected alarm skipping one more time, with nothing in the UI to
|
||||
// explain it -- and the user would reasonably read that as "not fixed".
|
||||
final ahora = DateTime(2026, 8, 3, 9, 0);
|
||||
final servicio = ServicioAlarmas(reloj: () => ahora);
|
||||
|
||||
// Saved by the buggy stop path: next Monday recorded as already handled.
|
||||
await servicio.guardarAlarma(
|
||||
semanalLunes(
|
||||
'a3',
|
||||
).copyWith(ultimaEjecucionGestionada: DateTime(2026, 8, 10, 16, 20)),
|
||||
);
|
||||
|
||||
final config = await servicio.recalcularTodas();
|
||||
final alarma = config.alarmas.single;
|
||||
|
||||
expect(alarma.ultimaEjecucionGestionada, isNull);
|
||||
expect(
|
||||
alarma.proximaEjecucion,
|
||||
DateTime(2026, 8, 3, 16, 20),
|
||||
reason: 'y con el dato corrupto fuera, hoy vuelve a ser candidata',
|
||||
);
|
||||
});
|
||||
|
||||
test('una ocurrencia gestionada REAL (pasada) se conserva: es la que evita '
|
||||
'que la alarma vuelva a sonar en el mismo minuto', () async {
|
||||
final ahora = DateTime(2026, 8, 3, 16, 20, 30);
|
||||
final servicio = ServicioAlarmas(reloj: () => ahora);
|
||||
final gestionada = DateTime(2026, 8, 3, 16, 20);
|
||||
|
||||
await servicio.guardarAlarma(
|
||||
semanalLunes('a4').copyWith(ultimaEjecucionGestionada: gestionada),
|
||||
);
|
||||
|
||||
final alarma = (await servicio.recalcularTodas()).alarmas.single;
|
||||
|
||||
expect(alarma.ultimaEjecucionGestionada, gestionada);
|
||||
expect(
|
||||
alarma.proximaEjecucion,
|
||||
DateTime(2026, 8, 10, 16, 20),
|
||||
reason: 'la de hoy ya sonó, la siguiente es el lunes que viene',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Detener sin nada sonando tampoco consume la próxima ocurrencia',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user