test: full test suite (Vitest + pgTAP + Playwright) + TDD plan restructure

Add a 4-layer test stack covering RLS, triggers, and UI flows for Fases 0–2a,
then restructure every plan file so future fases start with tests and end with
a verification gate.

Test suite
- packages/test-utils: Vitest integration tests signing HS256 JWTs via jose so
  each test acts as a specific seed user (createClientAs + createAdminClient)
- supabase/tests: pgTAP for accept_invitation(), item_frequency trigger, and
  promote-on-admin-leave; each file self-installs pgtap extension
- apps/web/tests: Playwright E2E with live Keycloak login per test (storageState
  caching doesn't rehydrate Supabase's session state reliably)
- just test-all chains the three suites; test-db forwards POSTGRES_PASSWORD
  as PGPASSWORD with ON_ERROR_STOP=1 so failures abort the chain

Supabase auth gotcha
- PostgREST queries inside onAuthStateChange deadlock on GoTrue's navigator.locks
  auth lock (getAccessToken → getSession → initializePromise waits on the same
  lock that's held during event dispatch). Fix is two defenses: a pass-through
  lock in $lib/supabase, and a setTimeout(0) defer in root +layout.svelte to
  push loadUserCollectives out of the callback's microtask chain. Either alone
  is insufficient; both together unblock the Playwright suite.

Env key rotation
- apps/web/.env.development had a stale demo anon key signed with a different
  secret than root .env; Vite inlined that into the browser bundle so Kong (which
  uses the root .env value) rejected every request with 401. Aligned the two
  files and added a memory entry to flag this for the next rotation.

Plan restructure (TDD)
- Every fase now opens with X.0 Tests primero and closes with X.Z Verificación
  final. Completed fases (0, 1, 2a) show the pattern retroactively with the
  tests that currently cover them; pending fases (2b, 3, 4) list the tests to
  write before implementation.

Docs
- CLAUDE.md status line reports 54 + 12 + 15 = 81 green tests, adds gotchas
  #11 (auth-lock deadlock) and #12 (no storageState caching)
- README.md adds a TDD methodology section and the test-all command
- .gitignore excludes Playwright's generated reports and auth state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 01:40:16 +02:00
parent 3af1276c15
commit f396897cb5
39 changed files with 2889 additions and 97 deletions

View File

@@ -1,7 +1,10 @@
### Fase 0 — Infraestructura y Entornos
### Fase 0 — Infraestructura y Entornos
**Estado: Completa**
**Duración estimada: 12 semanas**
**Objetivo: tener los dos entornos funcionando antes de escribir producto.**
> **Nota sobre el patrón TDD:** desde la Fase 1 en adelante, cada fase abre con una sección `X.0 Tests primero` y cierra con `X.Z Verificación final`. La Fase 0 no sigue ese patrón porque es puro bootstrapping de infraestructura; su "test" es que `just dev` arranque la stack y `just test-all` (introducido en una fase posterior) pueda ejecutarse sin errores de conexión.
#### 0.1 Variables de entorno
Todas las variables viven en `.env.dev` (dev) y `.env.production` (prod, fuera del repo). El fichero `.env.example` documenta cada variable con su propósito.
@@ -79,7 +82,7 @@ B2_APPLICATION_KEY=...
#### 0.2 Entorno de desarrollo local
- [ ] `docker-compose.dev.yml` con todos los servicios:
- [x] `docker-compose.dev.yml` con todos los servicios:
- **PostgreSQL 15** — puerto `5432`
- **Supabase Auth (GoTrue)** — configurado con Keycloak como OIDC externo
- **Supabase Realtime** — puerto `4000`
@@ -87,14 +90,14 @@ B2_APPLICATION_KEY=...
- **Kong API Gateway** — puerto `54321` (entrada principal de Supabase)
- **Supabase Studio** — puerto `54323`
- **Keycloak 24** — puerto `8080`, arranca con `--import-realm keycloak/realm-export.json`
- [ ] `keycloak/realm-export.json` — realm `colectivo` preconfigurado con:
- [x] `keycloak/realm-export.json` — realm `colectivo` preconfigurado con:
- Client `colectivo-web` (public, PKCE habilitado, redirect URIs para dev)
- **5 usuarios de prueba** creados y listos (ver tabla abajo)
- Roles de realm: `app-user` (asignado a todos por defecto)
- [ ] SvelteKit dev server en puerto `5173`, apuntando a variables de `.env.dev`
- [ ] `supabase/seed.sql` — crea los usuarios de prueba en `auth.users` con los mismos UUIDs que Keycloak, un colectivo de ejemplo, listas e ítems
- [ ] Generación automática de tipos TypeScript: `supabase gen types typescript`
- [ ] `Justfile` con `just dev`, `just db-reset`, `just db-types`, `just kc-export`, `just kc-open`
- [x] SvelteKit dev server en puerto `5173`, apuntando a variables de `.env.dev`
- [x] `supabase/seed.sql` — crea los usuarios de prueba en `auth.users` con los mismos UUIDs que Keycloak, un colectivo de ejemplo, listas e ítems
- [x] Generación automática de tipos TypeScript: `supabase gen types typescript`
- [x] `Justfile` con `just dev`, `just db-reset`, `just db-types`, `just kc-export`, `just kc-open`
**Usuarios de prueba en Keycloak (dev):**
@@ -112,16 +115,16 @@ B2_APPLICATION_KEY=...
> **El entorno de producción ya está montado y dockerizado.** Keycloak, nginx y su PostgreSQL propio ya están operativos. Esta sección cubre únicamente lo que hay que añadir.
- [ ] Incorporar los servicios de Supabase al `docker-compose.prod.yml` existente:
- [x] Incorporar los servicios de Supabase al `docker-compose.prod.yml` existente:
- PostgreSQL dedicado para Supabase — contenedor independiente, volumen propio, **no compartir con Keycloak**
- GoTrue (Auth), Realtime, Storage, Kong en la red interna Docker existente
- Kong expuesto en un puerto interno (ej: `127.0.0.1:54321`) — nginx hace proxy hacia él, no expuesto directamente
- [ ] Añadir el contenedor SvelteKit al compose, escuchando en un puerto interno (ej: `127.0.0.1:3000`) — nginx hace proxy hacia él
- [ ] Configurar nginx externamente (fuera del repo) para los nuevos servicios — ver bloques de ejemplo abajo
- [ ] Exportar el realm `colectivo` desde dev (`just kc-export`) e importarlo en el Keycloak de producción via Admin Console — sin `--import-realm` en prod
- [ ] `.env.production` en el VPS con todas las variables (ver sección 0.1), fuera del repo
- [ ] Scripts de backup — ver sección **0.6 Sistema de Backups** para el detalle completo
- [ ] Script de deploy: `ssh vps "cd /app && git pull && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d --no-deps web"`
- [x] Añadir el contenedor SvelteKit al compose, escuchando en un puerto interno (ej: `127.0.0.1:3000`) — nginx hace proxy hacia él
- [x] Configurar nginx externamente (fuera del repo) para los nuevos servicios — ver bloques de ejemplo abajo
- [x] Exportar el realm `colectivo` desde dev (`just kc-export`) e importarlo en el Keycloak de producción via Admin Console — sin `--import-realm` en prod
- [x] `.env.production` en el VPS con todas las variables (ver sección 0.1), fuera del repo
- [x] Scripts de backup — ver sección **0.6 Sistema de Backups** para el detalle completo
- [x] Script de deploy: `ssh vps "cd /app && git pull && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d --no-deps web"`
- `--no-deps web` para actualizar solo el contenedor SvelteKit sin reiniciar Supabase ni Keycloak
##### Configuración nginx de referencia
@@ -265,15 +268,15 @@ server {
#### 0.4 CI/CD
- [ ] `ci.yml`: lint + typecheck + tests unitarios en cada PR
- [ ] `deploy.yml`: build de imagen SvelteKit → GitHub Container Registry → deploy vía SSH en merge a `main`
- [ ] Secrets de GitHub: `SSH_KEY`, `VPS_HOST`, `GHCR_TOKEN`
- [x] `ci.yml`: lint + typecheck + tests unitarios en cada PR
- [x] `deploy.yml`: build de imagen SvelteKit → GitHub Container Registry → deploy vía SSH en merge a `main`
- [x] Secrets de GitHub: `SSH_KEY`, `VPS_HOST`, `GHCR_TOKEN`
#### 0.5 Monorepo
- [ ] Turborepo configurado con pipeline `build → test → deploy`
- [ ] `packages/types` como paquete interno
- [ ] `apps/web` consumiendo `packages/types`
- [x] Turborepo configurado con pipeline `build → test → deploy`
- [x] `packages/types` como paquete interno
- [x] `apps/web` consumiendo `packages/types`
#### 0.6 Sistema de Backups
@@ -359,13 +362,13 @@ Un solo entry en cron. El script decide internamente si es daily, weekly o month
##### Tareas de la Fase 0
- [ ] Crear volumen Docker (o directorio montado) en `BACKUP_LOCAL_DIR` con permisos correctos
- [ ] Implementar `backup.sh` con soporte para supabase y keycloak, compresión y subida opcional a B2
- [ ] Implementar `backup-cron.sh` con lógica de rotación y purga por tipo
- [ ] Implementar `restore.sh` con confirmación, parada de servicios y resumen post-restauración
- [ ] Configurar crontab en el host del VPS
- [ ] Verificar backup completo: ejecutar `backup.sh supabase`, comprobar fichero generado, ejecutar `restore.sh` en un entorno de prueba
- [ ] Verificar purga: crear backups de prueba con fechas antiguas, ejecutar `backup-cron.sh`, confirmar que se eliminan los que exceden la retención
- [x] Crear volumen Docker (o directorio montado) en `BACKUP_LOCAL_DIR` con permisos correctos
- [x] Implementar `backup.sh` con soporte para supabase y keycloak, compresión y subida opcional a B2
- [x] Implementar `backup-cron.sh` con lógica de rotación y purga por tipo
- [x] Implementar `restore.sh` con confirmación, parada de servicios y resumen post-restauración
- [x] Configurar crontab en el host del VPS
- [x] Verificar backup completo: ejecutar `backup.sh supabase`, comprobar fichero generado, ejecutar `restore.sh` en un entorno de prueba
- [x] Verificar purga: crear backups de prueba con fechas antiguas, ejecutar `backup-cron.sh`, confirmar que se eliminan los que exceden la retención
**Criterio de aceptación:** `just dev` levanta todo el stack en < 3 minutos. Los 5 usuarios de prueba pueden hacer login con Keycloak y llegar a la app. Deploy a producción funciona con un `git push`. Un backup manual de supabase y keycloak genera ficheros válidos que se restauran correctamente.

View File

@@ -1,7 +1,16 @@
### Fase 1 — Autenticación y Colectivo
### Fase 1 — Autenticación y Colectivo
**Estado: Completa** (pendiente: detección automática de idioma desde `Accept-Language`)
**Duración estimada: 23 semanas**
**Objetivo: la base del dominio. Sin esto no existe nada más.**
#### 1.0 Tests — escritos y verdes ✅
A-series (auth), B-series (colectivo), F-series (aislamiento) están cubiertas por:
- [x] Vitest integration (`packages/test-utils/tests/rls-{collective,isolation}.test.ts`) — lectura por rol, invitación, `accept_invitation()`, aislamiento no-miembro
- [x] pgTAP (`supabase/tests/001_accept_invitation.sql`, `003_promote_on_admin_leave.sql`) — happy path y casos de error de `accept_invitation`, promoción automática al último admin
- [x] Playwright E2E (`apps/web/tests/e2e/auth.test.ts`) — redirect a Keycloak, login full OAuth, ver colectivo en sidebar, sign-out, login como guest
#### 1.1 Base de datos
- [x] Migración: tabla `users` — espejo de `auth.users` de Supabase, poblada por trigger al primer login OIDC:
@@ -84,6 +93,10 @@ Librería: **Paraglide JS** (`@inlang/paraglide-sveltekit`). Compile-time, tree-
> **Auto-registro:** Self-registration está habilitado en Keycloak. Un usuario sin cuenta que recibe un link de invitación puede registrarse en la pantalla de Keycloak y volver al callback de invitación automáticamente.
#### 1.Z Verificación final ✅
- [x] `just test-all` — 0 failures (los 5 usuarios hacen login, A/B/F-series verdes)
**Criterio de aceptación:** los 5 usuarios de prueba pueden autenticarse contra Keycloak, llegar a la app con sesión activa, y `auth.uid()` en Supabase resuelve correctamente para las políticas RLS.
---

View File

@@ -1,26 +1,35 @@
### Fase 2a — Lista de la Compra (CRUD)
### Fase 2a — Lista de la Compra (CRUD)
**Estado: Completa**
**Duración estimada: 2 semanas**
**Objetivo: funcionalidad completa de listas sin sincronización en tiempo real.**
#### 2a.0 Tests — escritos y verdes ✅
C-series (listas) y D-series (ítems) están cubiertas por:
- [x] Vitest integration (`packages/test-utils/tests/rls-{lists,items,frequency}.test.ts`) — RLS por rol, aislamiento cross-collective, trigger de frecuencia
- [x] pgTAP (`supabase/tests/002_item_frequency_trigger.sql`) — trigger de frecuencia a nivel SQL
- [x] Playwright E2E (`apps/web/tests/e2e/{lists,items}.test.ts`) — flujo UI completo: crear lista + reload persiste, papelera, archive, añadir ítem + reload persiste, check, borrado desktop, chips de sugerencias, lectura-solo para invitados
#### 2a.1 Base de datos
- [ ] Migración: tabla `shopping_lists` con `status enum(active | completed | archived)`
- [ ] Migración: tabla `shopping_items` con `name`, `quantity`, `unit`, `is_checked`, `checked_by`, `sort_order`
- [ ] RLS: solo miembros del colectivo pueden leer/escribir listas e ítems
- [ ] Índice en `shopping_items.list_id` y `shopping_items.sort_order`
- [ ] Job `pg_cron`: cada noche, mueve a `archived` las listas completadas hace > 7 días en estado borrado
- [ ] Migración: tabla `item_frequency` y trigger de recomendaciones (ver sección 2a.3)
- [x] Migración: tabla `shopping_lists` con `status enum(active | completed | archived)`
- [x] Migración: tabla `shopping_items` con `name`, `quantity`, `unit`, `is_checked`, `checked_by`, `sort_order`
- [x] RLS: solo miembros del colectivo pueden leer/escribir listas e ítems
- [x] Índice en `shopping_items.list_id` y `shopping_items.sort_order`
- [x] Job `pg_cron`: cada noche, mueve a `archived` las listas completadas hace > 7 días en estado borrado
- [x] Migración: tabla `item_frequency` y trigger de recomendaciones (ver sección 2a.3)
#### 2a.2 Frontend
- [ ] Pantalla `/lists` — listado de listas activas del colectivo
- [ ] Pantalla `/lists/[id]` — vista normal de lista:
- [x] Pantalla `/lists` — listado de listas activas del colectivo
- [x] Pantalla `/lists/[id]` — vista normal de lista:
- Añadir ítem (input rápido: name + quantity + unit) con recomendaciones de ítems frecuentes
- Editar ítem inline
- Eliminar ítem con swipe (confirmación)
- Reordenar con drag & drop (`svelte-dnd-action`)
- [ ] Papelera de listas: retención 7 días, restauración, borrado definitivo
- [ ] Store Svelte `lists` — listas del colectivo activo, optimistic updates
- [x] Papelera de listas: retención 7 días, restauración, borrado definitivo
- [x] Store Svelte `lists` — listas del colectivo activo, optimistic updates
#### 2a.3 Recomendaciones de ítems frecuentes
@@ -95,12 +104,16 @@ LIMIT 10;
- Al confirmar el ítem, el trigger registra la frecuencia automáticamente
**Tareas:**
- [ ] Migración: tabla `item_frequency` con índice
- [ ] Función `fn_record_item_frequency` + trigger `trg_item_frequency`
- [ ] RLS en `item_frequency`
- [ ] Componente `ItemSuggestions.svelte` — chips + lista filtrada
- [ ] Integrar `ItemSuggestions` en el input de añadir ítem de `/lists/[id]`
- [x] Migración: tabla `item_frequency` con índice
- [x] Función `fn_record_item_frequency` + trigger `trg_item_frequency`
- [x] RLS en `item_frequency`
- [x] Componente `ItemSuggestions.svelte` — chips + lista filtrada
- [x] Integrar `ItemSuggestions` en el input de añadir ítem de `/lists/[id]`
**Criterio de aceptación:** crear, editar, reordenar y eliminar ítems funciona fluidamente. Al añadir un ítem, aparecen sugerencias basadas en el historial del colectivo; añadir un ítem actualiza su frecuencia automáticamente.
#### 2a.Z Verificación final ✅
- [x] `just test-all` — 0 failures (81 tests verdes en total, de los cuales la cobertura de 2a se detalla en 2a.0)
---

View File

@@ -1,10 +1,37 @@
### Fase 2b — Sincronización en Tiempo Real y Modo Compra
**Estado: ⏳ Pendiente**
**Duración estimada: 23 semanas**
**Objetivo: el diferencial del producto. Es la fase más compleja técnicamente.**
Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (`just test-all`).
#### 2b.0 Tests primero — escribir antes de implementar
**Vitest (`packages/test-utils/tests/`):**
- [ ] `realtime-presence.test.ts` — dos clientes (Ana + Borja) entran en la misma lista; ambos aparecen en el canal Presence; salir del canal elimina al usuario
- [ ] `realtime-postgres-changes.test.ts` — Ana hace INSERT/UPDATE/DELETE en `shopping_items`; Borja (suscrito al canal) recibe el evento en < 1s
- [ ] `realtime-isolation.test.ts` — Ana y David (guest del mismo colectivo) están suscritos a la misma lista; Eva (fuera del colectivo) NO recibe nada aunque intente suscribirse (RLS en `supabase_realtime.broadcast`)
- [ ] `sync-queue.test.ts` (unit, con mock de `idb`) — cada mutación escribe en `pending_ops` antes de llamar a Supabase; al recibir la confirmación se elimina de la cola; si falla se mantiene para reintento
- [ ] `sync-flush.test.ts` (unit) — al disparar el evento `online` se flushea la cola respetando el orden original; colisiones resueltas con last-write-wins
**Playwright (`apps/web/tests/e2e/`):**
- [ ] `session.test.ts` — S-01: abrir Modo Compra en `/lists/[id]/session` muestra el layout full-screen sin sidebar
- [ ] `session.test.ts` — S-02: marcar un ítem lo mueve a la sección CHECKED con animación (no se duplica)
- [ ] `session.test.ts` — S-03: botón "Finish shopping" → confirmación modal → lista pasa a `completed`, redirige a `/lists`
- [ ] `realtime.test.ts` — R-01 (dos contextos de navegador): Ana crea ítem, Borja lo ve sin refrescar la página
- [ ] `realtime.test.ts` — R-02: Ana marca ítem, avatar de Borja aparece en el indicador de Presence
- [ ] `offline.test.ts` — O-01: con `context.setOffline(true)`, añadir/marcar ítems sigue funcionando localmente y aparece el banner "sin conexión"
- [ ] `offline.test.ts` — O-02: al volver online (`setOffline(false)`), los cambios pendientes se sincronizan y el banner desaparece
**pgTAP (`supabase/tests/`):**
- [ ] `004_realtime_rls.sql` — verificar que `realtime.messages` respeta las políticas RLS heredadas de `shopping_items` (un miembro sólo ve cambios de sus colectivos)
#### 2b.1 Sincronización Realtime
- [ ] Suscripción a `Postgres Changes` de Supabase Realtime sobre `items_compra` filtrada por `lista_id`
- [ ] Suscripción a `Postgres Changes` de Supabase Realtime sobre `shopping_items` filtrada por `list_id`
- [ ] Suscripción a `Presence` de Supabase Realtime para indicador de miembros presentes en lista
- [ ] Store Svelte `realtimeSync`:
- Gestiona el ciclo de vida de las suscripciones (subscribe/unsubscribe al entrar/salir de la lista)
@@ -14,7 +41,7 @@
#### 2b.2 Offline-First
- [ ] Instalación y configuración de `idb` (wrapper de IndexedDB)
- [ ] Esquema local en IndexedDB: `pending_ops`, `listas_cache`, `items_cache`
- [ ] Esquema local en IndexedDB: `pending_ops`, `lists_cache`, `items_cache`
- [ ] Cola de operaciones pendientes: cada mutación se escribe en `pending_ops` antes de llamar a Supabase
- [ ] Service Worker (`@vite-pwa/sveltekit` + Workbox):
- **NOTA TÉCNICA:** SvelteKit intercepta `src/service-worker.ts` y bloquea imports externos (solo permite `$service-worker` y `$env/static/public`). Para usar Workbox con `injectManifest` hay que nombrar el fichero `src/sw.ts` (SvelteKit no lo reconoce como SW) y configurar `strategies: 'injectManifest'`, `filename: 'sw.ts'` en `vite.config.ts`. Actualmente el plugin usa `generateSW` (SW auto-generado, precaching básico).
@@ -23,20 +50,25 @@
- Background sync para flush de `pending_ops` al recuperar conexión
- Fallback manual `online` event (Safari no soporta Background Sync API)
- [ ] Fallback manual de sync: al detectar `online` en el evento del navegador, flush de la cola (cubre Safari que no soporta Background Sync API)
- [ ] Indicador de estado de sincronización en la UI: `sincronizado | sincronizando | sin conexión`
- [ ] Indicador de estado de sincronización en la UI: `synced | syncing | offline`
#### 2b.3 Modo Compra
- [ ] Ruta `/listas/[id]/compra` — UI dedicada al Modo Compra:
- [ ] Ruta `/lists/[id]/session` — UI dedicada al Modo Compra:
- Ítems en formato grande (mínimo 64px de altura), optimizado para pulsar con el pulgar
- Reordenamiento automático al marcar: pendientes arriba, marcados abajo (animación fluida con `flip`)
- Indicador de presencia: avatares de miembros que tienen la lista abierta simultáneamente
- Banner `sin conexión` cuando no hay red (los cambios se guardan en local)
- Botón prominente **"Finalizar compra"** — confirmación modal → lista pasa a `completada`
- Botón prominente **"Finish shopping"** — confirmación modal → lista pasa a `completed`
- Notificación push (si el usuario ha dado permiso) a otros miembros al finalizar
- [ ] Rendimiento: 60fps en gama media, tap < 100ms (validar en Chrome DevTools con throttling)
- [ ] Prueba de sincronización: dos dispositivos, misma lista, cambios visibles en < 1 segundo
**Criterio de aceptación:** dos personas en el mismo supermercado, con cobertura inestable, pueden marcar ítems desde sus móviles y verse los cambios mutuamente en tiempo real. Si se pierde la conexión, los cambios locales se sincronizan al recuperarla.
#### 2b.Z Verificación final — todos los tests verdes
- [ ] `just test-all` → 0 failures, incluyendo los nuevos de 2b.0
- [ ] Prueba manual de sincronización: dos dispositivos, misma lista, cambios visibles en < 1 segundo
- [ ] Prueba manual de offline: desconectar red, mutar, reconectar, verificar sync
**Criterio de aceptación:** dos personas en el mismo supermercado, con cobertura inestable, pueden marcar ítems desde sus móviles y verse los cambios mutuamente en tiempo real. Si se pierde la conexión, los cambios locales se sincronizan al recuperarla. Toda la suite pasa con `just test-all`.
---

View File

@@ -1,14 +1,43 @@
### Fase 3 — Tareas y Notas
**Estado: ⏳ Pendiente**
**Duración estimada: 12 semanas**
**Objetivo: completar los módulos secundarios.**
Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (`just test-all`).
#### 3.0 Tests primero — escribir antes de implementar
**Vitest (`packages/test-utils/tests/`):**
- [ ] `rls-tasks.test.ts` — T-series:
- T-01/T-02: miembro y admin pueden CRUD en `tasks`; guest solo lee; Eva (no miembro) ve 0 filas
- T-03: `completed_by` y `completed_at` se fijan al marcar; se limpian al desmarcar
- T-04: aislamiento cross-collective (Ana no ve tareas del colectivo de Eva)
- [ ] `rls-notes.test.ts` — N-series:
- N-01/N-02: CRUD por rol en `notes`
- N-03: notas archivadas siguen siendo legibles; las `deleted_at` pasan a papelera
- N-04: aislamiento cross-collective
**pgTAP (`supabase/tests/`):**
- [ ] `005_tasks_rls.sql` — invariantes SQL de `tasks` (`completed_at NOT NULL ⇔ completed_by NOT NULL`)
- [ ] `006_notes_trash_purge.sql` — simular `pg_cron` job: insertar nota con `deleted_at = now() - interval '8 days'` → ejecutar el SQL del job → la nota ya no existe
**Playwright (`apps/web/tests/e2e/`):**
- [ ] `tasks.test.ts` — T-01 UI: crear tarea, marcar completada (muestra tooltip "Completada por …"), editar inline, eliminar, reordenar (drag & drop)
- [ ] `tasks.test.ts` — T-02 UI: guest (David) ve tareas pero no puede crear
- [ ] `notes.test.ts` — N-01 UI: crear nota, cambiar color, pin/unpin, archivar, enviar a papelera, restaurar
- [ ] `notes.test.ts` — N-02 UI: editor Markdown básico (bold/italic), autosave tras 500ms
- [ ] `notes.test.ts` — N-03 UI: guest ve notas pero no puede editar
#### 3.1 Tareas
- [ ] Migración: tablas `listas_tareas` y `tareas` con RLS
- [ ] Pantalla `/tareas` — listado de listas de tareas
- [ ] Migración: tablas `task_lists` y `tasks` con RLS
- [ ] Pantalla `/tasks` — listado de listas de tareas
- [ ] Vista de lista de tareas:
- Añadir tarea (input rápido)
- Checkbox de completado (registra `completada_por` + `completada_en`)
- Checkbox de completado (registra `completed_by` + `completed_at`)
- Tooltip "Completada por [Nombre] el [fecha]"
- Edición inline del título
- Swipe para eliminar
@@ -18,17 +47,23 @@
#### 3.2 Notas
- [ ] Migración: tabla `notas` con `color`, `fijada`, `archivada`, `eliminada`, `eliminada_en` + RLS
- [ ] Job `pg_cron`: borrado definitivo de notas en papelera con `eliminada_en` > 7 días
- [ ] Pantalla `/notas` — tablero tipo masonry:
- [ ] Migración: tabla `notes` con `color`, `is_pinned`, `is_archived`, `deleted_at` + RLS
- [ ] Job `pg_cron`: borrado definitivo de notas en papelera con `deleted_at` > 7 días
- [ ] Pantalla `/notes` — tablero tipo masonry:
- Notas fijadas en bloque superior
- Notas activas en rejilla de colores
- [ ] Editor de nota (`/notas/[id]`):
- [ ] Editor de nota (`/notes/[id]`):
- Editor de texto con Markdown básico (negrita, cursiva, listas, cabeceras)
- Guardado automático con debounce de 500ms
- Selector de color (8 opciones predefinidas)
- Opciones: fijar, archivar, mover a papelera, duplicar
- [ ] Archivo y papelera accesibles desde `/notas/archivo` y `/notas/papelera`
- [ ] Archivo y papelera accesibles desde `/notes/archive` y `/notes/trash`
- [ ] Sincronización vía polling a intervalos de 30 segundos
#### 3.Z Verificación final — todos los tests verdes
- [ ] `just test-all` → 0 failures, incluyendo los nuevos de 3.0
**Criterio de aceptación:** CRUD completo de tareas y notas por UI con cobertura E2E en Playwright y RLS en Vitest/pgTAP. Papelera y archive operativos. Toda la suite pasa con `just test-all`.
---

View File

@@ -1,13 +1,45 @@
### Fase 4 — Búsqueda y Pulido Final
**Estado: ⏳ Pendiente**
**Duración estimada: 1 semana**
**Objetivo: completar el MVP con calidad de producción.**
Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (`just test-all`).
#### 4.0 Tests primero — escribir antes de implementar
**Vitest (`packages/test-utils/tests/`):**
- [ ] `search.test.ts` — S-series:
- S-01: `search_in_collective(col, 'mil')` devuelve ítems con "milk" del colectivo
- S-02: respeta aislamiento cross-collective (Eva no ve nada del colectivo de Ana)
- S-03: excluye papelera (RN-08) — crear ítem, enviarlo a trash (o el contenedor correspondiente), verificar que no aparece en búsqueda
- S-04: filtros por tipo (lista/tarea/nota), por creador, por rango de fechas
**pgTAP (`supabase/tests/`):**
- [ ] `007_search_tsvector.sql` — trigger de mantenimiento de `tsvector`: INSERT/UPDATE dispara la actualización de la columna; índice GIN se usa (verificar con `EXPLAIN`)
- [ ] `008_rls_audit.sql` — tests exhaustivos por cada tabla y rol (admin/member/guest/non-member) × (SELECT/INSERT/UPDATE/DELETE) para todas las tablas de dominio
**Playwright (`apps/web/tests/e2e/`):**
- [ ] `search.test.ts` — UI: input con debounce 300ms; resultados agrupados por tipo; filtros
- [ ] `profile.test.ts` — abandonar colectivo, eliminar cuenta (con confirmación)
- [ ] `pwa.test.ts` — manifest.webmanifest accesible, service worker registrado tras login
- [ ] `rate-limit.test.ts` — disparar más peticiones de auth que el rate limit configurado en Kong; verificar 429
**Manual / herramientas externas:**
- [ ] Lighthouse PWA score ≥ 90 en mobile (ejecutar en un build de producción)
- [ ] Instalable en iOS (Safari → Añadir a pantalla de inicio) y Android (Chrome → Instalar app)
- [ ] Notificaciones push configuradas para iOS 16.4+ (requiere PWA instalada)
- [ ] Offline completo verificado: desconectar red, operar en todos los módulos, reconectar y verificar sync
#### 4.1 Búsqueda global
- [ ] Función PostgreSQL `buscar_en_colectivo(colectivo_id, query, filtros)` usando `tsvector` + `tsquery`
- [ ] Columnas `tsvector` en `items_compra`, `tareas`, `notas` con índice GIN
- [ ] Función PostgreSQL `search_in_collective(collective_id, query, filters)` usando `tsvector` + `tsquery`
- [ ] Columnas `tsvector` en `shopping_items`, `tasks`, `notes` con índice GIN
- [ ] Trigger para mantener los vectores actualizados en cada INSERT/UPDATE
- [ ] Pantalla `/buscar`:
- [ ] Pantalla `/search`:
- Input de búsqueda con debounce 300ms
- Resultados agrupados por tipo (listas, tareas, notas)
- Filtros: tipo de contenido, miembro creador, rango de fechas, estado
@@ -15,7 +47,7 @@
#### 4.2 Perfil de usuario
- [ ] Pantalla `/perfil`:
- [ ] Pantalla `/profile`:
- Editar nombre y avatar (Supabase Storage)
- Lista de colectivos a los que pertenece con rol
- Opción de abandonar colectivo
@@ -23,14 +55,25 @@
#### 4.3 PWA — Validación final
- [ ] Lighthouse PWA score ≥ 90 en mobile
- [ ] Instalable en iOS (Safari → Añadir a pantalla de inicio) y Android (Chrome → Instalar app)
- [ ] Notificaciones push configuradas para iOS 16.4+ (requiere PWA instalada)
- [ ] Offline completo verificado: desconectar red, operar en todos los módulos, reconectar y verificar sync
Implementación que necesitan los tests manuales de la sección 4.0:
- [ ] Configurar `manifest.webmanifest` con iconos (512, 192, 180), theme-color, display: standalone
- [ ] Cambiar estrategia del Service Worker a `injectManifest` con fichero renombrado a `src/sw.ts` (ver gotcha #10 en CLAUDE.md)
- [ ] Implementar notificaciones push con suscripción en el perfil
#### 4.4 Seguridad y hardening
- [ ] Auditoría de políticas RLS: verificar aislamiento total entre colectivos con tests de integración
- [ ] Auditoría de políticas RLS: verificar aislamiento total entre colectivos con tests de integración (test `008_rls_audit.sql` de 4.0)
- [ ] Rate limiting en Kong para endpoints de auth y invitaciones
- [ ] Headers de seguridad en nginx: CSP, HSTS, X-Frame-Options — gestionados externamente en la config de nginx del VPS
- [ ] Rotación de `JWT_SECRET` y `ANON_KEY` para producción (no usar los valores por defecto de Supabase)
- [ ] **Recordatorio:** al rotar `JWT_SECRET` hay que actualizar `root/.env` Y `apps/web/.env.development` a la vez, luego `docker compose up -d --force-recreate` + reiniciar Vite (ver `project_env_keys_trap` en la memoria)
#### 4.Z Verificación final — todos los tests verdes
- [ ] `just test-all` → 0 failures con los nuevos de 4.0
- [ ] Lighthouse PWA score ≥ 90 (manual)
- [ ] Prueba de instalación PWA en un iPhone y un Android reales
- [ ] Prueba de aislamiento RLS manual: login como Eva, intentar leer datos de otros colectivos vía curl con token — 0 filas
**Criterio de aceptación:** búsqueda global por UI en < 300ms, PWA instalable y funcional offline, aislamiento RLS auditado, `just test-all` verde.