Files
inq-roi-simulador-web/sprints/sprint-6/ETAPA_3_PROMPT.md
2026-07-06 18:52:24 -03:00

9.8 KiB

Etapa 3 — Limpieza técnica y diagnóstico de auth

Sprint: 6 Fecha: 2026-06-24 Alcance: fix confirmado de localStorage key en ProfileSheet, eliminación de setTimeout(350), spec E2E de auth, actualización de TECH_DEBT.md.


Contexto

Esta etapa cierra deuda técnica documentada y resuelve una contradicción activa en el manejo de auth que podría ser un bug real de persistencia de perfil.


Parte A — Bug potencial: localStorage key en ProfileSheet

El problema

ProfileSheet.getAccessToken() lee de la clave 'supabase.auth.token':

// ProfileSheet.tsx línea 14-23
function getAccessToken(): string | null {
  try {
    const raw = localStorage.getItem('supabase.auth.token')  // ← clave potencialmente incorrecta
    ...
  }
}

Pero auth-setup.ts documenta y confirma empíricamente que @supabase/supabase-js v2's createClient() escribe la sesión bajo sb-${projectRef}-auth-token, no bajo supabase.auth.token. El projectRef se deriva de la URL del proyecto.

Si en producción la sesión está bajo sb-${projectRef}-auth-token, entonces localStorage.getItem('supabase.auth.token') retorna nullgetAccessToken() retorna nullif (!token) return en línea 67 silencia el error → el persist a Supabase de nombre y empresa nunca se ejecuta. El usuario ve el cambio en UI (vía updateUser() en memoria) pero no se persiste.

Diagnóstico requerido

Antes de modificar código, confirmar el estado real con un spec o con console.log en dev:

Opción A (spec): agregar un spec E2E tests/e2e/auth-localStorage-key.spec.ts que:

  1. Navega con storageState de auth
  2. Evalúa document.cookie o localStorage en el contexto del browser
  3. Afirma qué clave de auth existe realmente (supabase.auth.token vs sb-${projectRef}-auth-token)
  4. Afirma que la clave correcta contiene un access_token válido (string no vacío)

Fix según diagnóstico:

Si la clave real es sb-${projectRef}-auth-token (probable):

// Reemplazar getAccessToken() completa:
function getAccessToken(): string | null {
  try {
    // supabase-js v2 calcula la clave como sb-{projectRef}-auth-token
    // donde projectRef = hostname.split('.')[0] de la URL del proyecto.
    const projectRef = new URL(import.meta.env.VITE_SUPABASE_URL as string).hostname.split('.')[0]
    const storageKey = `sb-${projectRef}-auth-token`
    const raw = localStorage.getItem(storageKey)
    if (!raw) return null
    const parsed = JSON.parse(raw) as { access_token?: string }
    return parsed.access_token ?? null
  } catch {
    return null
  }
}

Si en producción ambas claves coexisten (posible si hay versiones mixtas de @supabase/auth-js), leer con fallback:

const raw = localStorage.getItem(storageKey) ?? localStorage.getItem('supabase.auth.token')

Actualizar el comentario en ProfileSheet.tsx para reflejar la clave real y el diagnóstico confirmado. El comentario actual dice "STORAGE_KEY = 'supabase.auth.token'" — eso era la documentación de auth-js v1/v2-old, no de supabase-js v2.

Criterios de Parte A

  • Spec E2E confirma qué clave existe en localStorage en producción bajo auth real
  • getAccessToken() lee la clave correcta
  • Comentario en ProfileSheet.tsx refleja la clave real y la razón
  • El save de perfil (nombre, empresa) persiste en Supabase — verificar manualmente en Studio o via spec

Parte B — Eliminar setTimeout(350) de ProfileSheet

El problema

ProfileSheet.handleSave() difiere updateUser() 350ms:

// ProfileSheet.tsx líneas 57-60
onOpenChange(false)
setTimeout(() => {
  updateUser({ name: savedName, company: savedCompany })
}, 350)

El comentario dice que es para evitar que @radix-ui/react-remove-scroll deje pointer-events:none permanente en el body. Pero src/components/ui/sheet.tsx ya tiene el fix definitivo (useEffect sobre prop open que restaura pointer-events defensivamente). El setTimeout es redundante.

Fix

  1. Reemplazar el bloque con llamada directa:
onOpenChange(false)
updateUser({ name: savedName, company: savedCompany })
  1. Verificar manualmente que después de guardar el perfil, la UI responde (no hay pointer-events:none atascado en body). Si el fix de sheet.tsx es suficiente, el comportamiento es idéntico. Si reaparece el bug, restaurar el setTimeout y documentar que el fix de sheet.tsx no es suficiente solo.

  2. Actualizar el comentario para explicar que el fix vive en sheet.tsx.

Criterios de Parte B

  • setTimeout(350) eliminado de ProfileSheet.tsx
  • Verificación manual: guardar perfil → UI responde correctamente, sin pointer-events bloqueados
  • Comentario actualizado explicando que el fix está en sheet.tsx

Parte C — Spec E2E de validación de auth real

Independientemente de la resolución de Parte A, agregar un spec que valide que el auth E2E funciona realmente (no solo que el storageState se carga, sino que Supabase devuelve datos reales del usuario).

Archivo: tests/e2e/auth-real.spec.ts

import { test, expect } from '@playwright/test'

test.use({ storageState: 'tests/e2e/setup/auth-state.json' })

test('auth E2E: Supabase devuelve datos reales del usuario autenticado', async ({ page }) => {
  // Navegar a la app autenticada
  await page.goto('http://localhost:4173/')

  // Esperar a que AppHeader muestre el avatar/nombre (confirma que AuthContext cargó la sesión)
  await expect(page.getByTestId('app-header-user-avatar')).toBeVisible({ timeout: 10000 })

  // Verificar que el email del usuario de test aparece en el dropdown de perfil
  // (o bien hacer una query directa a Supabase REST para confirmar que el token es válido)
  const email = process.env.TEST_USER_EMAIL
  if (email) {
    await page.getByTestId('app-header-user-avatar').click()
    await expect(page.getByText(email)).toBeVisible()
  }
})

Adaptar según los data-testid reales del AppHeader (consultarlos antes de escribir el spec).

Si TEST_USER_EMAIL no está en .env.test, el spec hace skip con mensaje descriptivo (mismo patrón de export-pdf.spec.ts).

Criterios de Parte C

  • Spec en tests/e2e/auth-real.spec.ts que pasa cuando auth funciona
  • Spec hace test.skip() descriptivo si TEST_USER_EMAIL no está configurado
  • Si el spec falla, el diagnóstico es definitivo: el auth E2E no funciona realmente → fix en auth-setup.ts o en AuthContext

Parte D — Actualización de TECH_DEBT.md

Al terminar A, B y C, actualizar TECH_DEBT.md:

  1. Marcar [INVESTIGATING] Clave de localStorage del auth E2E vs producción como [RESOLVED] con el resultado real del diagnóstico.

  2. Marcar [BACKLOG] setTimeout(350) en ProfileSheet como [RESOLVED].

  3. Agregar nuevo ítem:

### [BACKLOG] CumulativeSavingsChart — derivación de inversión base desde cumulativeSavingsHorizon
**Estado actual:** cuando `roiBase` está presente (modo sensibilidad activo), el componente
recupera la inversión original algebraicamente: `investment_base = roiBase.annualSavings * horizonYears - roiBase.cumulativeSavingsHorizon`.
Es correcto mientras `horizonYears` del chart coincida con el usado en `calculateRoi` (hoy siempre es el mismo).
**Riesgo:** si en el futuro el chart recibe un horizonte diferente al del cálculo de ROI, el valor
de referencia del eje Y para la línea base será incorrecto, sin error visible.
**Solución propuesta:** agregar prop `investmentBase?: number` a `CumulativeSavingsChartProps`
y pasar `process.automationInvestment` directamente desde `ReportPage`.
**Costo:** bajo. **Origen:** Sprint 6 Etapa 2 — detectado en auditoría post-implementación.

Parte E — Opcional: animación de expansión en ActivityPanel

Solo implementar si las partes A-D no consumen el slot completo.

La sección de automatización en ActivityPanel usa max-height para expandir/colapsar. La aceleración no es lineal en los últimos píxeles (se nota el "freno" al final).

Fix: usar useRef + scrollHeight para animar a la altura real del contenido:

const contentRef = useRef<HTMLDivElement>(null)
// Al expandir:
contentRef.current.style.maxHeight = contentRef.current.scrollHeight + 'px'
// Al colapsar:
contentRef.current.style.maxHeight = '0'

Con transición CSS transition-[max-height] duration-300 ease-in-out.

Verificar visualmente antes de commitear.


Orden de ejecución recomendado

  1. Parte A (diagnóstico y fix de localStorage key) — puede revelar un bug real, prioridad más alta
  2. Parte B (setTimeout cleanup) — depende de confirmar que sheet.tsx fix es suficiente
  3. Parte C (spec E2E) — complementa Parte A
  4. Parte D (TECH_DEBT.md) — al final, cuando A-C estén resueltos
  5. Parte E (animación) — solo si hay tiempo

Criterios de validación del conjunto

  • getAccessToken() en ProfileSheet.tsx usa la clave correcta de localStorage
  • Save de perfil persiste en Supabase (nombre y empresa visibles al recargar)
  • setTimeout(350) eliminado, UI funciona correctamente después de guardar perfil
  • Spec E2E de auth pasa o hace skip descriptivo
  • TECH_DEBT.md actualizado: ítems resueltos marcados, nueva nota de CumulativeSavingsChart
  • npm run lint sin errores
  • npm run build limpio
  • npm run test → 552+ tests verdes
  • Validación del director sobre el estado de la documentación antes del OK formal

Archivos a modificar

  • src/components/ProfileSheet.tsx — clave localStorage + eliminar setTimeout
  • tests/e2e/auth-localStorage-key.spec.ts — spec de diagnóstico (nuevo)
  • tests/e2e/auth-real.spec.ts — spec de validación E2E (nuevo)
  • TECH_DEBT.md — marcar resueltos + agregar nota CumulativeSavingsChart

Opcional:

  • src/features/workspace/ActivityPanel.tsx — animación scrollHeight

No modificar:

  • tests/e2e/setup/auth-setup.ts — ya documentado correctamente
  • Ningún otro archivo fuera de esta lista sin consulta previa