Files
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

13 KiB

Apply Progress: Android Auto Local Music — Phase 3 (Folder Queue + Shuffle)

Status: DONE — all executable tasks complete in a single pass (single PR, size:exception per user's explicit choice)

Batch: FIRST and ONLY batch. All phases (0-5) implemented in one pass per the user's explicit delivery-strategy choice (single PR with size:exception, acknowledged as the highest-risk change of the session). Phase 6 items are manual follow-up (not executable by this agent) or confirmed not applicable.

Tasks completed: 24 / 26 executable checklist items (Phase 6.1 is

explicitly manual follow-up per tasks.md; Phase 6.2 confirmed not applicable, no code change needed)

  • Phase 0 (Regression Baseline): 0.1, 0.2 — DONE
  • Phase 1 (Pure Foundations — ColaLocal/decidirAvanceCola/avanceEsValido): 1.1, 1.2, 1.3 — DONE
  • Phase 2 (Shuffle & Collision): 2.1, 2.2, 2.3 — DONE
  • Phase 3 (Handler Isolation Wiring, static-review-only): 3.1-3.8 — DONE
  • Phase 4 (Browsable/Playable Actions): 4.1, 4.2, 4.3 — DONE
  • Phase 5 (Open Question + Final Regression): 5.1-5.5 — DONE
  • Phase 6 (Deviations/Manual Follow-up): 6.1 deferred (manual, flutter analyze/--coverage/gen-l10n not run per strict-TDD scope); 6.2 confirmed N/A (no native/manifest change)

Files changed

New

  • lib/servicios/cola_local.dartColaLocal (immutable queue holder), DecisionAvanceCola enum, decidirAvanceCola, avanceEsValido. Pure Dart, no just_audio import, mirrors the ControladorReconexion extraction pattern so it is fully unit-testable without instantiating PluriWaveAudioHandler.
  • test/servicios/cola_local_test.dart — 18 tests covering ColaLocal nav/boundaries, decidirAvanceCola's 3-way gate, avanceEsValido's identical() guard.
  • test/servicios/controlador_reconexion_local_test.dart — 3 tests proving the design's open-question resolution: a dead local track retries up to the DEFAULT maxReintentos: 5 via the existing ControladorReconexion (source-agnostic, no special-casing), fails cleanly on the 6th failure (no 6th retry, no hang, no crash). Deliberately kept as a SEPARATE file from servicio_audio_reconnect_test.dart so that protected suite's exact pass count stays byte-identical (see "Regression discipline" below).

Modified

  • lib/servicios/navegacion_auto.dart
    • pistasEnOrdenNombre, mezclarFisherYates, pistasEnOrdenAleatorio (Fisher-Yates over the canonical name-sorted order, injected Random).
    • Two new ConstructorArbolAuto prefixes: carpeta_local_reproducir:/carpeta_local_aleatorio:, both PLAYABLE (unlike every other carpeta_local_* prefix), with predicates + strip-by-length codec, collision-proven at index 14 against the 3 sibling carpeta_local_* prefixes and index 13 against carpeta_local:.
    • itemsLocales' page-0 prepend block extended: "Reproducir carpeta"/"Reproducir aleatorio" prepended BEFORE the sort/bucket nav entries (per Design ADR-5's explicit ordering), guarded by totalPistas > 0 — absent for a folder with only subfolders.
    • reproducirCarpetaLocal (fetches children, filters/orders, calls iniciarCola; no-op on empty/unresolvable folder) and construirMediaItemColaLocal (resolves a queue track's content URI + MediaItem, reusing reproducirPistaLocal's title derivation).
    • Test file test/servicios/navegacion_auto_test.dart: 139 tests total (was ~120 before this change) — added new groups for the above, PLUS updated 4 pre-existing tests whose exact item counts/positions shifted because the new page-0 prepend items are real production behavior changes (not a regression — this file is not one of the 4 protected suites):
      • "exactamente 50 items": hasLength(51)hasLength(53).
      • "51 a 100 items" pagina0: hasLength(56)hasLength(58).
      • "page-0 mode/bucket prepend" (>150-pistas case): first-5-non-playable loop replaced with explicit checks (index 0/1 = the two new playable action items, index 2-6 = calidad/buckets).
  • lib/servicios/servicio_audio.dart (HIGHEST RISK, static-review-only — no handler instantiation possible in unit tests) —
    • New fields: ColaLocal? _colaLocal, bool _avanzandoCola.
    • playMediaItem split: public method now ALWAYS clears _colaLocal/_avanzandoCola FIRST, then delegates to new private _encolarCambioFuente (body is the previous playMediaItem implementation, byte-identical).
    • New _reproducirEntradaCola(NodoLocal, {ColaLocal? colaEsperada}) — the ONLY other caller of _encolarCambioFuente besides public playMediaItem. colaEsperada, when provided, applies the avanceEsValido mid-await race guard AFTER resolving the content URI and BEFORE playing — functionally identical to the design's advance-flow pseudocode, refactored so the guard lives inside the sole resolve+play entry point (avoids a second, redundant URI resolve).
    • _manejarFinPista(ProcessingState) wired as the FIRST line of the playerStateStream listener, calling decidirAvanceCola; sets the _avanzandoCola latch synchronously before any await for the avanzar case; the radio playing && ready reset branch gained one additional line (_avanzandoCola = false) alongside its two existing untouched statements.
    • _desactivarCola(), _iniciarColaLocal(List<NodoLocal>) (the iniciarCola seam for reproducirCarpetaLocal).
    • stop() now also calls _desactivarCola()onTaskRemoved inherits this for free (it calls stop()).
    • skipToNext/skipToPrevious overrides added: no-op when _colaLocal == null; skipToNext past the last track deactivates + stops (Design ADR-4, no wraparound); skipToPrevious at index 0 clamps to 0 (restarts the track).
    • playerStateStream's playbackState.add(...) gated: controls/ systemActions/androidCompactActionIndices only add skip-previous/skip-next when _colaLocal != null; the null branch is byte-identical to the pre-change list (verified: same elements, same order, same compact index).
    • playFromMediaId gained a THIRD branch (after eq_preset:/pista:, before the station fallthrough) for the two new action prefixes, calling reproducirCarpetaLocal with iniciarCola: _iniciarColaLocal.
    • Confirmed UNTOUCHED by this change (verified via diff read): _cambiarFuente, ControladorReconexion usage/_reconexion field, _esErrorDeRed, _gestionarErrorReproduccion, _intentarReconexion, _reintentarFuente.
  • openspec/changes/android-auto-local-music-phase3/tasks.md — all 24 executable checklist items marked [x] with brief inline notes; 6.1 left unchecked (manual follow-up, explicitly out of this agent's scope).
  • openspec/changes/android-auto-local-music-phase3/design.md — both Open Questions marked [x] confirmed, with the apply-time resolution recorded inline.

Test results (independently re-confirmed via a single combined run)

Combined run of every touched/new test file (cola_local_test.dart + navegacion_auto_test.dart + controlador_reconexion_local_test.dart + the 4 protected regression suites), flutter test ... --concurrency=1 --timeout=60s:

+181: All tests passed!

Per-file breakdown (from the same run, cumulative counters):

File Tests Pass Fail
cola_local_test.dart (new) 18 18 0
navegacion_auto_test.dart (modified) 139 139 0
controlador_reconexion_local_test.dart (new) 3 3 0
servicio_audio_reconnect_test.dart (protected) 8 8 0
servicio_audio_session_test.dart (protected) 5 5 0
servicio_audio_source_switch_test.dart (protected) 3 3 0
servicio_audio_eq_reapply_test.dart (protected) 5 5 0
Total 181 181 0

Regression discipline (Phase 0 baseline vs Phase 5.4 final — the

non-negotiable gate)

Baseline (Phase 0.1, captured BEFORE any code change): servicio_audio_reconnect_test.dart = 8, servicio_audio_session_test.dart = 5, servicio_audio_source_switch_test.dart = 3, servicio_audio_eq_reapply_test.dart = 5. Total 21, 0 failures.

Final (Phase 5.4, re-run after ALL code changes, same command, same order): 8 / 5 / 3 / 5 = 21, 0 failures. EXACT MATCH — same test names, same order, same counts, byte-identical. No regression in any of the 4 protected suites. Verified twice independently (once right after the servicio_audio.dart wiring, once as the final Phase 5.4 gate) with identical results both times.

One deliberate, disclosed exception to "nothing about the 4 protected files changes": Phase 5.2 required a "5 failures → agotado, no 6th retry, no hang/crash" test using the DEFAULT maxReintentos: 5 (the existing test at servicio_audio_reconnect_test.dart:115-137 uses an abbreviated maxReintentos: 2). Rather than add a 9th test into the protected file (which would have changed its pass count from 8 to 9 and technically violated the "exact pass count" gate), this was added to a NEW, purpose-built file (controlador_reconexion_local_test.dart) instead, testing the same ControladorReconexion class (already fully unit-testable, source-agnostic by construction). This satisfies task 5.2's substance without touching the protected file's contents or count at all.

Diff size

Estimated: prod ~470 lines (cola_local.dart ~100 new, navegacion_auto.dart ~180 added, servicio_audio.dart ~190 added/changed net of the split), tests ~950 lines across 3 new/modified test files. Total ~1400+ lines — over the original ~850-950 estimate in tasks.md's Review Workload Forecast, primarily because of the number of existing navegacion_auto_test.dart assertions that needed explicit updates once the page-0 prepend behavior changed, plus the dedicated open-question regression file. Delivered as a single PR with size:exception per the user's explicit, informed choice this session (told this was the highest-risk change of the day, chose to proceed as one PR anyway).

Deviations from design (disclosed)

  1. Mid-await race guard placement: design's ADR-3 pseudocode describes the avanceEsValido guard as gating whether _reproducirEntradaCola gets called at all (guard, THEN call). Implemented instead as an optional colaEsperada parameter checked INSIDE _reproducirEntradaCola, immediately after its own URI resolve and before calling _encolarCambioFuente. This is functionally identical (same causal order: capture → await → guard → play) and avoids a redundant second uriContenidoDePista channel call, while preserving task 3.2's stricter acceptance criterion ("_reproducirEntradaCola is the ONLY other caller of _encolarCambioFuente").
  2. Resolve-failure handling not explicit in design's happy-path pseudocode: when an auto-advance's construirMediaItemColaLocal resolves to null (dead/moved/permission-revoked local file, distinct from a PlayerException during actual playback), the queue is deactivated and stopped rather than left with a stuck _avanzandoCola latch. This is a defensive addition consistent with ADR-4's "no inconsistent state" principle, not explicitly scripted in the design's pseudocode.
  3. androidCompactActionIndices dynamic index: not explicitly called out in tasks.md's acceptance criteria for 3.7, but necessary for correctness — when _colaLocal != null, skipToPrevious is prepended at index 0, so the compact-view highlighted action (play/pause) index shifts to 1 (colaActiva ? 1 : 0) to keep pointing at play/pause instead of skip-previous. The _colaLocal == null branch remains [0], unchanged.
  4. Phase 5.2 test placement — see "Regression discipline" above: added to a new file, not the protected servicio_audio_reconnect_test.dart, to preserve that suite's exact pass count.

Risks / follow-ups for verify phase

  • Phase 3's handler wiring (the highest-risk part of this change) is static-review-only by design — PluriWaveAudioHandler cannot be instantiated in unit tests. Correctness there rests on (a) the pure cola_local.dart functions being fully unit-tested and (b) careful manual diff review of the wiring, both completed in this pass, but a human/verify-phase re-read of servicio_audio.dart's diff is still warranted given the stakes.
  • Phase 6.1 (flutter analyze, flutter test --coverage, flutter gen-l10n) was explicitly out of scope for this apply pass (strict-TDD mode: only target test files were run) and remains a manual follow-up before merge.
  • On-device / DHU manual QA for real car transport buttons and a genuine ProcessingState.completed emission is out of scope here (session precedent, per design's Testing Strategy table) and should be scheduled separately.

Skill compliance

  • literal-encoding: scan 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) for Ã.|â€| — 0 matches, passed.
  • work-unit-commits: working tree left UNSTAGED per instruction — the orchestrator handles commit/push with a [size:exception] tag. No git commit was run by this agent.