feat(alarmas): surface the three native scheduling failures in Dart
Completes the bridge the native side already exposed. AlarmScheduler and PluriWaveAlarmService record a pre-notice that could not be armed, a refused foreground-service start, and a per-alarm reschedule that failed after a reboot -- but nothing read them, so all three still ended at logcat. EstadoAlarmas now drains them at startup and turns each into a per-alarm exception, which the card already knows how to mark. An alarm that never reached the OS stops looking identical to one that did. The read is deliberately tolerant: a failure to read is logged and swallowed, never surfaced as an alarm error, so a diagnostics gap cannot masquerade as a scheduling problem.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
|
||||
/// The native scheduler records three failures entirely on its own, outside
|
||||
/// any Dart call: a pre-notice that could not be armed, a refused
|
||||
/// foreground-service start when the alarm should have rung, and a per-alarm
|
||||
/// reschedule that failed after a reboot.
|
||||
///
|
||||
/// Before this wiring existed, all three logged to logcat and stopped there.
|
||||
/// The alarm stayed switched on in the list, drawn exactly as if scheduling
|
||||
/// had succeeded, and simply never fired — the reported "as if there were no
|
||||
/// alarm at all". These tests pin the drain path that makes them visible.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late FakePuertoAlarmasAndroid android;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
android = FakePuertoAlarmasAndroid();
|
||||
});
|
||||
|
||||
EstadoAlarmas crearEstado() =>
|
||||
EstadoAlarmas(android: android, iniciarAutomaticamente: false);
|
||||
|
||||
test('a native pre-notice failure becomes a per-alarm exception', () async {
|
||||
android.fallosNativos = [
|
||||
{'alarmaId': 'a1', 'tipo': ExcepcionAlarma.tipoFalloPreaviso},
|
||||
];
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.cargarFallosNativos();
|
||||
|
||||
final fallo = estado.ultimaExcepcionPara('a1');
|
||||
expect(fallo, isNotNull);
|
||||
expect(fallo!.tipo, ExcepcionAlarma.tipoFalloPreaviso);
|
||||
});
|
||||
|
||||
test(
|
||||
'a refused foreground-service start becomes a per-alarm exception',
|
||||
() async {
|
||||
android.fallosNativos = [
|
||||
{'alarmaId': 'a2', 'tipo': ExcepcionAlarma.tipoFalloServicioSonido},
|
||||
];
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.cargarFallosNativos();
|
||||
|
||||
expect(
|
||||
estado.ultimaExcepcionPara('a2')?.tipo,
|
||||
ExcepcionAlarma.tipoFalloServicioSonido,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('only the alarm whose reschedule failed is marked', () async {
|
||||
android.fallosNativos = [
|
||||
{
|
||||
'alarmaId': 'a3',
|
||||
'tipo': ExcepcionAlarma.tipoFalloReprogramacionArranque,
|
||||
},
|
||||
];
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.cargarFallosNativos();
|
||||
|
||||
expect(estado.ultimaExcepcionPara('a3'), isNotNull);
|
||||
expect(
|
||||
estado.ultimaExcepcionPara('a4'),
|
||||
isNull,
|
||||
reason: 'a sibling alarm that scheduled fine must stay unmarked',
|
||||
);
|
||||
});
|
||||
|
||||
test('malformed native entries are skipped without throwing', () async {
|
||||
android.fallosNativos = [
|
||||
{'tipo': ExcepcionAlarma.tipoFalloPreaviso}, // no alarmaId
|
||||
{'alarmaId': 'a5'}, // no tipo
|
||||
];
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.cargarFallosNativos();
|
||||
|
||||
expect(estado.ultimaExcepcionPara('a5'), isNull);
|
||||
});
|
||||
|
||||
test('a failing native read never surfaces as an alarm error', () async {
|
||||
// A diagnostics gap must not look like a scheduling problem: an older
|
||||
// native build simply has no such channel method.
|
||||
android.fallaLecturaFallosNativos = true;
|
||||
final estado = crearEstado();
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.cargarFallosNativos();
|
||||
|
||||
expect(estado.error, isNull);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes_alarmas.dart';
|
||||
@@ -73,4 +74,66 @@ void main() {
|
||||
expect(estado.ultimaExcepcionPara('sana1'), isNull);
|
||||
expect(estado.error, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'inicializar importa los fallos nativos (pre-aviso, servicio en primer '
|
||||
'plano, reprogramacion tras arranque) como excepciones por alarma',
|
||||
() async {
|
||||
final android =
|
||||
FakePuertoAlarmasAndroid()
|
||||
..fallosProgramacionNativos.addAll([
|
||||
FalloProgramacionNativo(
|
||||
alarmaId: 'con-preaviso-roto',
|
||||
tipo: ExcepcionAlarma.tipoFalloPreaviso,
|
||||
ocurridoEn: DateTime(2026, 5, 25, 7),
|
||||
),
|
||||
FalloProgramacionNativo(
|
||||
alarmaId: 'servicio-rechazado',
|
||||
tipo: ExcepcionAlarma.tipoFalloServicioSonido,
|
||||
ocurridoEn: DateTime(2026, 5, 25, 7),
|
||||
),
|
||||
]);
|
||||
final servicio = ServicioAlarmas(
|
||||
reloj: () => DateTime(2026, 5, 25, 7, 30),
|
||||
);
|
||||
await servicio.guardarAlarma(
|
||||
AlarmaMusical(
|
||||
id: 'con-preaviso-roto',
|
||||
nombre: 'Diaria',
|
||||
hora: 7,
|
||||
minuto: 30,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: const [],
|
||||
),
|
||||
);
|
||||
await servicio.guardarAlarma(
|
||||
AlarmaMusical(
|
||||
id: 'servicio-rechazado',
|
||||
nombre: 'Diaria',
|
||||
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();
|
||||
|
||||
final falloPreaviso = estado.ultimaExcepcionPara('con-preaviso-roto');
|
||||
expect(falloPreaviso, isNotNull);
|
||||
expect(falloPreaviso!.tipo, ExcepcionAlarma.tipoFalloPreaviso);
|
||||
|
||||
final falloServicio = estado.ultimaExcepcionPara('servicio-rechazado');
|
||||
expect(falloServicio, isNotNull);
|
||||
expect(falloServicio!.tipo, ExcepcionAlarma.tipoFalloServicioSonido);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
final soloOcultadas = <String>[];
|
||||
final ejecucionesNativas = <EjecucionAlarmaNativa>[];
|
||||
final snoozesNativos = <EstadoSnoozeNativo>[];
|
||||
final fallosProgramacionNativos = <FalloProgramacionNativo>[];
|
||||
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
|
||||
bool ignoraOptimizacionBateria = true;
|
||||
int solicitudesExencionBateria = 0;
|
||||
@@ -186,9 +187,33 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo() async =>
|
||||
List.of(snoozesNativos);
|
||||
|
||||
@override
|
||||
Future<List<FalloProgramacionNativo>>
|
||||
obtenerFallosProgramacionNativos() async =>
|
||||
List.of(fallosProgramacionNativos);
|
||||
|
||||
int solicitudesPermisoAlarmasExactas = 0;
|
||||
int solicitudesPermisoPantallaCompleta = 0;
|
||||
|
||||
/// Native-recorded failures the next read should return. Tests seed this
|
||||
/// to simulate a pre-notice that never armed, a refused foreground-service
|
||||
/// start, or a per-alarm reschedule that failed after a reboot.
|
||||
List<Map<String, Object?>> fallosNativos = const [];
|
||||
|
||||
int lecturasFallosNativos = 0;
|
||||
|
||||
/// Simulates an older native build with no such channel method.
|
||||
bool fallaLecturaFallosNativos = false;
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
||||
lecturasFallosNativos++;
|
||||
if (fallaLecturaFallosNativos) {
|
||||
throw StateError('canal no disponible');
|
||||
}
|
||||
return fallosNativos;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> solicitarPermisoAlarmasExactas() async {
|
||||
solicitudesPermisoAlarmasExactas++;
|
||||
|
||||
@@ -124,11 +124,42 @@ void main() {
|
||||
ExcepcionAlarma.tipoFalloProgramacion,
|
||||
);
|
||||
|
||||
final config = await servicio.limpiarFalloProgramacion('a1');
|
||||
final config = await servicio.limpiarFalloProgramacion(
|
||||
'a1',
|
||||
ExcepcionAlarma.tipoFalloProgramacion,
|
||||
);
|
||||
|
||||
expect(config.excepciones, isEmpty);
|
||||
});
|
||||
|
||||
test('limpiarFalloProgramacion NO limpia un fallo de un tipo distinto '
|
||||
'(cada subsistema se limpia por su cuenta)', () 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.limpiarFalloProgramacion(
|
||||
'a1',
|
||||
ExcepcionAlarma.tipoFalloProgramacion,
|
||||
);
|
||||
|
||||
expect(config.excepciones, hasLength(1));
|
||||
expect(config.excepciones.single.tipo, ExcepcionAlarma.tipoFalloPreaviso);
|
||||
});
|
||||
|
||||
test('limpiarFalloProgramacion sin fallo previo no rompe y no persiste '
|
||||
'cambios', () async {
|
||||
final servicio = ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7));
|
||||
@@ -143,7 +174,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final config = await servicio.limpiarFalloProgramacion('a1');
|
||||
final config = await servicio.limpiarFalloProgramacion(
|
||||
'a1',
|
||||
ExcepcionAlarma.tipoFalloProgramacion,
|
||||
);
|
||||
|
||||
expect(config.excepciones, isEmpty);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user