feat(alarmas): record and clear per-alarm scheduling failures
Adds ServicioAlarmas.registrarFalloProgramacion/limpiarFalloProgramacion, persisting a scheduling-reliability failure through the same ExcepcionAlarma model saltarProxima already uses. Only one failure record is kept per alarm (latest attempt wins) and skipNext entries for any alarm are never touched. EstadoAlarmas wiring follows next.
This commit is contained in:
@@ -350,6 +350,62 @@ class ServicioAlarmas {
|
|||||||
return nuevo;
|
return nuevo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Records a scheduling-reliability failure for [alarmaId] (fix/alarmas-
|
||||||
|
/// fallos-silenciosos): reuses the SAME `ExcepcionAlarma` model
|
||||||
|
/// `saltarProxima` already persists, so `EstadoAlarmas.ultimaExcepcionPara`
|
||||||
|
/// surfaces it on the exact alarm card affected instead of only a
|
||||||
|
/// transient, alarm-agnostic message. A previous FAILURE record for the
|
||||||
|
/// same alarm is replaced (only the latest attempt's outcome matters) --
|
||||||
|
/// any `skipNext` exception for this or other alarms is left untouched.
|
||||||
|
/// Never affects scheduling: `ServicioProgramacionAlarmas._esValida` only
|
||||||
|
/// treats `tipoSaltoSiguiente` as an actual skip.
|
||||||
|
Future<ConfiguracionAlarmas> registrarFalloProgramacion(
|
||||||
|
String alarmaId,
|
||||||
|
DateTime ejecucion,
|
||||||
|
String tipo,
|
||||||
|
) => _enCola(() async {
|
||||||
|
final config = await _configActual();
|
||||||
|
final excepciones = [
|
||||||
|
..._sinFalloPrevio(config.excepciones, alarmaId),
|
||||||
|
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: ejecucion, tipo: tipo),
|
||||||
|
];
|
||||||
|
final nuevo = ConfiguracionAlarmas(
|
||||||
|
alarmas: config.alarmas,
|
||||||
|
vacaciones: config.vacaciones,
|
||||||
|
excepciones: excepciones,
|
||||||
|
);
|
||||||
|
await _guardar(nuevo);
|
||||||
|
return nuevo;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Clears any outstanding failure record for [alarmaId] (a subsequent
|
||||||
|
/// scheduling attempt succeeded). No-op when there is nothing to clear.
|
||||||
|
Future<ConfiguracionAlarmas> limpiarFalloProgramacion(
|
||||||
|
String alarmaId,
|
||||||
|
) => _enCola(() async {
|
||||||
|
final config = await _configActual();
|
||||||
|
final sinFallo = _sinFalloPrevio(config.excepciones, alarmaId);
|
||||||
|
if (sinFallo.length == config.excepciones.length) return config;
|
||||||
|
final nuevo = ConfiguracionAlarmas(
|
||||||
|
alarmas: config.alarmas,
|
||||||
|
vacaciones: config.vacaciones,
|
||||||
|
excepciones: sinFallo,
|
||||||
|
);
|
||||||
|
await _guardar(nuevo);
|
||||||
|
return nuevo;
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ExcepcionAlarma> _sinFalloPrevio(
|
||||||
|
List<ExcepcionAlarma> excepciones,
|
||||||
|
String alarmaId,
|
||||||
|
) => excepciones
|
||||||
|
.where(
|
||||||
|
(e) =>
|
||||||
|
!(e.alarmaId == alarmaId &&
|
||||||
|
ExcepcionAlarma.tiposFallo.contains(e.tipo)),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
Future<ConfiguracionAlarmas> posponerEjecucion(
|
Future<ConfiguracionAlarmas> posponerEjecucion(
|
||||||
String alarmaId,
|
String alarmaId,
|
||||||
DateTime ejecucion,
|
DateTime ejecucion,
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Coverage for the scheduling-reliability failure records (fix/alarmas-
|
||||||
|
/// fallos-silenciosos): a failed `android.programar` call is no longer only
|
||||||
|
/// a transient, alarm-agnostic `EstadoAlarmas.error` string -- it is also
|
||||||
|
/// recorded per-alarm through the SAME `ExcepcionAlarma` model `saltarProxima`
|
||||||
|
/// already uses, so `EstadoAlarmas.ultimaExcepcionPara` can surface it on the
|
||||||
|
/// exact card affected.
|
||||||
|
void main() {
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registrarFalloProgramacion agrega una excepcion de fallo para la '
|
||||||
|
'alarma indicada', () async {
|
||||||
|
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'a1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final config = await servicio.registrarFalloProgramacion(
|
||||||
|
'a1',
|
||||||
|
DateTime(2026, 5, 25, 7, 30),
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.excepciones, hasLength(1));
|
||||||
|
expect(config.excepciones.single.alarmaId, 'a1');
|
||||||
|
expect(
|
||||||
|
config.excepciones.single.tipo,
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registrarFalloProgramacion reemplaza un fallo previo de la MISMA '
|
||||||
|
'alarma en vez de acumular', () async {
|
||||||
|
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'a1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.registrarFalloProgramacion(
|
||||||
|
'a1',
|
||||||
|
DateTime(2026, 5, 25, 7, 30),
|
||||||
|
ExcepcionAlarma.tipoFalloPreaviso,
|
||||||
|
);
|
||||||
|
final config = await servicio.registrarFalloProgramacion(
|
||||||
|
'a1',
|
||||||
|
DateTime(2026, 5, 26, 7, 30),
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.excepciones, hasLength(1));
|
||||||
|
expect(
|
||||||
|
config.excepciones.single.tipo,
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('registrarFalloProgramacion NUNCA toca las excepciones skipNext de '
|
||||||
|
'otras alarmas ni de la misma', () async {
|
||||||
|
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'a1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await servicio.saltarProxima('a1');
|
||||||
|
|
||||||
|
final config = await servicio.registrarFalloProgramacion(
|
||||||
|
'a1',
|
||||||
|
DateTime(2026, 5, 26, 7, 30),
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.excepciones, hasLength(2));
|
||||||
|
expect(
|
||||||
|
config.excepciones.map((e) => e.tipo),
|
||||||
|
containsAll(<String>[
|
||||||
|
ExcepcionAlarma.tipoSaltoSiguiente,
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limpiarFalloProgramacion elimina el fallo registrado para esa '
|
||||||
|
'alarma', () async {
|
||||||
|
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'a1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await servicio.registrarFalloProgramacion(
|
||||||
|
'a1',
|
||||||
|
DateTime(2026, 5, 25, 7, 30),
|
||||||
|
ExcepcionAlarma.tipoFalloProgramacion,
|
||||||
|
);
|
||||||
|
|
||||||
|
final config = await servicio.limpiarFalloProgramacion('a1');
|
||||||
|
|
||||||
|
expect(config.excepciones, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('limpiarFalloProgramacion sin fallo previo no rompe y no persiste '
|
||||||
|
'cambios', () async {
|
||||||
|
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'a1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final config = await servicio.limpiarFalloProgramacion('a1');
|
||||||
|
|
||||||
|
expect(config.excepciones, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user