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:
2026-08-03 22:04:37 +02:00
parent df9252a621
commit 04300592e0
2 changed files with 70 additions and 2 deletions
@@ -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 {