Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-favorite-groups/tasks.md
T
FreeTLab 066fedb7bc docs(openspec): archive android-auto-favorite-groups
Merges its delta requirements into the android-auto-media base spec.
2026-07-19 13:47:39 +02:00

79 lines
7.5 KiB
Markdown

# 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<GrupoFavoritos>? 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:<id>'`, `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:<id>` 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.