Files
pluriwave/openspec/changes/archive/2026-07-10-eq-device-disconnect-revert/verify-report.md
T
FreeTLab 8f7ca8059b
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s
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.
2026-07-10 23:54:01 +02:00

12 KiB

Verification Report

Change: eq-device-disconnect-revert Version: N/A (delta spec, no prior openspec/specs/multi-device-eq/spec.md) Mode: Strict TDD

Completeness

Metric Value
Tasks total 16
Tasks complete 15
Tasks incomplete 1 (5.1, manual on-device sanity, intentionally left open as a non-blocking human gate)

Build and Tests Execution

Build: not run (explicitly excluded from this verification per instruction; flutter build must never be run in this phase)

Tests: 43 passed / 0 failed (target file); 68 passed / 0 failed (target file plus 3 related regression files)

flutter test test/estado/estado_ecualizador_test.dart --reporter expanded
-> 43/43 passed, including D.1-D.5 (new), 5.5a/5.5b, 5.7a/5.8b, 2.1-2.4, rename API, guardarPresetDispositivo groups

flutter test test/estado/estado_ecualizador_test.dart test/servicios/servicio_audio_eq_reapply_test.dart test/servicios/servicio_ecualizador_test.dart test/pantallas/pantalla_ajustes_test.dart --reporter expanded
-> 68/68 passed

flutter test --reporter compact   (attempted full suite)
-> TIMED OUT / hung at ~300s. Confirmed known unrelated flake: runaway log spam originating in
   test/estado/estado_alarmas_ejecuciones_test.dart and test/estado/estado_alarmas_snooze_test.dart
   (repeating log line once per simulated minute). Process terminated cleanly by tool timeout; verified
   no orphaned dart.exe processes afterward. Fell back to explicit targeted runs per instructions.
   Mode used: TARGETED FALLBACK, not full suite.

Coverage: Diff-region (the 2 actual changed executable lines): 100 percent. Whole-file baseline (this test file alone): 59.3 percent (118/199 lines), pre-existing, not moved backward by this change. See Changed File Coverage below.

Spec Compliance Matrix

Requirement Scenario Test Result
Device-Event Resolve-and-Apply Connect applies the active device preset estado_ecualizador_test.dart, test 5.3b (pre-existing, unmodified) COMPLIANT
Device-Event Resolve-and-Apply Disconnect resolves and applies the base preset estado_ecualizador_test.dart, test D.1 COMPLIANT
First-Seen Bootstrap Must Not Hijack Base-Device Resolution Disconnect to a never-seen builtin_speaker resolves from the hierarchy estado_ecualizador_test.dart, test D.2 PARTIAL, see WARNING-1
Cold Start Applies Base Resolution, Never a Stale Persisted Preset Cold start with no device connected applies the base preset estado_ecualizador_test.dart, test D.3 COMPLIANT
Connect-Disconnect-Reconnect Cycle Lands on the Correct Preset at Each Step Full cycle honors station/matrix overrides at each step estado_ecualizador_test.dart, test D.4 COMPLIANT
Toggle-Off Behavior Is Byte-for-Byte Unchanged Toggle off ignores device events entirely estado_ecualizador_test.dart, tests 5.3c and 5.7a (pre-existing, unmodified) COMPLIANT
(bonus, design Testing Strategy, not a distinct spec scenario) Station override still wins over global on disconnect-to-base estado_ecualizador_test.dart, test D.5 COMPLIANT (supplementary coverage)

Compliance summary: 6/6 formal spec scenarios compliant (5 fully compliant, 1 PARTIAL on literal scenario wording only; functional intent met and exceeded) plus 1 bonus scenario compliant.

Note: the verification task brief referenced "6 requirements / 7 scenarios". The actual persisted spec artifact (engram sdd/eq-device-disconnect-revert/spec #2306, byte-identical to openspec/changes/eq-device-disconnect-revert/specs/multi-device-eq/spec.md) contains 5 "Requirement:" headings and 6 "Scenario:" headings (Requirement 1 has 2 scenarios; Requirements 2-5 have 1 each). Counting design-driven bonus test D.5 as a 7th test case reconciles the "7" figure; there is no 6th formal requirement in the artifact.

Correctness (Static Evidence)

Requirement Status Notes
Device-Event Resolve-and-Apply Implemented Single handler _onDispositivoCambiado, unconditional resolve (_resolverPresetActivo) plus apply (audio.aplicarPreset) at L228-231, no event-type branch
First-Seen Bootstrap Must Not Hijack Base-Device Resolution Implemented, stronger than literal scenario text esBase guard at L220-221 fully excludes altavozInterno from bootstrap; no entry is ever persisted for builtin_speaker, not even a correct-value one
Cold Start Applies Base Resolution Implemented cargarPersistido calls _sembrarDispositivoActual which calls _onDispositivoCambiado with the builtin_speaker default; confirmed via source read L141-196
Connect-Disconnect-Reconnect Cycle Implemented No state mutation specific to disconnect beyond the guarded bootstrap; hierarchy is a pure function of station and device
Toggle-Off Behavior Byte-for-Byte Unchanged Implemented L211 early return untouched; guard changes are unreachable when eqMultiDeviceEnabled is false

Coherence (Design)

Decision Followed Notes
1. Single handler, unconditional resolve+apply, no connect/disconnect branch on event type Yes Confirmed via git diff and full source read; L213 assignment and L228-231 resolve+apply untouched
2. Bootstrap guard scoped to altavozInterno only, not all BT/USB/wired Yes esBase = dispositivo.tipo == TipoDispositivo.altavozInterno; wired/BT/USB still bootstrap on first-seen (confirmed via passing 5.5a, 2.2, and D.4 BT first-connect assertion)
3. Deterministic re-resolution, not snapshot/restore Yes No snapshot storage added; _resolverPresetActivo remains the sole resolver
4. Reuse aplicarPreset choke point Yes L229-230 unchanged
5. Cold start trusts existing seed path, no new code Yes cargarPersistido to _sembrarDispositivoActual to _onDispositivoCambiado, verified unchanged
6. Toggle-off invariance, L211 early return untouched Yes Line unchanged; 5.3c/5.7a/5.8b still pass

TDD Compliance

Check Result Details
TDD Evidence reported Yes apply-progress #2323 includes a TDD Cycle Evidence table (3-column RED/GREEN/REFACTOR plus prose, not the full 5-column template, see SUGGESTION-2)
All tasks have tests Yes 5/5 Phase-D tasks (D.1-D.5) have one dedicated test case each
RED confirmed, tests exist Yes All 5 test cases verified present via direct file read of test/estado/estado_ecualizador_test.dart
GREEN confirmed, tests pass Yes 5/5 pass on independent re-execution this session (43/43 in file; 68/68 in fallback regression run)
Triangulation adequate Yes Each Phase-D scenario has exactly one purpose-built, non-overlapping test; no scenario needed multiple variants
Safety Net for modified files Yes lib/estado/estado_ecualizador.dart (modified, not new); 43/43 full-file regression green, independently re-verified this session

TDD Compliance: 6/6 checks passed


Test Layer Distribution

Layer Tests Files Tools
Unit 5 (new, Phase D) 1 (estado_ecualizador_test.dart) package:test
Integration/Widget 0 new pre-existing pantalla_ajustes_test.dart used only as unrelated regression check flutter_test widget testing
E2E 0 0 not installed
Total (new to this change) 5 1

Changed File Coverage

File Line pct whole file Diff-region coverage Uncovered lines in diff region Rating
lib/estado/estado_ecualizador.dart 59.3 pct (118/199, baseline; file has many unrelated pre-existing methods not exercised by this single test file) 100 pct (L220 esBase hit 2x; L221 guarded if hit 3x; L222-224 bootstrap body hit 3x via 5.5a/2.2/D.4; L228-231 resolve+apply hit continuously) none Excellent (diff region)
test/estado/estado_ecualizador_test.dart N/A, test file, not instrumented - - -

Source: coverage/lcov.info generated this session via flutter test --coverage test/estado/estado_ecualizador_test.dart (gitignored, not committed, not left in working tree changes).


Assertion Quality

All assertions verify real behavior. No tautologies, ghost loops, assertion-free tests, or CSS/implementation-detail-only checks found across D.1-D.5. Every test drives real production code (_onDispositivoCambiado, _resolverPresetActivo, cargarPersistido) and asserts on production-derived values (presetActual, presetsDispositivo, FakeServicioAudio.presetsAplicados), consistent with the spec instruction to assert presetsAplicados (proves push, not state-only mutation) rather than state alone.

Manually traced D.2 pre-fix versus post-fix behavior against the actual presetEfectivo hierarchy to independently confirm it is a genuine, non-tautological regression test (would have resolved presetActual == rock, stale, pre-fix versus pop, fresh, post-fix on the second builtin_speaker emit). This matches the claim already documented in apply-progress that D.2 is the only test among D.1-D.5 that targets the actual bootstrap-collision bug.

Assertion quality: All assertions verify real behavior.


Quality Metrics

Linter/Analyzer: No errors. flutter analyze reported zero issues (ran this session, full project, 63.6s) Type Checker: N/A separate tool; covered by flutter analyze (Dart static analysis)

Issues Found

CRITICAL: None

WARNING:

  1. Spec scenario text versus implementation divergence, Requirement 2 "First-Seen Bootstrap Must Not Hijack Base-Device Resolution". The spec scenario literally states that the presetsDispositivo entry for builtin_speaker is bootstrapped to rock (global fallback), implying the key gets created with the correct value. The actual implementation, matching Design Decision 2 and its explicit reasoning ("Rejected: keep and tolerate, leaves persisted mask bug"), NEVER creates that entry at all. The esBase guard unconditionally skips the whole bootstrap block, so the presetsDispositivo containsKey check for builtin_speaker stays false forever, proven by test D.2. This is objectively safer than the literal scenario (D.2 proves it prevents the L3-masks-L4 bug that a bootstrap-with-correct-value approach would not prevent) and is fully justified by the design document rationale; it is not a functional defect. But it means a reader following the spec literally would expect that containsKey check to be true, which is false: a real drift between spec-as-written and spec-as-implemented. Recommendation: before archive, update the spec scenario THEN clause to state that the entry is never bootstrapped and always resolves live through the hierarchy, so the spec matches the safer, already-implemented, already-tested behavior.

SUGGESTION:

  1. Reconcile spec text per WARNING-1 recommendation before archive (listed there to avoid double-counting).
  2. The apply-progress TDD Cycle Evidence table uses a 3-column format (RED/GREEN/REFACTOR plus prose) instead of the full 5-column strict-TDD template (RED/GREEN/TRIANGULATE/SAFETY NET/REFACTOR). Substance is all present and was cross-verified independently in this session, but a future sdd-apply run should use the standard table shape for easier automated auditing.
  3. The apply-progress "Issues Found" section already flags, correctly, as out-of-scope for this change, that cambiarPresetPrincipal immediate re-apply bypasses _resolverPresetActivo and applies the raw global value directly rather than through the hierarchy. Carrying this forward as a tracked tech-debt item; no action needed in this change, but worth a follow-up ticket since it could theoretically let a station/matrix override be transiently bypassed for one apply call when the global preset changes.

Verdict

PASS WITH WARNINGS

0 CRITICAL, 1 WARNING (spec-text/implementation wording drift on Requirement 2, functionally sound and safer than written; recommend a spec-text correction before archive), 3 SUGGESTION. All 43 tests in the target file pass (including all 5 new Phase-D tests and every named consistency lock: 5.5b, 5.7a, 5.8b, 2.2), flutter analyze is clean, the code change matches the design Bootstrap Guard block exactly (verified via git diff and direct source read), and 15/16 tasks are complete with task 5.1 intentionally left as the non-blocking manual on-device gate.