fix(alarm): anchor snooze to the ringing occurrence, never a future one
posponerAlarma anchored the snooze to snoozeOrigen ?? proximaEjecucion, but once the native fire path works, the fire-time sync records the handled occurrence and recalculation advances proximaEjecucion to the NEXT day before the user can even tap snooze on the still-ringing screen. "Posponer 3" therefore armed the snooze a full day out (captured on-device: snoozeCountdown remaining=1443 minutes). The bug was invisible before because the broken delivery path never advanced proximaEjecucion while ringing — each fix unmasked the next. The anchor is now the newest occurrence that is not meaningfully in the future (shared 90s imminence window): snoozeOrigen for re-snoozes, proximaEjecucion on the watchdog path where it is still today's just-due occurrence, ultimaEjecucionGestionada on the native-fire path where the sync recorded the ringing occurrence, then now. ServicioAlarmas exposes ahora() so the anchor uses the same injectable clock as the rest of the scheduling math. Test fixtures that snoozed half an hour before the ring — a state the ringing screen can never be in, since it is posponerAlarma's only production caller — now move the clock to ring time, preserving their original expectations.
This commit is contained in:
@@ -27,9 +27,13 @@ void main() {
|
||||
snoozeMinutos: snoozeMinutos,
|
||||
);
|
||||
|
||||
test('posponerAlarma ancla snoozeHasta en proximaEjecucion + minutos, '
|
||||
test('posponerAlarma ancla el snooze a la ocurrencia sonando, '
|
||||
'programa una sola vez y notifica', () async {
|
||||
final ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
// The ring screen is posponerAlarma's ONLY production caller, so the
|
||||
// anchor is the occurrence that is ringing (just due), never a future
|
||||
// one. Saved at 7:00, ringing at 7:30:20 -> snooze lands at 7:35,
|
||||
// computed from the 7:30 occurrence.
|
||||
var ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => ahora),
|
||||
@@ -46,6 +50,7 @@ void main() {
|
||||
var notificaciones = 0;
|
||||
estado.addListener(() => notificaciones++);
|
||||
|
||||
ahora = DateTime(2026, 6, 11, 7, 30, 20);
|
||||
await estado.posponerAlarma(alarma, 5);
|
||||
|
||||
expect(estado.alarmas.single.snoozeHasta, DateTime(2026, 6, 11, 7, 35));
|
||||
@@ -55,10 +60,57 @@ void main() {
|
||||
expect(notificaciones, greaterThanOrEqualTo(1));
|
||||
});
|
||||
|
||||
test('posponerAlarma nunca ancla a una proximaEjecucion futura: tras el '
|
||||
'disparo nativo sincronizado, el snooze sale de la ocurrencia sonando '
|
||||
'(regresion: snooze armado para manana)', () async {
|
||||
// Real on-device sequence: the native FIRE fires at 22:59, onAlarmFired
|
||||
// records the handled occurrence, the cold-start sync imports it and
|
||||
// recalculation advances proximaEjecucion to TOMORROW — all before the
|
||||
// user taps snooze on the still-ringing screen. Anchoring to
|
||||
// proximaEjecucion armed "posponer 3" for tomorrow 23:02 (captured in
|
||||
// logcat as snoozeCountdown remaining=1443). The anchor must be the
|
||||
// ringing occurrence (ultimaEjecucionGestionada), landing today.
|
||||
var ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final servicio = ServicioAlarmas(reloj: () => ahora);
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: servicio,
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
addTearDown(android.dispose);
|
||||
await estado.guardarAlarma(alarmaDiaria('s2'));
|
||||
|
||||
// Native fire at 7:30; the fire-time sync marks the occurrence handled
|
||||
// and recalculation moves proxima to tomorrow while the ring is live.
|
||||
ahora = DateTime(2026, 6, 11, 7, 30, 20);
|
||||
await servicio.sincronizarEjecucionesNativas({
|
||||
's2': DateTime(2026, 6, 11, 7, 30),
|
||||
});
|
||||
await estado.refrescarProgramacion();
|
||||
final alarma = estado.alarmas.single;
|
||||
expect(
|
||||
alarma.proximaEjecucion,
|
||||
DateTime(2026, 6, 12, 7, 30),
|
||||
reason: 'precondicion: la proxima ya avanzo a manana',
|
||||
);
|
||||
expect(alarma.ultimaEjecucionGestionada, DateTime(2026, 6, 11, 7, 30));
|
||||
|
||||
await estado.posponerAlarma(alarma, 3);
|
||||
|
||||
expect(
|
||||
estado.alarmas.single.snoozeHasta,
|
||||
DateTime(2026, 6, 11, 7, 33),
|
||||
reason: 'el snooze debe sonar HOY a los 3 minutos, no manana',
|
||||
);
|
||||
expect(estado.alarmas.single.snoozeOrigen, DateTime(2026, 6, 11, 7, 30));
|
||||
});
|
||||
|
||||
test(
|
||||
'la lista de alarmas refleja el snooze de forma sincrona tras posponer',
|
||||
() async {
|
||||
final ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
var ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => ahora),
|
||||
@@ -69,6 +121,7 @@ void main() {
|
||||
addTearDown(android.dispose);
|
||||
await estado.guardarAlarma(alarmaDiaria('sync1'));
|
||||
|
||||
ahora = DateTime(2026, 6, 11, 7, 30, 10);
|
||||
final futuro = estado.posponerAlarma(estado.alarmas.single, 10);
|
||||
await futuro;
|
||||
|
||||
@@ -251,7 +304,7 @@ void main() {
|
||||
|
||||
test('posponerAlarma: cuando android.programar falla, no relanza, notifica y '
|
||||
'registra el error (sin corromper el estado en memoria)', () async {
|
||||
final ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
var ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => ahora),
|
||||
@@ -263,6 +316,7 @@ void main() {
|
||||
await estado.guardarAlarma(alarmaDiaria('fail1'));
|
||||
final alarma = estado.alarmas.single;
|
||||
|
||||
ahora = DateTime(2026, 6, 11, 7, 30, 10);
|
||||
android.fallaProgramar = true;
|
||||
var notificaciones = 0;
|
||||
estado.addListener(() => notificaciones++);
|
||||
@@ -280,7 +334,7 @@ void main() {
|
||||
|
||||
test('posponerAlarma: tras un fallo previo, un reintento exitoso limpia '
|
||||
'estado.error (D5)', () async {
|
||||
final ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
var ahora = DateTime(2026, 6, 11, 7, 0);
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => ahora),
|
||||
@@ -292,6 +346,7 @@ void main() {
|
||||
await estado.guardarAlarma(alarmaDiaria('fail2'));
|
||||
final alarma = estado.alarmas.single;
|
||||
|
||||
ahora = DateTime(2026, 6, 11, 7, 30, 10);
|
||||
android.fallaProgramar = true;
|
||||
await estado.posponerAlarma(alarma, 5);
|
||||
expect(estado.error, isNotNull);
|
||||
|
||||
Reference in New Issue
Block a user