Files
pluriwave/openspec/changes/archive/2026-07-20-android-auto-local-music-phase3/verify-report.md
T
FreeTLab 49def4b276
Build & Deploy PluriWave / Análisis de código (push) Successful in 28s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 1m18s
docs(openspec): archive android-auto-local-music-phase3
Merges its delta requirements into the android-auto-media base spec.
Completes the 3-phase local-music-in-Android-Auto feature. Standing
pre-release gate: on-device/DHU validation of every native surface
built across all 4 phases (SAF picking, metadata extraction, art
cache, queue/shuffle handler wiring) is still outstanding.
2026-07-20 01:15:46 +02:00

12 KiB

Verification Report: android-auto-local-music-phase3

Verdict: PASS WITH WARNINGS

Priority Investigation Verdict

1. Regression-suite byte-identical claim - VERIFIED TRUE

Independently re-ran the combined suite: flutter test test/servicios/cola_local_test.dart test/servicios/navegacion_auto_test.dart test/servicios/controlador_reconexion_local_test.dart test/servicios/servicio_audio_reconnect_test.dart test/servicios/servicio_audio_session_test.dart test/servicios/servicio_audio_source_switch_test.dart test/servicios/servicio_audio_eq_reapply_test.dart --concurrency=1 --timeout=60s

Result: +181: All tests passed!

Per-file breakdown counted directly from the run output: cola_local=18, navegacion_auto=139, controlador_reconexion_local=3, reconnect=8, session=5, source_switch=3, eq_reapply=5, sum 181, matching apply-progress exactly, including the 4 protected suites at 8/5/3/5=21.

Stronger confirmation than requested: git status --porcelain and git diff --stat on the 4 protected test files (servicio_audio_reconnect_test.dart, servicio_audio_session_test.dart, servicio_audio_source_switch_test.dart, servicio_audio_eq_reapply_test.dart) returned completely empty - these files are byte-for-byte untouched by this change, not merely "same pass count." This is the strongest possible proof available.

2. Isolation mechanism - traced from actual code, not the design doc's description

  • (a) playMediaItem (lib/servicios/servicio_audio.dart:499-504) unconditionally sets _colaLocal = null; _avanzandoCola = false; on every call before delegating to _encolarCambioFuente. No branch, no early return, no code path skips this.
  • (b) git diff on servicio_audio.dart shows zero lines changed inside _cambiarFuente, _esErrorDeRed, _gestionarErrorReproduccion, _intentarReconexion, _reintentarFuente, _detenerReconexion, or the _reconexion (ControladorReconexion) field usage. Confirmed genuinely untouched by direct diff inspection, not by trusting the design/apply-progress claim.
  • (c) Concrete scenario traced by reading the code: local queue is active (_colaLocal != null), user/car taps emisora:<uuid> leads to playFromMediaId, which is not eq_preset:/pista:/folder-action, falls to reproducirPorMediaId(mediaId, fuente, reproducir: playMediaItem) (navegacion_auto.dart:794-818), which does await reproducir(item) = playMediaItem(item), which clears _colaLocal/_avanzandoCola then delegates to _encolarCambioFuente. After this, any later _manejarFinPista call computes decidirAvanceCola(colaLocal: null, ...), which returns ninguna unconditionally (first two guard checks in decidirAvanceCola). No auto-advance can fire. Confirmed by code trace, not by assumption.

3. Auto-advance correctness

  • (a) ProcessingState.completed handling gated by decidirAvanceCola (cola_local.dart:80-91): !trackCompletado returns ninguna; colaLocal == null returns ninguna; avanzandoCola returns ninguna; else avanzar/desactivar. No path fires when the queue is inactive.
  • (b) The re-entry latch has a real test (cola_local_test.dart:105-124): it calls decidirAvanceCola twice, simulating two rapid completion events, first with avanzandoCola: false (expects avanzar), second with avanzandoCola: true (expects ninguna). This is a genuine state-transition test exercising the actual pure function, not a mock that always behaves nicely.
  • (c) playerStateStream (Stream, carries processingState) and playbackEventStream's onError callback (fires only with Object error, StackTrace) are structurally disjoint Dart Streams from just_audio's AudioPlayer API (_conectarStreamsPlayer, servicio_audio.dart:266-341). ProcessingState.completed is a value inside PlayerState, never an error object, so it structurally cannot flow through the onError channel. Verified via code read, not via a comment's assertion.

4. Self-reported deviations - all verified genuine, none weaken the design's safety intent

  • Deviation 1 (avanceEsValido folded into _reproducirEntradaCola via optional colaEsperada param instead of an external gate): code preserves the identical causal order the design intended, capture candidate, await URI resolve, avanceEsValido identity guard, then play. Functionally equivalent to the design's guard-then-call pseudocode, not a weaker approximation.
  • Deviation 2 (defensive deactivate+stop on unresolvable local-track URI during auto-advance): _desactivarCola() is idempotent (always sets both _colaLocal/_avanzandoCola), and stop() is safe to call redundantly. No inconsistent state is reachable.
  • Deviation 3 (androidCompactActionIndices made dynamic): verified byte-identical for radio, colaActiva == false gives [0], matching the pre-change const [0]; colaActiva == true gives [1], correctly re-pointing at play/pause since skip-previous is prepended at index 0 when the queue is active. Minor SUGGESTION: the list is no longer a compile-time const (a new 1-element list is allocated on every playbackState.add call), negligible perf nitpick, zero functional impact.
  • Deviation 4 (5-failures-default-maxReintentos test placed in new file controlador_reconexion_local_test.dart instead of the protected servicio_audio_reconnect_test.dart): file exists, contains 3 real tests using a fake Timer injection, genuinely asserting bounded-retry behavior (5 retries scheduled, 6th returns agotado with no new timer, all timers end cancelled, no hang). Not a stub; independently re-run and passed. Placement choice is reasonable and does not hide a regression, the protected file has literally zero diff.

5. Open-question regression test

controlador_reconexion_local_test.dart genuinely exercises ControladorReconexion.registrarFallo with the default maxReintentos: 5 (source-agnostic, no local-track special-casing possible since the method takes no source-type parameter), asserts agotado on the 6th failure, no new timer created, and all timers end cancelled. Real, not trivial, independently re-run and passing.

Normal Verification Checklist

  1. Media-id collision-freedom: both new prefixes (carpeta_local_reproducir:, carpeta_local_aleatorio:) confirmed playable: true in _itemReproducirCarpeta/_itemReproducirAleatorio (navegacion_auto.dart:517-535). An exhaustive collision test (navegacion_auto_test.dart:358-430) checks both directions against all 8 existing prefixes/ids (emisora:, grupo:, eq_preset:, carpeta_local:, carpeta_local_pag:, carpeta_local_ord:, carpeta_local_bucket:, pista:).
  2. Fisher-Yates shuffle: mezclarFisherYates confirmed to run over pistasEnOrdenNombre's canonical name-sorted order (not native enumeration order, which is not guaranteed stable), with an injectable Random. Determinism test (Random(42) twice yields identical order) and distribution sanity test (1000 runs, every position occupied by every item at least once) both present, real, and passing.
  3. End-of-queue: STOP + deactivate (no loop) implemented in _manejarFinPista's desactivar case and skipToNext's end-of-queue branch; skipToPrevious clamps at index 0 (no wraparound). This wiring is static-review-only, PluriWaveAudioHandler cannot be instantiated in unit tests (confirmed: no PluriWaveAudioHandler() construction anywhere in the test suite), a pre-existing, disclosed testability gap from tasks.md, not a new finding.
  4. Browsable action items: the empty-folder-shows-no-action-item edge case (folder with only subfolders yields neither playable action prepended) is genuinely tested (navegacion_auto_test.dart:1681-1709).
  5. No AI attribution, no debug prints, no dead code, no real leftover TODOs found in changed files (one grep false-positive matched the Spanish word TODOS inside a test description string).
  6. Working tree state: confirmed nothing committed or staged. git diff --cached --stat is empty; git log -1 shows the pre-existing HEAD (85dd043 docs archive android-auto-local-music-phase2); all changes are unstaged modified/untracked files, matching apply-progress's stated intent to leave the tree unstaged for the orchestrator.
  7. Diff size: git diff --stat = 957 lines across 3 tracked modified files, plus 435 lines across 3 untracked new files (cola_local.dart 103, cola_local_test.dart 181, controlador_reconexion_local_test.dart 151) = approximately 1392 lines total, consistent with apply-progress's claim of roughly 1400+ lines.
  8. Independently re-ran the full claimed 181/181 test count across all touched/new + protected files, confirmed exact match, zero failures.

Issues by Severity

CRITICAL

None found.

WARNING

  • Phase 3 handler wiring (highest-risk part of this change) is static-review-only by unavoidable environment constraint. PluriWaveAudioHandler cannot be instantiated in unit tests because its constructor builds a real just_audio.AudioPlayer requiring platform MethodChannels. This means a genuine ProcessingState.completed firing, real car transport button presses (skipToNext/skipToPrevious), and the actual runtime behavior of the mid-await avanceEsValido race guard have never been exercised by an executable test in this environment, correctness rests entirely on (1) the pure, fully-unit-tested cola_local.dart decision functions and (2) careful manual diff review, both of which this verify pass independently re-confirmed. This is a genuine residual risk requiring human on-device/DHU (Desktop Head Unit) confirmation before shipping, not a formality. The design and apply-progress both already flag this gap; this verify pass confirms it is real and has not been concealed or downplayed.
  • Phase 6.1 deferred: flutter analyze, flutter test --coverage, flutter gen-l10n were explicitly not run per Strict TDD scope (only target test files were run). Must be run manually before merge.
  • Single-PR delivery with size:exception (~1400+ lines, over the original ~850-950 estimate in tasks.md's Review Workload Forecast), an informed, disclosed choice by the user this session (told this was the highest-risk change of the day, chose to proceed as one PR anyway), not a new finding, restated here as a review-workload note for whoever reviews the PR.
  • Pre-release gate for the entire 3-phase local-music feature (CRITICAL STANDING RISK): On-device / DHU (Desktop Head Unit) validation of EVERY native Android Auto surface built this session has never been runtime-verified. This includes: SAF folder picking (Phase 1), native metadata extraction via MediaMetadataRetriever (Phase 2), the FileProvider art cache (Phase 2), and NOW the queue/shuffle handler wiring with real car transport buttons (Phase 3). All of it is static-review-only or unit-tested-via-extraction; none of it has run on a real Android Auto head unit. This is the single most important open risk across the entire local-music feature and applies to ALL FOUR archived changes (Phase 1 2026-07-19, paging 2026-07-19, Phase 2 2026-07-19, Phase 3 2026-07-20). A pre-release sign-off must include a dedicated on-device QA pass on a real head unit or DHU covering all four surfaces before any production release.

SUGGESTION

  • androidCompactActionIndices is no longer a compile-time const list (was const [0], now [colaActiva ? 1 : 0]), allocates a new 1-element list on every playbackState.add call. Zero functional impact, negligible GC pressure; not worth blocking on.

Test Evidence

Combined run (independently executed, not trusted from apply-progress):

flutter test test/servicios/cola_local_test.dart test/servicios/navegacion_auto_test.dart test/servicios/controlador_reconexion_local_test.dart test/servicios/servicio_audio_reconnect_test.dart test/servicios/servicio_audio_session_test.dart test/servicios/servicio_audio_source_switch_test.dart test/servicios/servicio_audio_eq_reapply_test.dart --concurrency=1 --timeout=60s

+181: All tests passed! (0 failures)

Literal-encoding scan (skill literal-encoding) run over all touched/new .dart files (cola_local.dart, navegacion_auto.dart, servicio_audio.dart, cola_local_test.dart, navegacion_auto_test.dart, controlador_reconexion_local_test.dart), zero corruption matches found.

sdd-archive - no CRITICAL findings block archival, but the WARNING about on-device/DHU confirmation for the static-review-only handler wiring should be explicitly carried into the archive record as an accepted, tracked risk, not silently dropped.