import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/controlador_reconexion.dart'; /// Local-music design open question (`android-auto-local-music-phase3` /// design.md "Open Questions"): local-track source errors during /// queue-advance (a dead/moved/permission-revoked file) enter the SAME /// `ControladorReconexion` bounded-retry machine live radio uses, unchanged /// — the design's recommendation is to leave `ControladorReconexion` /// untouched rather than add queue-awareness to the sensitive error path /// (Phase 5 task 5.1/5.2). /// /// [ControladorReconexion.registrarFallo] takes NO source-type parameter /// (confirmed by reading `controlador_reconexion.dart` — Phase 5 task 5.1's /// static review), so it structurally CANNOT special-case a local-track /// error vs a radio error: this file proves the bounded-retry contract /// directly against the controller (source-agnostic by construction), with /// the DEFAULT `maxReintentos: 5` this design decision relies on — a /// "dead local track" retries up to 5 times then fails cleanly, with no 6th /// retry, no hang, and no crash. /// /// Kept as a SEPARATE file from `servicio_audio_reconnect_test.dart` /// (rather than adding a case there) so that protected regression suite's /// exact pass count stays byte-identical across this change (Phase 5 task /// 5.4's exact-count re-verification gate). class _TemporizadorFalso implements Timer { _TemporizadorFalso(this.duracion, this.callback); final Duration duracion; final void Function() callback; bool cancelado = false; @override void cancel() => cancelado = true; @override bool get isActive => !cancelado; @override int get tick => 0; } void main() { group( 'ControladorReconexion — pista local muerta (design open question, ' 'maxReintentos default = 5)', () { test( '5 fallos consecutivos => reintentar x5 con backoff, luego agotado ' 'en el 6to, SIN programar un 6to reintento, sin lanzar (no hang, ' 'no crash)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = ControladorReconexion( crearTemporizador: (duracion, callback) { final timer = _TemporizadorFalso(duracion, callback); temporizadores.add(timer); return timer; }, ); for (var i = 1; i <= 5; i++) { final decision = controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); expect( decision, DecisionReconexion.reintentar, reason: 'el intento $i de 5 debe programar un reintento', ); } expect(controlador.intentos, 5); expect(temporizadores, hasLength(5)); final sexto = controlador.registrarFallo( intencionReproducir: true, alReintentar: () => fail('no debe programar un 6to reintento tras agotar'), ); expect(sexto, DecisionReconexion.agotado); expect( temporizadores, hasLength(5), reason: 'ningún temporizador nuevo se creó para el 6to fallo', ); expect( controlador.reintentoPendiente, isFalse, reason: 'no queda ningún reintento pendiente tras agotar (sin ' 'hang)', ); }, ); test( 'tras agotar, TODOS los temporizadores (los 5 backoff + el estado ' 'final) quedan cancelados — ninguno sigue activo (no hang)', () { final temporizadores = <_TemporizadorFalso>[]; final controlador = ControladorReconexion( crearTemporizador: (duracion, callback) { final timer = _TemporizadorFalso(duracion, callback); temporizadores.add(timer); return timer; }, ); for (var i = 1; i <= 5; i++) { controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); } controlador.registrarFallo( intencionReproducir: true, alReintentar: () => fail('agotado: no debe reintentar'), ); expect( temporizadores.every((t) => t.cancelado), isTrue, reason: 'cada temporizador se cancela cuando el siguiente fallo ' 'programa uno nuevo, y el último se cancela explícitamente ' 'al agotar (ControladorReconexion.cancelar())', ); }, ); test( 'registrarFallo no recibe ningún parámetro de tipo de fuente — ' 'estructuralmente no puede distinguir una pista local de una ' 'emisora de radio (Phase 5 task 5.1, revisión estática confirmada ' 'por este test de firma)', () { // Documentación ejecutable: si esta llamada compila con // EXACTAMENTE estos 2 named parameters, la firma no tiene (ni // tuvo) un parámetro de tipo de fuente agregado. final controlador = ControladorReconexion(); final decision = controlador.registrarFallo( intencionReproducir: true, alReintentar: () {}, ); expect(decision, DecisionReconexion.reintentar); }, ); }, ); }