fix(alarmas): wire scheduling failures into per-alarm exceptions
guardarAlarma/posponerAlarma/posponerProximaDesdePreaviso now record a scheduling failure via ServicioAlarmas.registrarFalloProgramacion on catch and clear it on a successful (re)schedule, in addition to the existing transient EstadoAlarmas.error string. This makes the failure visible per-alarm via ultimaExcepcionPara instead of only a generic app-wide message. Also fixes _sincronizarTodas: a single alarm's android.programar throw used to abort the whole loop, silently skipping every sibling alarm scheduled AFTER it on that pass (including on every app launch, via inicializar). Each alarm's outcome is now independent.
This commit is contained in:
@@ -611,6 +611,145 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'guardarAlarma: cuando android.programar falla, marca la alarma con una '
|
||||
'excepcion de fallo visible via ultimaExcepcionPara',
|
||||
() async {
|
||||
final android = FakePuertoAlarmasAndroid()..fallaProgramar = true;
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
addTearDown(android.dispose);
|
||||
|
||||
await estado.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'fallo1',
|
||||
nombre: 'Diaria',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
|
||||
final excepcion = estado.ultimaExcepcionPara('fallo1');
|
||||
expect(excepcion, isNotNull);
|
||||
expect(excepcion!.tipo, ExcepcionAlarma.tipoFalloProgramacion);
|
||||
expect(estado.error, isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'guardarAlarma: un reintento exitoso limpia la excepcion de fallo previa',
|
||||
() async {
|
||||
final android = FakePuertoAlarmasAndroid()..fallaProgramar = true;
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
addTearDown(android.dispose);
|
||||
final alarma = const AlarmaMusical(
|
||||
id: 'fallo2',
|
||||
nombre: 'Diaria',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
);
|
||||
await estado.guardarAlarma(alarma);
|
||||
expect(estado.ultimaExcepcionPara('fallo2'), isNotNull);
|
||||
|
||||
android.fallaProgramar = false;
|
||||
await estado.guardarAlarma(estado.alarmas.single);
|
||||
|
||||
expect(estado.ultimaExcepcionPara('fallo2'), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'guardarAlarma en el camino feliz nunca registra una excepcion de fallo',
|
||||
() async {
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
addTearDown(android.dispose);
|
||||
|
||||
await estado.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'ok1',
|
||||
nombre: 'Diaria',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
|
||||
expect(estado.ultimaExcepcionPara('ok1'), isNull);
|
||||
expect(estado.error, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'inicializar: un fallo de programacion en UNA alarma no aborta la '
|
||||
'sincronizacion de las demas (S-sincronizarTodas continua tras error)',
|
||||
() async {
|
||||
final android = FakePuertoAlarmasAndroid()
|
||||
..idsFallanProgramar.add('rota');
|
||||
final servicio = ServicioAlarmas(
|
||||
reloj: () => DateTime(2026, 5, 25, 6, 0),
|
||||
);
|
||||
await servicio.guardarAlarma(
|
||||
AlarmaMusical(
|
||||
id: 'rota',
|
||||
nombre: 'Rota',
|
||||
hora: 7,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
),
|
||||
);
|
||||
await servicio.guardarAlarma(
|
||||
AlarmaMusical(
|
||||
id: 'sana',
|
||||
nombre: 'Sana',
|
||||
hora: 8,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
),
|
||||
);
|
||||
|
||||
final estado = EstadoAlarmas(
|
||||
servicio: servicio,
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
addTearDown(android.dispose);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(
|
||||
android.programadas.map((a) => a.id),
|
||||
contains('sana'),
|
||||
reason:
|
||||
'la alarma sana debe seguir programandose aunque la rota falle',
|
||||
);
|
||||
expect(estado.ultimaExcepcionPara('rota'), isNotNull);
|
||||
expect(estado.ultimaExcepcionPara('sana'), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
group('EstadoAlarmas — consultas de vacaciones (ADR-6, WU9)', () {
|
||||
test('rangoVacacionesActivo devuelve el rango cuyo intervalo incluye '
|
||||
'"ahora" (dias restantes derivables de finDia), o null si ninguno '
|
||||
|
||||
@@ -41,6 +41,12 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
/// could not otherwise produce.
|
||||
bool fallaProgramar = false;
|
||||
|
||||
/// Test-only PER-ALARM failure switch (fix/alarmas-fallos-silenciosos):
|
||||
/// [programar] throws only for ids in this set, letting a test simulate
|
||||
/// one alarm failing to schedule while its siblings succeed -- the global
|
||||
/// [fallaProgramar] switch cannot express that (it fails everything).
|
||||
final Set<String> idsFallanProgramar = {};
|
||||
|
||||
/// Test-only failure switch: when true, [detenerSonidoActivo] reports an
|
||||
/// unconfirmed/failed stop instead of a confirmed one.
|
||||
bool fallaDetener = false;
|
||||
@@ -73,7 +79,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
|
||||
@override
|
||||
Future<void> programar(AlarmaMusical alarma) async {
|
||||
if (fallaProgramar) {
|
||||
if (fallaProgramar || idsFallanProgramar.contains(alarma.id)) {
|
||||
throw StateError('fake programar failure');
|
||||
}
|
||||
programadas.add(alarma);
|
||||
|
||||
Reference in New Issue
Block a user