Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-eq-presets/verify-report.md
T
FreeTLab 9bfa9ac408 docs(openspec): archive android-auto-eq-presets
Merges its delta requirements into the android-auto-media base spec.
2026-07-19 14:19:11 +02:00

8.3 KiB

Verification Report: android-auto-eq-presets

Mode: Strict TDD | Verdict: PASS

Completeness

All 21 tasks in tasks.md (Phases 1-5) are checked complete and match the live code. No task marked complete lacks a corresponding code/test change.

Test Execution Evidence (independently re-run, not trusted from apply-progress)

Command Result
flutter test test/servicios/navegacion_auto_test.dart --concurrency=1 --timeout=60s (compact reporter) 46/46 passed
Same file, -r json, counted testDone events 46 (independently confirmed via JSON reporter, not just trusting the compact-reporter tally)
flutter test test/estado/estado_radio_test.dart --concurrency=1 --timeout=60s (favorite-groups regression suite, shared playFromMediaId/dispatch surface) 22/22 passed (JSON testDone count also 22)

Apply-progress's claimed 46/46 for navegacion_auto_test.dart is accurate. The extra regression pass on estado_radio_test.dart (not part of apply-progress's own claims) also passes clean - no regression on the favorite-groups feature that shares the touched dispatch surface.

Load-Bearing Invariant: EQ selection never disturbs playback

Traced playFromMediaId (lib/servicios/servicio_audio.dart:781-820) directly in the live file:

  • The esPresetMediaId(mediaId) branch is the first statement inside the (now single) outer try, before _fuenteNavegacionGlobal is even read.
  • It ends with an unconditional return; - genuinely cannot fall through to reproducirPorMediaId/playMediaItem.
  • No mediaItem mutation, no play()/setUrl()/reconnect call anywhere in that branch - only ServicioEcualizador() construction and aplicarPresetPorMediaId(...).

Traced aplicarPresetPorMediaId's actual signature (lib/servicios/navegacion_auto.dart:364-371, read directly, not from docstring):

Future<void> aplicarPresetPorMediaId(
  String id, {
  required List<PresetEcualizador> presets,
  required String? uuidActual,
  required Future<Set<String>> Function() clavesPorEmisora,
  required Future<void> Function(PresetEcualizador) persistirPrincipal,
  required Future<void> Function(PresetEcualizador) aplicar,
})

Confirmed: no playMediaItem, mediaItem, or playbackState parameter exists - structurally impossible to inject a playback seam. This matches the docstring claim and the dedicated structural test (navegacion_auto_test.dart:755-784).

Root Folder Count

  • ConstructorArbolAuto.raiz() (lib/servicios/navegacion_auto.dart:171-176) returns 4 folders, Ecualizador last, playable: false.
  • test/servicios/navegacion_auto_test.dart:226 - expect(raiz, hasLength(4)) - confirmed updated (line number shifted from the apply-progress's original estimate of 225 to 226 due to file growth, as anticipated by the reviewer's warning; content matches).
  • Line 241-242 additionally assert raiz.last.id == idEcualizador and raiz.last.playable == false (ADR-2 last-position + non-playable, beyond the base spec's minimum).
  • Grepped the full test/ tree for other raiz()/root-count assertions: only navegacion_auto_test.dart matches - confirms task 4.2's claim.

Persistence Seam

Grepped both call sites in the diff:

  • servicio_audio.dart:800 - persistirPrincipal: servicio.guardarPrincipal -> ServicioEcualizador.guardarPrincipal (servicio_ecualizador.dart:129-132, writes SharedPreferences key eq_preset_principal_v1).
  • No call to EstadoEcualizador.cambiarPresetPrincipal anywhere in the diff or the new code paths (confirmed via grep across navegacion_auto.dart/servicio_audio.dart).
  • Matches ADR-4 exactly: handler + service seam, not the phone's ChangeNotifier.

debeAplicarPrincipalAhora Gate Parity

Read both gates directly:

  • estado_ecualizador.dart:302-304: uuid == null || !_presetsEmisoraMap.containsKey(uuid)
  • navegacion_auto.dart:348-351: uuidActual == null || !clavesPorEmisora.contains(uuidActual)

Also traced that clavesPorEmisora is populated from servicio.cargar().porEmisora.keys, and servicio_ecualizador.dart's cargar() reads porEmisora from the same persisted per-station map that populates EstadoEcualizador._presetsEmisoraMap (estado_ecualizador.dart:161-163, ..addAll(config.porEmisora)). The two gates are genuinely operating over the same underlying data, not just superficially similar in shape.

Unknown/Stale Preset ID

resolverPresetEq returns null for unknown name / empty-after-prefix / non-eq_preset: id (navegacion_auto.dart:332-340). aplicarPresetPorMediaId returns immediately on null without invoking either seam. Test (navegacion_auto_test.dart:731-753) asserts persistirLlamadas == 0 and aplicarLlamadas == 0 for 'eq_preset:Inexistente' - the assertion is on call counts, not just a test name; genuinely proves the no-op.

Active Preset Indication (Optional requirement)

Confirmed OUT OF SCOPE as designed (ADR-6). Grepped navegacion_auto.dart for marker/selection-related terms - no half-built UI, no dead marker code, no leftover scaffolding. itemPresetEq emits only the plain preset name as title.

Deviations Self-Reported by apply-progress.md - Verified

  1. servicio_ecualizador.dart import added to servicio_audio.dart, not listed in task 3.3's enumerated file list. Confirmed real: git diff shows +import 'servicio_ecualizador.dart'; at the top of the file; task 3.3's text only mentions verifying the preset_ecualizador.dart import. This is a genuine (harmless) task-description gap, not a code defect.
  2. playFromMediaId restructured to a single outer try/catch. Confirmed real via git diff: previously final fuente = _fuenteNavegacionGlobal; if (fuente == null) return; sat OUTSIDE the try block; now both lines are inside the try, after the new eq_preset: branch. Behaviorally inert for the pre-existing emisora:/grupo: path - the null-check itself cannot throw, so moving it inside try changes nothing observable. Verified via the full estado_radio_test.dart (22/22) and navegacion_auto_test.dart (46/46, includes reproducirPorMediaId tests) suites, both green.

Diff Size Cross-Check

git diff --stat on the three touched files:

lib/servicios/navegacion_auto.dart       |  94 ++++++++++++-
lib/servicios/servicio_audio.dart        |  26 +++-
test/servicios/navegacion_auto_test.dart | 226 ++++++++++++++++++++++++++++++-
3 files changed, 339 insertions(+), 7 deletions(-)

339 + 7 = 346 changed lines - matches apply-progress's self-reported figure exactly. Within the 300-420 forecast, under the 400-line budget.

Cleanliness Checks

  • No AI attribution strings found in the diff (Co-Authored-By, Generated with, Claude, ChatGPT, etc. - none matched).
  • No stray print/debugPrint calls introduced.
  • No TODO/FIXME markers introduced.
  • No dead code found in the reviewed hunks.
  • Working tree: navegacion_auto.dart, servicio_audio.dart, test/servicios/navegacion_auto_test.dart are modified-but-unstaged; openspec/changes/android-auto-eq-presets/ is untracked. Nothing has been committed - confirmed via git log (HEAD is still 066fedb, predating this change) and git status.

Spec Compliance Matrix

Spec Requirement Status Evidence
Browsable Media Tree (4 folders, Ecualizador included) PASS raiz() + test at line 226
EQ Preset Browsable Folder (6 items, eq_preset:) PASS presetsEq/itemPresetEq + tests
EQ Preset Selection Applies Without Disturbing Playback PASS Structural signature proof + first-branch/unconditional-return trace
EQ Preset Persistence and Phone/Car Parity PASS guardarPrincipal call site confirmed, SP key shared with phone
Active Preset Indication (Optional) PASS (scoped out) ADR-6, no half-built code

Design Coherence

All 6 ADRs (ADR-1 through ADR-6) traced against live code - implementation matches design as documented. No undocumented deviation found beyond the two apply-progress already self-reported (both verified genuine and low-risk).

Issues Found

None CRITICAL. None WARNING. None SUGGESTION beyond a purely cosmetic note: apply-progress's line-number references (e.g. "225" vs actual "226") drift slightly as the file grows - informational only, does not affect correctness or task completeness.

Final Verdict

PASS - 0 CRITICAL, 0 WARNING, 0 SUGGESTION (one informational-only line-number drift note, not a defect).