From 48e74e6bfeec917c88db1dc71ddb17e93b960461 Mon Sep 17 00:00:00 2001 From: freetlab Date: Sat, 11 Jul 2026 17:19:42 +0200 Subject: [PATCH] fix(radio): treat custom-station path resolution as part of the IO surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _cargarEmisorasCustom resolved the file path outside the IO guard, so a throw from the resolver escaped into _init()'s Future.wait and took the sibling loads (populares, favoritos, grupos) down with it — a gap the old catch-all used to cover. Path resolution now gets the same IO-fail treatment as an unreadable file: degraded flag, logged skip, siblings unaffected. --- lib/estado/estado_radio.dart | 17 ++++++++++++++++- test/estado/estado_radio_test.dart | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index 7a53953..d052a22 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -529,7 +529,22 @@ class EstadoRadio extends ChangeNotifier { /// shared [parseListaTolerante] (D1): survivors are kept, no flag/ /// quarantine at all. Future _cargarEmisorasCustom() async { - final archivo = await _archivoCustom(); + // Resolving the path itself is part of the IO surface: a throw here must + // get the same IO-fail treatment as an unreadable file, not escape into + // _init()'s Future.wait and take the sibling loads down with it. + final File archivo; + try { + archivo = await _archivoCustom(); + } catch (e) { + _customDegradado = true; + registrarSaltoPersistencia( + subsistema: 'emisoras_custom', + detalle: 'resolucion de ruta', + razon: e.toString(), + ); + notifyListeners(); + return; + } final contenido = await _leerContenidoCustom(archivo); if (contenido == null) return; // ya resuelto: vacio o degradado por IO. diff --git a/test/estado/estado_radio_test.dart b/test/estado/estado_radio_test.dart index 451b49c..977d165 100644 --- a/test/estado/estado_radio_test.dart +++ b/test/estado/estado_radio_test.dart @@ -408,6 +408,30 @@ void main() { }, ); + test( + 'si resolver la ruta del archivo custom falla, la inicializacion ' + 'sobrevive y las cargas hermanas no se pierden (D5 IO-fail)', + () async { + final estado = EstadoRadio( + audio: FakeServicioAudio(), + favoritos: FakeServicioFavoritos(), + radio: FakeServicioRadio(), + servicioEcualizador: FakeServicioEcualizador(), + resolverArchivoCustom: + () async => throw const FileSystemException('sin storage'), + iniciarAutomaticamente: false, + ); + + // Path resolution failing must be treated as an IO-fail, not + // escape _cargarEmisorasCustom: it runs inside _init()'s + // Future.wait, so an uncaught throw would also reject the + // sibling loads (populares/favoritos/grupos). + await estado.inicializar(); + + expect(estado.emisorasCustom, isEmpty); + }, + ); + test( 'JSON invalido al nivel superior pone en cuarentena el archivo ' 'original (D5 parse-fail)',