Files
pluriwave/test/servicios/servicio_programacion_alarmas_test.dart
T
FreeTLab 88bd251eba fix(alarmas): scope schedule-skip exceptions to skipNext only
ExcepcionAlarma._esValida matched ANY exception tipo against an
occurrence, treating it as a user skip. Only the 'skipNext' tipo
existed until now, but the next commits reuse the same model to
record scheduling-reliability failures per alarm (so the alarms list
can surface them via ultimaExcepcionPara) -- without this guard, a
recorded failure would be silently treated as if the user asked to
skip that occurrence, corrupting scheduling. Adds tipo constants to
ExcepcionAlarma for the upcoming failure kinds.
2026-07-31 20:59:02 +02:00

358 lines
11 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/servicios/servicio_programacion_alarmas.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('ServicioProgramacionAlarmas', () {
final servicio = ServicioProgramacionAlarmas();
test('calcula la próxima alarma diaria futura', () {
final alarma = AlarmaMusical(
id: 'a1',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 5, 21, 7),
);
expect(proxima, DateTime(2026, 5, 21, 7, 30));
});
test('salta vacaciones si la alarma no debe sonar esos días', () {
final alarma = AlarmaMusical(
id: 'a2',
nombre: 'Laboral',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
sonarEnVacaciones: false,
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 8, 1, 7),
vacaciones: [
RangoVacaciones(
id: 'v1',
nombre: 'Verano',
inicio: DateTime(2026, 8),
fin: DateTime(2026, 8, 3),
),
],
);
expect(proxima, DateTime(2026, 8, 4, 8));
});
test('saltar próxima solo omite esa ejecución', () {
final alarma = AlarmaMusical(
id: 'a3',
nombre: 'Diaria',
hora: 9,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
final omitida = DateTime(2026, 5, 22, 9);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 5, 22, 8),
excepciones: [
ExcepcionAlarma(alarmaId: 'a3', ejecucion: omitida, tipo: 'skipNext'),
],
);
expect(proxima, DateTime(2026, 5, 23, 9));
});
test(
'un registro de fallo de programacion NO omite esa ejecucion (solo '
'skipNext debe hacerlo)',
() {
final alarma = AlarmaMusical(
id: 'a4',
nombre: 'Diaria',
hora: 9,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
final ocurrencia = DateTime(2026, 5, 22, 9);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 5, 22, 8),
excepciones: [
ExcepcionAlarma(
alarmaId: 'a4',
ejecucion: ocurrencia,
tipo: ExcepcionAlarma.tipoFalloProgramacion,
),
],
);
expect(
proxima,
ocurrencia,
reason:
'un fallo de programacion registrado no debe comportarse '
'como un salto de usuario',
);
},
);
test('snooze solo permite 3, 5 o 10 minutos y cae a 5', () {
expect(
servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 10),
DateTime(2026, 5, 21, 7, 10),
);
expect(
servicio.calcularSnooze(DateTime(2026, 5, 21, 7), 99),
DateTime(2026, 5, 21, 7, 5),
);
});
test(
'servicio limpia proxima ejecucion obsoleta al recalcular unica vencida',
() async {
SharedPreferences.setMockInitialValues({});
final servicioAlarmas = ServicioAlarmas(
reloj: () => DateTime(2026, 5, 22, 10),
);
final alarma = AlarmaMusical(
id: 'a4',
nombre: 'Unica vencida',
hora: 9,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.unica,
diasSemana: const [],
fechaUnica: DateTime(2026, 5, 22),
proximaEjecucion: DateTime(2026, 5, 22, 9),
);
final guardada = await servicioAlarmas.guardarAlarma(alarma);
expect(guardada.alarmas.single.proximaEjecucion, isNull);
},
);
test('mantiene alarma unica creada dentro del mismo minuto', () {
final alarma = AlarmaMusical(
id: 'a5',
nombre: 'Ahora',
hora: 20,
minuto: 13,
tipoProgramacion: TipoProgramacionAlarma.unica,
diasSemana: const [],
fechaUnica: DateTime(2026, 5, 23),
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 5, 23, 20, 13, 45),
);
expect(proxima, DateTime(2026, 5, 23, 20, 13, 47));
});
test('calcula siguiente diaria despues de ejecucion completada', () {
final alarma = AlarmaMusical(
id: 'a6',
nombre: 'Diaria',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
);
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: DateTime(2026, 5, 25, 7, 30),
);
expect(siguiente, DateTime(2026, 5, 26, 7, 30));
});
test('no reutiliza ejecucion gestionada aunque siga en tolerancia', () {
final alarma = AlarmaMusical(
id: 'a9',
nombre: 'Diaria gestionada',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
ultimaEjecucionGestionada: DateTime(2026, 5, 25, 7, 30),
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 5, 25, 7, 30, 20),
);
expect(proxima, DateTime(2026, 5, 26, 7, 30));
});
test('la hora de pared nunca deriva al iterar dias (DST-safe): diaria', () {
// Property lock for the DST drift bug: advancing candidates with
// add(Duration(days: 1)) shifts the ABSOLUTE instant by 86400s, so on
// DST-observing machines (this repo's dev machine runs Europe/Madrid)
// crossing 2026-03-29 / 2026-10-25 drifted the wall hour +-1h
// permanently. Iterating a full year of next-occurrences must keep
// every result at exactly hora:minuto.
final alarma = AlarmaMusical(
id: 'dst-diaria',
nombre: 'Diaria DST',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
var proxima =
servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 1, 5, 6, 0),
)!;
for (var i = 0; i < 400; i++) {
expect(
proxima.hour,
7,
reason: 'deriva de hora de pared en ${proxima.toIso8601String()}',
);
expect(proxima.minute, 30);
proxima =
servicio.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: proxima,
)!;
}
});
test('la hora de pared nunca deriva al iterar dias (DST-safe): '
'dias de semana', () {
final alarma = AlarmaMusical(
id: 'dst-semana',
nombre: 'Semana DST',
hora: 7,
minuto: 30,
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
diasSemana: const [1, 3, 5],
);
var proxima =
servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 1, 5, 6, 0),
)!;
for (var i = 0; i < 200; i++) {
expect(
proxima.hour,
7,
reason: 'deriva de hora de pared en ${proxima.toIso8601String()}',
);
expect(proxima.minute, 30);
expect(const [1, 3, 5], contains(proxima.weekday));
proxima =
servicio.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: proxima,
)!;
}
});
test('mantiene la ocurrencia diaria cuyo disparo acaba de pasar', () {
// Contract the native AlarmScheduler.computeNextTriggerMillis MUST
// mirror: a trigger that just passed within toleranciaDisparoInminente
// (90s) and has NOT been handled is kept (normalized to desde + 2s),
// never skipped to tomorrow. The native side lacking this window is
// what recomputed the next occurrence as tomorrow and cancelled the
// in-flight fire, so the alarm never rang until the screen was turned
// on. If this expectation changes, the native constant
// IMMINENT_TOLERANCE_MILLIS must change with it.
final alarma = AlarmaMusical(
id: 'a-inminente-diaria',
nombre: 'Diaria inminente',
hora: 22,
minuto: 35,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 7, 11, 22, 35, 30),
);
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
});
test('mantiene la ocurrencia por dias de semana recien pasada', () {
final alarma = AlarmaMusical(
id: 'a-inminente-semana',
nombre: 'Semana inminente',
hora: 22,
minuto: 35,
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
diasSemana: const [1, 2, 3, 4, 5, 6, 7],
);
final proxima = servicio.calcularProxima(
alarma: alarma,
desde: DateTime(2026, 7, 11, 22, 35, 30),
);
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
});
test('calcula siguiente por dias de semana despues de ejecucion', () {
final alarma = AlarmaMusical(
id: 'a7',
nombre: 'Laboral',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
diasSemana: const [DateTime.monday, DateTime.wednesday],
proximaEjecucion: DateTime(2026, 5, 25, 8),
);
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: DateTime(2026, 5, 25, 8),
);
expect(siguiente, DateTime(2026, 5, 27, 8));
});
test('alarma unica completada no calcula siguiente', () {
final alarma = AlarmaMusical(
id: 'a8',
nombre: 'Unica',
hora: 8,
minuto: 0,
tipoProgramacion: TipoProgramacionAlarma.unica,
diasSemana: const [],
fechaUnica: DateTime(2026, 5, 25),
proximaEjecucion: DateTime(2026, 5, 25, 8),
);
final siguiente = servicio.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: DateTime(2026, 5, 25, 8),
);
expect(siguiente, isNull);
});
});
}