fix(eq): resolve base-speaker preset live instead of pinning a stale copy
_onDispositivoCambiado() bootstrapped a device-level preset entry for every never-seen device id, including the built-in speaker. That persistent level-3 entry masked later global-preset edits (level 3 beats level 4 on every resolution), so disconnecting a BT device or cold-starting without one could leave the EQ stuck on an outdated copy instead of the current global preset. The base speaker is now excluded from the first-seen bootstrap: disconnect and cold start always resolve through the live hierarchy. BT/wired/USB devices keep their bootstrap behavior unchanged.
This commit is contained in:
@@ -333,6 +333,220 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase D: device disconnect / cold-start revert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('device disconnect / cold-start revert (Phase D)', () {
|
||||
const stationUuid = 'station-uuid-disconnect';
|
||||
const btDevice = DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'BT Speaker',
|
||||
);
|
||||
const builtinSpeaker = DispositivoAudio(
|
||||
id: 'builtin_speaker',
|
||||
tipo: TipoDispositivo.altavozInterno,
|
||||
nombre: 'Speaker',
|
||||
);
|
||||
|
||||
test(
|
||||
'D.1 disconnect resolves and applies the base (builtin_speaker) preset',
|
||||
() async {
|
||||
final fakeAudio = FakeServicioAudio();
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: fakeAudio,
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.flat,
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {btDevice.id: PresetEcualizador.bassBoost},
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// Connect BT first — active device now resolves to its own preset.
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(eq.presetActual, equals(PresetEcualizador.bassBoost));
|
||||
|
||||
// Disconnect: platform emits the fallback device (builtin_speaker).
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
// builtin_speaker has no station/matrix/device entry -> global (flat).
|
||||
expect(eq.presetActual, equals(PresetEcualizador.flat));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.flat));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'D.2 disconnect-created builtin_speaker entry must not mask a later '
|
||||
'global preset change (bootstrap-skip proof, not a value tautology)',
|
||||
() async {
|
||||
final fakeAudio = FakeServicioAudio();
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: fakeAudio,
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.rock,
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {btDevice.id: PresetEcualizador.jazz},
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// BT active with its own preset (jazz).
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
// Disconnect to builtin_speaker (never seen before): first-seen
|
||||
// bootstrap must NOT persist an entry for it, so a subsequent global
|
||||
// change is never masked by a stale L3 copy.
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
// Change the global preset after the disconnect.
|
||||
await eq.cambiarPresetPrincipal(PresetEcualizador.pop);
|
||||
|
||||
// Re-trigger resolution for builtin_speaker (e.g. another
|
||||
// disconnect/device-changed event). If bootstrap wrote an entry on
|
||||
// the first emit above, this second emit would resolve the STALE
|
||||
// value (rock) instead of the fresh global (pop) — proving the
|
||||
// masking bug. This assertion only passes when no L3 entry for
|
||||
// builtin_speaker was ever created.
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(eq.presetsDispositivo.containsKey('builtin_speaker'), isFalse);
|
||||
expect(eq.presetActual, equals(PresetEcualizador.pop));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.pop));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'D.3 cold start with no device connected applies base resolution, '
|
||||
'never a stale persisted device preset',
|
||||
() async {
|
||||
final fakeAudio = FakeServicioAudio();
|
||||
// No emitirDispositivo call: obtenerDispositivoActual() defaults to
|
||||
// builtin_speaker, simulating a cold start with nothing connected.
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: fakeAudio,
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.rock,
|
||||
eqMultiDeviceEnabled: true,
|
||||
// Stale entry for a BT device that is NOT currently connected.
|
||||
presetsDispositivo: {btDevice.id: PresetEcualizador.jazz},
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
|
||||
await eq.cargarPersistido();
|
||||
|
||||
expect(eq.dispositivoActualId, equals('builtin_speaker'));
|
||||
expect(eq.presetActual, equals(PresetEcualizador.rock));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.rock));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'D.4 connect -> disconnect -> reconnect cycle lands on the correct '
|
||||
'preset at each step and never corrupts the device entry',
|
||||
() async {
|
||||
final fakeAudio = FakeServicioAudio();
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: fakeAudio,
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.flat,
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsMatriz: {'$stationUuid:${btDevice.id}': PresetEcualizador.bassBoost},
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// Connect: matrix override wins. BT is never-seen, so its own
|
||||
// first-seen bootstrap fires here too (unrelated to this change --
|
||||
// only builtin_speaker is excluded from bootstrap).
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(eq.presetActual, equals(PresetEcualizador.bassBoost));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.bassBoost));
|
||||
final btPresetTrasConectar = eq.presetsDispositivo[btDevice.id];
|
||||
expect(btPresetTrasConectar, isNotNull);
|
||||
|
||||
// Disconnect: builtin_speaker has no matrix/station/device entry -> global.
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(eq.presetActual, equals(PresetEcualizador.flat));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.flat));
|
||||
|
||||
// Reconnect: matrix override wins again.
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
expect(eq.presetActual, equals(PresetEcualizador.bassBoost));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.bassBoost));
|
||||
|
||||
// The disconnect step must never have touched the BT device's own
|
||||
// entry (locks existing test 5.5b: reconnect must not overwrite it).
|
||||
expect(
|
||||
eq.presetsDispositivo[btDevice.id],
|
||||
equals(btPresetTrasConectar),
|
||||
);
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'D.5 station override still wins over global on disconnect-to-base',
|
||||
() async {
|
||||
final fakeAudio = FakeServicioAudio();
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: fakeAudio,
|
||||
servicio: FakeServicioEcualizador(
|
||||
principal: PresetEcualizador.flat,
|
||||
porEmisora: {stationUuid: PresetEcualizador.rock},
|
||||
eqMultiDeviceEnabled: true,
|
||||
presetsDispositivo: {btDevice.id: PresetEcualizador.jazz},
|
||||
),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => stationUuid,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// BT active. Note the station override (Level 2) already applies to
|
||||
// every device on this station, including BT -- resolution is rock
|
||||
// regardless of BT's own Level-3 entry, which is expected hierarchy
|
||||
// behavior (Level 2 beats Level 3) and not itself under test here.
|
||||
fakeDispositivo.emitirDispositivo(btDevice);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
// Disconnect to builtin_speaker: station override (Level 2) must
|
||||
// still beat global (Level 4) -- deterministic revert, not a flatten
|
||||
// to global.
|
||||
fakeDispositivo.emitirDispositivo(builtinSpeaker);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(eq.presetActual, equals(PresetEcualizador.rock));
|
||||
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.rock));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 5.5–5.6: First-seen device copies current preset
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user