# Delta for multi-device-eq No prior `openspec/specs/multi-device-eq/spec.md` exists. This delta adds requirements as new (ADDED), scoped to disconnect/cold-start revert only. Connect-path and 4-level-hierarchy requirements are out of scope (already covered by the passing test suite). ## ADDED Requirements ### Requirement: Device-Event Resolve-and-Apply The system MUST treat every device-change event — connect or disconnect — identically: set the now-active device as current, re-resolve via the existing 4-level hierarchy (matrix -> station -> device -> global), and APPLY via `ServicioAudio.aplicarPreset()`. The system MUST NOT branch on event type; no add/remove discriminator exists on the event, and none is required. #### Scenario: Connect applies the device's own preset - GIVEN `_eqMultiDeviceEnabled` is true and a station is active - WHEN a device-change event arrives for a device with a resolvable preset - THEN the resolved preset becomes `presetActual` - AND `audio.aplicarPreset()` is called with it (asserted via `FakeServicioAudio.presetsAplicados`) #### Scenario: Disconnect resolves and applies the base preset - GIVEN `_eqMultiDeviceEnabled` is true, a BT device is active with its own resolved preset, and `builtin_speaker` resolves to a distinct preset - WHEN a device-change event arrives carrying `builtin_speaker` (simulating disconnect-to-base via `FakeServicioDispositivoAudio.emitirDispositivo`, which carries no add/remove flag) - THEN `_dispositivoActualId` becomes `'builtin_speaker'` - AND the hierarchy resolution for `builtin_speaker` is applied - AND `audio.aplicarPreset()` records it as the last entry in `presetsAplicados` (proves push, not state-only mutation) ### Requirement: First-Seen Bootstrap Must Not Hijack Base-Device Resolution The system MUST NOT force-copy a stale resolved preset into `presetsDispositivo['builtin_speaker']` on disconnect when it has no existing entry. The base speaker is excluded from first-seen bootstrap entirely: no `presetsDispositivo['builtin_speaker']` entry is ever auto-created, so the base device always resolves live through the hierarchy (a persistent device-level entry would mask later global-preset edits, since level 3 beats level 4 on every subsequent resolution). #### Scenario: Disconnect to a never-seen builtin_speaker resolves from the hierarchy - GIVEN `_eqMultiDeviceEnabled` is true, `presetsDispositivo` has no entry for `builtin_speaker`, a BT device is active resolved to `jazz`, and global `_presetPrincipal` is `rock` - WHEN the device-change event carries `builtin_speaker` - THEN no `presetsDispositivo['builtin_speaker']` entry is created, and resolution falls through to the global preset `rock`, NOT `jazz` - AND `audio.aplicarPreset()` is called with `rock` - AND a later change of the global preset while on `builtin_speaker` re-resolves to the new global value (no stale level-3 entry masks it) ### Requirement: Cold Start Applies Base Resolution, Never a Stale Persisted Preset The system MUST guarantee that cold start with no external device connected applies the phone/base resolution for `builtin_speaker` through `audio.aplicarPreset()`, never a stale persisted preset for a previously-connected, now-inactive device. #### Scenario: Cold start with no device connected applies the base preset - GIVEN `_eqMultiDeviceEnabled` is true, `presetsDispositivo` has a stale entry for a BT device id that is NOT currently connected, and `obtenerDispositivoActual()` resolves to `builtin_speaker` (its default when no device was ever emitted) - WHEN `cargarPersistido()` runs (`_sembrarDispositivoActual()` -> `_onDispositivoCambiado()`) - THEN `_dispositivoActualId` becomes `'builtin_speaker'` - AND `presetActual` is the hierarchy resolution for `builtin_speaker` - AND `audio.aplicarPreset()` records that resolution, NOT the stale BT device's preset ### Requirement: Connect-Disconnect-Reconnect Cycle Lands on the Correct Preset at Each Step The system MUST resolve and apply the correct preset at every step of a connect -> disconnect -> reconnect cycle, honoring station and matrix overrides, and MUST NOT let disconnect corrupt state such that reconnect resolves incorrectly. #### Scenario: Full cycle honors station/matrix overrides at each step - GIVEN a matrix override for `(stationUuid, btDevice.id)` = `bassBoost`, and `builtin_speaker` resolves to global `flat` - WHEN the device stream emits: BT device (connect) -> `builtin_speaker` (disconnect) -> BT device (reconnect) - THEN after connect, `presetActual` is `bassBoost`; after disconnect, `flat`; after reconnect, `bassBoost` again — each recorded as the last entry in `presetsAplicados` - AND the BT device's `presetsDispositivo` entry is UNCHANGED by the disconnect step (consistent with existing test 5.5b: reconnect must not overwrite a user-saved device preset) ### Requirement: Toggle-Off Behavior Is Byte-for-Byte Unchanged When `_eqMultiDeviceEnabled` is false, device-change events MUST NOT alter resolution: no device subscription is active, no first-seen bootstrap runs, and resolution stays 2-level (station -> global) only. #### Scenario: Toggle off ignores device events entirely - GIVEN `_eqMultiDeviceEnabled` is false - WHEN a device-change event is emitted (any device, including `builtin_speaker`) - THEN `_onDispositivoCambiado` returns immediately with no state change - AND `presetEfectivo(stationUuid, deviceId)` still resolves via the 2-level path exactly as before this change - AND `presetsAplicados` gains no new entry from the event ## Testability All scenarios are Dart-testable via existing fakes in `test/helpers/fakes.dart` — no new fake infrastructure required: - `FakeServicioDispositivoAudio.emitirDispositivo(dispositivo)` — pushes any `DispositivoAudio` through `onDispositivoCambiado`; used identically for simulated connect and disconnect (no discriminator on the real stream). - `FakeServicioDispositivoAudio.obtenerDispositivoActual()` — defaults to `builtin_speaker`; used for the cold-start scenario. - `FakeServicioAudio.presetsAplicados` — proves gains were PUSHED via `aplicarPreset()`, not just mutated in state. Every scenario asserts this list, not only `presetActual`. - `FakeServicioEcualizador` constructor params (`principal`, `porEmisora`, `presetsDispositivo`, `presetsMatriz`) — seed the hierarchy as the existing Phase 5 suite already does. ## Localization No new l10n strings are introduced by this change.