Files
pluriwave/openspec/changes/android-auto-local-music-phase3/verify-report.md
T
FreeTLab dfd40ca937 feat(auto): queue playback and shuffle for local music folders [size:exception]
Adds "Reproducir carpeta" (sequential) and "Reproducir aleatorio"
(Fisher-Yates over the name-sorted order) as folder-scoped playable
actions, with auto-advance on track completion and skip next/prev.

Isolation from live radio is structural, not disciplinary: the
public playMediaItem always clears the local queue on any call, and
a new private _encolarCambioFuente is the only path that can advance
within it. _cambiarFuente, ControladorReconexion, and the reconnect
error path are untouched -- confirmed by a byte-for-byte empty diff
on all 4 pre-existing radio/reconnect regression suites, independently
re-run before and after (21/21 both times).

Handler wiring itself is static-review-only (PluriWaveAudioHandler
can't be unit-instantiated); the isolation/advance/race-guard
decision logic is extracted into cola_local.dart's pure functions,
which are fully unit-tested.
2026-07-20 01:08:15 +02:00

78 lines
11 KiB
Markdown

# 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<PlayerState>, 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
6. 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:).
7. Fisher-Yates shuffle: `mezclarFisherYates` confirmed to run over `pistasEnOrdenNombre`'s canonical name-sorted order (not native enumeration order), 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.
8. 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.
9. 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).
10. 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).
11. 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.
12. 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.
13. 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.
### 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.
## Next Recommended
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.