diff --git a/lib/estado/estado_alarmas.dart b/lib/estado/estado_alarmas.dart index e6a7636..77937d0 100644 --- a/lib/estado/estado_alarmas.dart +++ b/lib/estado/estado_alarmas.dart @@ -556,10 +556,7 @@ class EstadoAlarmas extends ChangeNotifier { try { final fallos = await android.fallosNativosProgramacion(); for (final fallo in fallos) { - final alarmaId = fallo['alarmaId'] as String?; - final tipo = fallo['tipo'] as String?; - if (alarmaId == null || tipo == null) continue; - await _registrarFalloProgramacion(alarmaId, tipo: tipo); + await _registrarFalloProgramacion(fallo.alarmaId, tipo: fallo.tipo); } if (fallos.isNotEmpty) { debugPrint( diff --git a/lib/servicios/servicio_alarmas_android.dart b/lib/servicios/servicio_alarmas_android.dart index 5cf4f32..d854ee3 100644 --- a/lib/servicios/servicio_alarmas_android.dart +++ b/lib/servicios/servicio_alarmas_android.dart @@ -209,7 +209,14 @@ abstract class PuertoAlarmasAndroid { /// Before this existed every one of those paths logged to logcat and /// stopped there, so an alarm could sit switched on in the list having /// never reached the OS at all — the user's "as if there were no alarm". - Future>> fallosNativosProgramacion(); + /// + /// Returns the typed model rather than raw maps ON PURPOSE: + /// [FalloProgramacionNativo.fromMap] is the single place the native key + /// names (`alarmId`/`type`/`atMillis`) appear. Consuming raw maps here + /// once silently dropped every entry, because the caller guessed Spanish + /// key names and the fake was seeded with the same guess — the test + /// confirmed the mistake instead of catching it. + Future> fallosNativosProgramacion(); Future ocultarNotificacionAlarma(String alarmaId); /// Notification-only dismissal (RES-1): hides the fire notification for @@ -439,7 +446,7 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid { } @override - Future>> fallosNativosProgramacion() async { + Future> fallosNativosProgramacion() async { try { final raw = await _channel.invokeMethod>( 'getNativeSchedulingFailures', @@ -447,7 +454,8 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid { if (raw == null) return const []; return raw .whereType>() - .map((m) => m.map((k, v) => MapEntry(k.toString(), v))) + .map(FalloProgramacionNativo.fromMap) + .where((f) => f.alarmaId.isNotEmpty && f.tipo.isNotEmpty) .toList(); } catch (e) { // Never let a diagnostics read break alarm handling: an older build diff --git a/test/estado/estado_alarmas_fallos_nativos_test.dart b/test/estado/estado_alarmas_fallos_nativos_test.dart index 6e5df37..53567e3 100644 --- a/test/estado/estado_alarmas_fallos_nativos_test.dart +++ b/test/estado/estado_alarmas_fallos_nativos_test.dart @@ -1,6 +1,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_android.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/fakes_alarmas.dart'; @@ -14,6 +15,12 @@ import '../helpers/fakes_alarmas.dart'; /// 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. +/// +/// They deliberately build fixtures through [FalloProgramacionNativo.fromMap] +/// using the REAL native key names (`alarmId`/`type`/`atMillis`, see +/// `AlarmScheduler.kt:1389-1391`). An earlier draft seeded the fake with +/// guessed Spanish keys and consumed the same guess, so every entry would +/// have been silently dropped in production while the tests stayed green. void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -27,9 +34,24 @@ void main() { EstadoAlarmas crearEstado() => EstadoAlarmas(android: android, iniciarAutomaticamente: false); + /// Mirrors exactly what the native side puts on the channel. + FalloProgramacionNativo falloNativo(String alarmaId, String tipo) => + FalloProgramacionNativo.fromMap({ + 'alarmId': alarmaId, + 'type': tipo, + 'atMillis': 1700000000000, + }); + + test('the fixture helper decodes the real native key names', () { + final fallo = falloNativo('a0', ExcepcionAlarma.tipoFalloPreaviso); + + expect(fallo.alarmaId, 'a0'); + expect(fallo.tipo, ExcepcionAlarma.tipoFalloPreaviso); + }); + test('a native pre-notice failure becomes a per-alarm exception', () async { android.fallosNativos = [ - {'alarmaId': 'a1', 'tipo': ExcepcionAlarma.tipoFalloPreaviso}, + falloNativo('a1', ExcepcionAlarma.tipoFalloPreaviso), ]; final estado = crearEstado(); addTearDown(estado.dispose); @@ -45,7 +67,7 @@ void main() { 'a refused foreground-service start becomes a per-alarm exception', () async { android.fallosNativos = [ - {'alarmaId': 'a2', 'tipo': ExcepcionAlarma.tipoFalloServicioSonido}, + falloNativo('a2', ExcepcionAlarma.tipoFalloServicioSonido), ]; final estado = crearEstado(); addTearDown(estado.dispose); @@ -61,10 +83,7 @@ void main() { test('only the alarm whose reschedule failed is marked', () async { android.fallosNativos = [ - { - 'alarmaId': 'a3', - 'tipo': ExcepcionAlarma.tipoFalloReprogramacionArranque, - }, + falloNativo('a3', ExcepcionAlarma.tipoFalloReprogramacionArranque), ]; final estado = crearEstado(); addTearDown(estado.dispose); @@ -79,19 +98,6 @@ void main() { ); }); - 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. diff --git a/test/helpers/fakes_alarmas.dart b/test/helpers/fakes_alarmas.dart index 3ec140e..b9a72d2 100644 --- a/test/helpers/fakes_alarmas.dart +++ b/test/helpers/fakes_alarmas.dart @@ -198,7 +198,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid { /// 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> fallosNativos = const []; + List fallosNativos = const []; int lecturasFallosNativos = 0; @@ -206,7 +206,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid { bool fallaLecturaFallosNativos = false; @override - Future>> fallosNativosProgramacion() async { + Future> fallosNativosProgramacion() async { lecturasFallosNativos++; if (fallaLecturaFallosNativos) { throw StateError('canal no disponible');