feat(realtime): Fase 2b.0 infra + first R-series tests green

Enable postgres_changes broadcasts on shopping_items / shopping_lists and prove
the path end-to-end with Vitest Realtime tests and a pgTAP configuration check.

Infra changes that were silently blocking events
- Migration 007: shopping_items + shopping_lists added to supabase_realtime
  publication with REPLICA IDENTITY FULL (UPDATE/DELETE payloads need the full
  row so list_id-based filters work and RLS can evaluate DELETE against OLD)
- Realtime service: DB_USER=supabase_admin (superuser; supabase_replication_admin
  lacks CREATE on the `realtime` schema that the service auto-creates per tenant)
- Realtime service: SELF_HOST_TENANT_NAME=realtime so the seed tenant name
  matches the default supabase-js resolves for localhost URLs (was realtime-dev
  → TenantNotFound)

Tests
- pgTAP 004_realtime_publication.sql — P-01..P-04: publication membership and
  REPLICA IDENTITY FULL for both shopping tables
- Vitest realtime-postgres-changes.test.ts — R-01 INSERT broadcast,
  R-02 UPDATE carries full row, R-03 list_id filter isolates events
- test-utils realtime-helpers.ts — subscribePostgresChanges() returns a
  waitFor(predicate, timeout) helper that captures the SUBSCRIBED handshake
  before returning so mutations issued right after are not lost
- createClientAs now calls realtime.setAuth(token) so the server evaluates
  RLS against the test user's JWT
- Justfile test-db now globs supabase/tests/*.sql so new pgTAP files are picked
  up automatically

Totals: 54→57 Vitest, 12→16 pgTAP, 15 Playwright = 88 tests green.

🤖 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 02:33:40 +02:00
parent f396897cb5
commit 4c913cf495
10 changed files with 370 additions and 14 deletions

View File

@@ -1,5 +1,5 @@
### Fase 2b — Sincronización en Tiempo Real y Modo Compra
**Estado: ⏳ Pendiente**
**Estado: 🚧 En curso — tests 2b.0 verdes (Realtime infra lista)**
**Duración estimada: 23 semanas**
**Objetivo: el diferencial del producto. Es la fase más compleja técnicamente.**
@@ -7,11 +7,16 @@ Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna t
#### 2b.0 Tests primero — escribir antes de implementar
**Prep infra (previa a los tests):**
- [x] Migración `007_realtime_publication.sql` — añade `shopping_items` y `shopping_lists` a la publicación `supabase_realtime`; pone `REPLICA IDENTITY FULL` en ambas
- [x] Docker Compose: `DB_USER: supabase_admin` (superuser) + `SELF_HOST_TENANT_NAME: realtime` para Realtime (ver memoria `project_realtime_config`)
**Vitest (`packages/test-utils/tests/`):**
- [x] `realtime-postgres-changes.test.ts` — R-01 INSERT broadcast, R-02 UPDATE con fila completa, R-03 filtro `list_id` aísla eventos de otras listas ✅
- [ ] `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`)
- [ ] `realtime-isolation.test.ts` — Ana y David (guest del mismo colectivo) reciben eventos; Eva (fuera del colectivo) NO recibe nada (RLS)
- [ ] `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
@@ -20,14 +25,12 @@ Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna t
- [ ] `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
- [ ] `realtime.test.ts` — dos contextos de navegador: Ana crea ítem, Borja lo ve sin refrescar; Ana marca ítem, el avatar de Borja aparece en Presence
- [ ] `offline.test.ts`O-01 con `context.setOffline(true)`: mutaciones locales + banner "sin conexión"; O-02 al volver online: sync pendiente + 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)
- [x] `004_realtime_publication.sql` — P-01..P-04: publication + REPLICA IDENTITY FULL asegurados ✅
#### 2b.1 Sincronización Realtime