# Tasks: Android Auto Favorite Groups Strict TDD active for Dart layers. Behavioral task = RED (failing test) -> GREEN (minimal impl) -> REFACTOR. `flutter build`/`flutter run`/`flutter analyze`/ `flutter gen-l10n` MUST NOT be executed in this environment (hang) — marked **[DEVIATION]**, same precedent as `openspec/changes/archive/2026-07-19-auto-media-art-quality/tasks.md`. ## Review Workload Forecast | Field | Value | |-------|-------| | Estimated changed lines | 280-370 | | 400-line budget risk | Medium | | Chained PRs recommended | No | | Suggested split | Single PR; fallback 2-way split if apply exceeds budget: PR 1 = Phase 1-2 (builders+tests), PR 2 = Phase 3-5 (wiring+cleanup) | | Delivery strategy | ask-on-risk (default, not overridden by caller) | | Chain strategy | pending | Decision needed before apply: No Chained PRs recommended: No Chain strategy: pending 400-line budget risk: Medium ### Suggested Work Units | Unit | Goal | Likely PR | Notes | |------|------|-----------|-------| | 1 | Snapshot seam + pure builders (Phase 1-2) + tests | PR 1 | Independent of dispatch wiring; fully unit-testable | | 2 | Handler/state wiring (Phase 3-5) | PR 2 (fallback only) | Depends on Unit 1; only split out if diff runs over 400 | ## Baseline (verified against live code, not spec/design prose) - `lib/servicios/navegacion_auto.dart:86-107` `FuenteEmisorasAuto` — no `grupos()`; `actualizarSnapshot` lacks a `grupos` param. - `lib/servicios/navegacion_auto.dart:112-185` `ConstructorArbolAuto` — no group consts/builders; no `GrupoFavoritos` import. - `lib/servicios/navegacion_auto.dart:229-323` `FuenteEmisorasAutoLocal` — no grupos snapshot/read; `favoritos()` try/catch pattern at 257-268 is the template to mirror. - `lib/servicios/servicio_audio.dart:730-796` `getChildren`/`_listaParaCarpeta` — `favoritos` id routes through flat `hijos()`; no `grupo:` branch. - `lib/estado/estado_radio.dart:346-349` `cargarGruposFavoritos()` — no `_fuenteAuto?.actualizarSnapshot(grupos: ...)` push (sibling `cargarFavoritos()` at line 342 does push). - `lib/servicios/servicio_favoritos.dart:166-173` `obtenerGrupos()` confirmed `orden ASC, nombre ASC`; `sin_asignar` auto-inserted + protected (lines 83, 142-153). - `lib/pantallas/pantalla_favoritos.dart:58-99` confirms the UI iterates `grupos` in query order with no re-sort — design's "phone order" claim holds. - **Contract gap found in design.md**: `carpetasFavoritos` MUST route `sin_asignar` leaves through the existing `hijos(idFavoritos, emisoras: ...)` path (identical sort+cap+map) to guarantee byte-identical output when no custom groups exist. design.md's Interfaces section doesn't spell this out explicitly, but it's required by the regression scenario and Testing Strategy row 1 — tasks below make it explicit. - No existing unit tests exercise `FuenteEmisorasAutoLocal` directly (only the hand-written `_FakeFuenteEmisorasAuto`); `grupos()` follows the same untested-Local-impl precedent as sibling `favoritos()`/`misEmisoras()` — no new SQLite-mocking test added for it. ## Phase 1: Foundation — snapshot seam + media-id scheme - [x] 1.1 [RED] `navegacion_auto_test.dart`: `esCarpetaGrupo` test table — `'grupo:g1'`→true, `'favoritos'`/`'emisora:x'`/`''`→false. *(Design "media-id scheme")* - [x] 1.2 [GREEN] `navegacion_auto.dart`: add `_prefijoGrupo='grupo:'`, `_maxGruposPorFavoritos=50` consts; `esCarpetaGrupo(id) => id.startsWith(_prefijoGrupo)`. - [x] 1.3 `navegacion_auto.dart`: add `import '../modelos/grupo_favoritos.dart'`; extend `FuenteEmisorasAuto.grupos()` (abstract) + `actualizarSnapshot({..., List? grupos})`. - [x] 1.4 `navegacion_auto.dart`: `FuenteEmisorasAutoLocal` — add `_snapshotGrupos`, `grupos()` override (snapshot-or-`_favoritosServicio.obtenerGrupos()`, try/catch→`[]`, mirrors lines 257-268), wire `grupos` param in `actualizarSnapshot`. - [x] 1.5 `navegacion_auto_test.dart`: update `_FakeFuenteEmisorasAuto` — implement `grupos()` (default `const []`) + `actualizarSnapshot` grupos param, so existing tests keep compiling. ## Phase 2: Core Implementation — pure tree builders - [x] 2.1 [RED] `itemGrupo` test — id `'grupo:'`, `playable:false`, `title == g.nombre`. - [x] 2.2 [GREEN] `navegacion_auto.dart`: `itemGrupo(g) => _carpeta('$_prefijoGrupo${g.id}', g.nombre)` (reuses existing `_carpeta` helper, line 141). - [x] 2.3 [RED] `carpetasFavoritos` regression: `grupos=[sin_asignar]`, no custom groups → output equals current `hijos(idFavoritos, emisoras: favoritos)` (same items, same order). *Most important test in this change — Spec "Ungrouped station appears exactly as before".* - [x] 2.4 [RED] `carpetasFavoritos`: 1+ non-empty custom group → `grupo:` folders (playable:false, source order) + `sin_asignar` leaves, folders first. - [x] 2.5 [RED] `carpetasFavoritos`: empty custom group omitted from output; `sin_asignar` never becomes its own folder even when present in `grupos`. - [x] 2.6 [RED] `carpetasFavoritos`: >50 eligible non-empty groups truncated to 50. - [x] 2.7 [GREEN] implement `carpetasFavoritos({grupos, favoritos})`: filter `grupos` to non-`esSinAsignar` with ≥1 member (via `favoritos.any`), `.take(_maxGruposPorFavoritos).map(itemGrupo)`; append `hijos(idFavoritos, emisoras: favoritos.where(sin_asignar))`. Satisfies 2.3-2.6. - [x] 2.8 [RED] `hijosGrupo`: filters by `grupoFavoritosId`, reuses `ordenarEmisoras`+50 cap (mirror existing cap test ~line 334); unknown/stale/malformed id → `[]`. - [x] 2.9 [GREEN] implement `hijosGrupo(grupoMediaId, {favoritos})`: strip `_prefijoGrupo` defensively (mirror `resolver`), filter, sort via `ordenarEmisoras`, `.take(_maxItemsPorCarpeta)`, map `itemEmisora`. - [x] 2.10 [RED] assert `resolver('grupo:g1', universo) == null` — collision-free vs `emisora:` scheme. *(Spec "Media Item Resolution")* ## Phase 3: Integration / Wiring - [x] 3.1 `servicio_audio.dart:730-748` `getChildren`: branch `parentMediaId == idFavoritos` → `carpetasFavoritos(grupos: await fuente.grupos(), favoritos: await fuente.favoritos())`; branch `constructor.esCarpetaGrupo(parentMediaId)` → `hijosGrupo(parentMediaId, favoritos: await fuente.favoritos())`; keep existing `todas`/`mis_emisoras` path and outer try/catch unchanged. - [x] 3.2 `servicio_audio.dart:791-795` `_listaParaCarpeta`: remove the now-unreachable `idFavoritos` switch case (handled by 3.1). - [x] 3.3 `estado_radio.dart:346-349` `cargarGruposFavoritos()`: add `_fuenteAuto?.actualizarSnapshot(grupos: _gruposFavoritos);` (mirrors line 342's `favoritos:` push). ## Phase 4: Testing / Verification - [x] 4.1 Run `flutter test test/servicios/navegacion_auto_test.dart` — full green. - [x] 4.2 Run any `estado_radio`/`servicio_audio` test files if they exist; else note as manual follow-up (no existing suite found for these two files as of this writing). *(Updated: `test/estado/estado_radio_test.dart`'s `_FuenteEmisorasAutoEspia` needed `grupos()` wiring to keep compiling; added an assertion proving the new `actualizarSnapshot(grupos:)` push — full file green, 21/21.)* - [x] 4.3 [DEVIATION] `flutter build`/`flutter run`/`flutter analyze`/`flutter gen-l10n` — DO NOT RUN in this environment (hangs). Manual static review instead: no unused symbols, `grupo:`/`emisora:` prefixes non-colliding by inspection, no leftover references to the removed `_listaParaCarpeta` case. Real run recommended before merge/CI. ## Phase 5: Cleanup - [x] 5.1 [REFACTOR] `navegacion_auto.dart`: doc-comment new public members per the file's existing Design-decision-linking style; confirm full test suite still green.