fix(alarmas): decode native failures with the real channel key names
The first pass read 'alarmaId'/'tipo' from the channel payload while the native side sends 'alarmId'/'type'/'atMillis' (AlarmScheduler.kt:1389). Every entry would have been dropped silently in production. The tests passed because the fake was seeded with the same guessed keys, so they confirmed the mistake instead of catching it. Decoding now goes through FalloProgramacionNativo.fromMap -- the single place native key names appear -- and the fixtures build through that same constructor.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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<List<Map<String, Object?>>> 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<List<FalloProgramacionNativo>> fallosNativosProgramacion();
|
||||
Future<void> ocultarNotificacionAlarma(String alarmaId);
|
||||
|
||||
/// Notification-only dismissal (RES-1): hides the fire notification for
|
||||
@@ -439,7 +446,7 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
||||
Future<List<FalloProgramacionNativo>> fallosNativosProgramacion() async {
|
||||
try {
|
||||
final raw = await _channel.invokeMethod<List<Object?>>(
|
||||
'getNativeSchedulingFailures',
|
||||
@@ -447,7 +454,8 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
if (raw == null) return const [];
|
||||
return raw
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Map<String, Object?>> fallosNativos = const [];
|
||||
List<FalloProgramacionNativo> fallosNativos = const [];
|
||||
|
||||
int lecturasFallosNativos = 0;
|
||||
|
||||
@@ -206,7 +206,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
bool fallaLecturaFallosNativos = false;
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
||||
Future<List<FalloProgramacionNativo>> fallosNativosProgramacion() async {
|
||||
lecturasFallosNativos++;
|
||||
if (fallaLecturaFallosNativos) {
|
||||
throw StateError('canal no disponible');
|
||||
|
||||
Reference in New Issue
Block a user