Tutor IA
Módulos API: ai (controller: tutor.controller.ts)
Páginas web: TutorPanel (apps/web/src/components/lesson/TutorPanel.tsx) — panel lateral 380 px
Spec Playwright: apps/e2e/tests/student/ai-tutor.spec.ts — 📋 pendiente
Endpoints cubiertos
Section titled “Endpoints cubiertos”| Método | Endpoint | Descripción |
|---|---|---|
POST | /ai/tutor/stream | Enviar mensaje — responde en SSE palabra a palabra |
POST | /ai/tutor/chat | Enviar mensaje — respuesta completa sin streaming (legacy) |
POST | /ai/tutor/rate | Valorar un mensaje del tutor (thumbs up / thumbs down) |
GET | /ai/tutor/suggestions | Sugerencias de inicio de conversación por lección |
GET | /ai/tutor/monthly-usage | Uso mensual del usuario y límite de su plan |
GET | /ai/tutor/my-usage | Estadísticas completas de actividad (dashboard widget) |
GET | /ai/tutor/my-history | Historial paginado de conversaciones |
GET | /ai/tutor/session-info | Proveedor y modelo IA activos para tutor_chat |
User Stories
Section titled “User Stories”US-TUT-001: Abrir el panel del Tutor IA desde la página de lección
Section titled “US-TUT-001: Abrir el panel del Tutor IA desde la página de lección”Como estudiante con plan que incluye el Tutor IA Quiero abrir el panel del tutor mientras estudio una lección Para resolver dudas sin salir de la página
Criterios: AC-TUT-001 · Casos: TC-TUT-001, TC-TUT-002 · Prioridad: Alta · ✅ Implementado
US-TUT-002: El tutor recibe contexto de la lección activa
Section titled “US-TUT-002: El tutor recibe contexto de la lección activa”Como estudiante con el panel del tutor abierto Quiero que el tutor sepa qué lección estoy estudiando Para obtener respuestas relevantes al contenido que tengo delante
Criterios: AC-TUT-002 · Casos: TC-TUT-003 · Prioridad: Alta · ✅ Implementado
US-TUT-003: Enviar una pregunta y recibir respuesta en streaming SSE
Section titled “US-TUT-003: Enviar una pregunta y recibir respuesta en streaming SSE”Como estudiante usando el Tutor IA Quiero ver cómo se escribe la respuesta del tutor palabra a palabra Para tener retroalimentación inmediata sin esperar la respuesta completa
Criterios: AC-TUT-003 · Casos: TC-TUT-004, TC-TUT-005, TC-TUT-006 · Prioridad: Alta · ✅ Implementado
US-TUT-004: Ver el historial de conversaciones anteriores
Section titled “US-TUT-004: Ver el historial de conversaciones anteriores”Como estudiante que ha tenido sesiones previas con el tutor Quiero ver una lista de mis conversaciones anteriores Para retomar o revisar un hilo que inicié antes
Criterios: AC-TUT-004 · Casos: TC-TUT-007, TC-TUT-008 · Prioridad: Media · ✅ Implementado
US-TUT-005: Ver el contador de uso mensual y el límite del plan
Section titled “US-TUT-005: Ver el contador de uso mensual y el límite del plan”Como estudiante con plan que tiene límite de mensajes Quiero ver cuántos mensajes he usado este mes y cuántos me quedan Para administrar mi cuota sin llegar a un bloqueo inesperado
Criterios: AC-TUT-005 · Casos: TC-TUT-009, TC-TUT-010 · Prioridad: Alta · ✅ Implementado
US-TUT-006: Ser bloqueado al agotar el límite mensual y ver CTA de upgrade
Section titled “US-TUT-006: Ser bloqueado al agotar el límite mensual y ver CTA de upgrade”Como estudiante con plan FREE o STARTER que ha consumido todos sus mensajes Quiero ver un mensaje claro de límite alcanzado con un enlace a los planes Para entender por qué no puedo enviar más mensajes y cómo solucionarlo
Criterios: AC-TUT-006 · Casos: TC-TUT-011, TC-TUT-012 · Prioridad: Alta · ✅ Implementado
US-TUT-007: Valorar un mensaje de respuesta del tutor
Section titled “US-TUT-007: Valorar un mensaje de respuesta del tutor”Como estudiante que recibe una respuesta del Tutor IA Quiero indicar si la respuesta fue útil o no Para contribuir a la mejora de la calidad del tutor
Criterios: AC-TUT-007 · Casos: TC-TUT-013, TC-TUT-014 · Prioridad: Media · ✅ Implementado
US-TUT-008: Usuarios sin acceso al Tutor IA ven una pantalla de upgrade
Section titled “US-TUT-008: Usuarios sin acceso al Tutor IA ven una pantalla de upgrade”Como estudiante con plan que no incluye el Tutor IA Quiero ver un mensaje explicativo con un enlace a la página de precios Para saber que existe la funcionalidad y cómo activarla
Criterios: AC-TUT-008 · Casos: TC-TUT-015 · Prioridad: Alta · ✅ Implementado
US-TUT-009: Iniciar una nueva conversación limpiando el historial actual
Section titled “US-TUT-009: Iniciar una nueva conversación limpiando el historial actual”Como estudiante con el panel del tutor abierto Quiero iniciar una conversación nueva sin el hilo anterior Para hacer preguntas sobre un tema diferente con contexto limpio
Criterios: AC-TUT-009 · Casos: TC-TUT-016 · Prioridad: Media · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-TUT-001: Abrir y cerrar el panel del Tutor IA
Section titled “AC-TUT-001: Abrir y cerrar el panel del Tutor IA”US: US-TUT-001
AC-TUT-001.1 — Panel se abre con mensaje de bienvenida personalizado
Dado que soy un estudiante con plan que incluye el Tutor IAY estoy en la página de una lección activaCuando abro el panel del Tutor IAEntonces el panel se muestra con ancho de 380 px a la derecha de la lecciónY veo un mensaje de bienvenida del tutor con mi nombre de pilaY el panel muestra el título y número de la lección activa en una píldora de contextoAC-TUT-001.2 — Panel se puede cerrar
Dado que el panel del Tutor IA está abiertoCuando hago clic en el botón de cerrar (✕)Entonces el panel desaparece y la lección vuelve a ocupar el ancho completoAC-TUT-001.3 — Panel no disponible sin autenticación
Dado que no tengo sesión activaCuando intento acceder directamente a la ruta de lecciónEntonces soy redirigido al login antes de ver el panel del tutorAC-TUT-002: El tutor recibe contexto de la lección activa
Section titled “AC-TUT-002: El tutor recibe contexto de la lección activa”US: US-TUT-002
AC-TUT-002.1 — Contexto de lección se pasa al abrir el panel
Dado que estoy en la página de la lección con ID "lesson-abc"Cuando abro el panel del Tutor IAEntonces el store Redux registra moduleId y lessonId de la lección activaY la píldora de contexto muestra el número y título de la lecciónAC-TUT-002.2 — El backend recibe lessonId en la petición de stream
Dado que el panel del Tutor IA está abierto con contexto de lección "lesson-abc"Cuando envío el mensaje "¿Puedes explicar este concepto?"Entonces la petición POST /ai/tutor/stream incluye el campo lessonId con valor "lesson-abc"Y el backend carga el contenido de la lección para enriquecer el contexto del LLMAC-TUT-002.3 — Sin lección activa el tutor funciona en modo general
Dado que el panel del Tutor IA se abre sin lessonId (ej. desde el dashboard)Cuando envío una preguntaEntonces la petición no incluye lessonId o lo incluye vacíoY el tutor responde en modo general sin contexto de lecciónAC-TUT-003: Enviar mensaje y recibir respuesta en streaming SSE
Section titled “AC-TUT-003: Enviar mensaje y recibir respuesta en streaming SSE”US: US-TUT-003
AC-TUT-003.1 — Los tokens aparecen progresivamente mientras se recibe el stream
Dado que tengo el panel del Tutor IA abierto y mi pregunta escritaCuando envío el mensajeEntonces aparece una burbuja del tutor con puntos de escritura (typing dots) inmediatamenteY a medida que llegan eventos SSE de tipo "chunk", el texto de la burbuja crece palabra a palabraY se muestra un cursor parpadeante al final del texto mientras el stream sigue activoAC-TUT-003.2 — Al recibir el evento “done”, el mensaje queda finalizado
Dado que se está recibiendo un stream del tutorCuando llega el evento SSE de tipo "done" con messageIdEntonces el cursor parpadeante desapareceY aparecen los botones de rating (👍 / 👎) bajo la burbuja del asistenteY el campo de texto se habilita para escribir la siguiente preguntaAC-TUT-003.3 — El campo de texto se deshabilita durante el streaming
Dado que hay un stream de respuesta en cursoCuando el estado Redux es "streaming"Entonces el textarea aparece con opacidad reducida y no acepta nuevas entradasY el botón de enviar está deshabilitadoAC-TUT-003.4 — Aviso de respuesta lenta tras 8 segundos
Dado que el backend no ha enviado el primer chunk en 8 segundosCuando el timer de lentitud expiraEntonces aparece el mensaje "Esto está tardando un poco más de lo normal..."Y el aviso desaparece cuando llega el primer chunkAC-TUT-003.5 — Error de servicio muestra caja de error con reintento
Dado que la petición POST /ai/tutor/stream falla con un error de red o el servidor devuelve status >= 400Cuando el status Redux pasa a "error"Entonces aparece una caja de error roja con el texto "El tutor no está disponible momentáneamente"Y hay un botón "↺ Reintentar" que restaura el estado a "idle" para permitir un nuevo envíoAC-TUT-004: Ver el historial de conversaciones
Section titled “AC-TUT-004: Ver el historial de conversaciones”US: US-TUT-004
AC-TUT-004.1 — Abrir el historial muestra un drawer sobre el panel
Dado que tengo el panel del Tutor IA abiertoCuando hago clic en el botón de historial (☰)Entonces aparece un drawer con la lista de conversaciones anterioresY cada conversación muestra el nombre del módulo, la fecha y el número de mensajesY el primer mensaje del usuario se muestra como preview truncadoAC-TUT-004.2 — Sin conversaciones previas el historial muestra un mensaje vacío
Dado que el usuario nunca ha tenido una sesión con el tutorCuando abre el drawer de historialEntonces ve el texto "No tienes conversaciones anteriores"Y no aparecen tarjetas de sesiónAC-TUT-004.3 — El historial se cierra al pulsar el botón de cierre
Dado que el drawer de historial está abiertoCuando hago clic en el botón ✕ del drawerEntonces el drawer desaparece y vuelvo a ver el chat activoAC-TUT-005: Ver el contador de uso mensual
Section titled “AC-TUT-005: Ver el contador de uso mensual”US: US-TUT-005
AC-TUT-005.1 — Barra de uso visible para planes con límite
Dado que soy un usuario FREE o STARTER (plan con límite mensual > 0)Cuando el panel del Tutor IA está abiertoEntonces veo una barra de progreso en la parte inferior del panelY muestra "Consultas este mes: X / Y"Y el color de la barra es púrpura al 0-79%, ámbar al 80-94% y rojo al 95%+AC-TUT-005.2 — La barra no aparece para planes ilimitados
Dado que soy un usuario PRO, ELITE o con rol corporativo/admin (limit = 0)Cuando el panel del Tutor IA está abiertoEntonces no se muestra la barra de uso mensualAC-TUT-005.3 — La API reporta correctamente el uso acumulado
Dado que he enviado mensajes al tutor este mesCuando consulto GET /ai/tutor/monthly-usageEntonces la respuesta incluye "used", "limit" y "plan"Y "used" refleja el número de TutorMessages con role="user" desde el primer día del mesAC-TUT-006: Bloqueo al alcanzar el límite mensual
Section titled “AC-TUT-006: Bloqueo al alcanzar el límite mensual”US: US-TUT-006
AC-TUT-006.1 — Al alcanzar el límite, el input se reemplaza por un CTA
Dado que usageThisMonth es igual o mayor que usageLimit (> 0)Cuando el panel del Tutor IA se abre o se recibe la respuesta de monthly-usageEntonces el textarea y el botón de enviar desaparecenY aparece un bloque con el texto "Has alcanzado el límite mensual"Y hay un enlace "Ver planes ↗" que apunta a /pricingAC-TUT-006.2 — El endpoint /stream devuelve 429 cuando se supera el límite
Dado que el usuario FREE ha enviado ya 3 mensajes este mesCuando intenta enviar un cuarto mensaje vía POST /ai/tutor/streamEntonces la API devuelve status 429Y el cuerpo incluye { code: "TUTOR_LIMIT_REACHED", used: 3, limit: 3, resetDate: "<inicio próximo mes>" }AC-TUT-006.3 — El límite se reinicia automáticamente el primer día del mes
Dado que hoy es el primer día del nuevo mesCuando el usuario FREE consulta GET /ai/tutor/monthly-usageEntonces "used" es 0Y puede volver a enviar mensajes normalmenteAC-TUT-007: Valorar un mensaje del tutor
Section titled “AC-TUT-007: Valorar un mensaje del tutor”US: US-TUT-007
AC-TUT-007.1 — Los botones de rating aparecen bajo cada respuesta del asistente
Dado que el tutor ha respondido y el evento "done" se ha recibidoCuando visualizo la burbuja de respuestaEntonces bajo la burbuja aparecen dos botones: 👍 (Útil) y 👎 (No útil)Y ambos están en estado inactivo (sin color de acento)AC-TUT-007.2 — Seleccionar rating actualiza el estado local y llama a la API
Dado que los botones de rating están en estado inactivoCuando hago clic en 👍Entonces el botón 👍 se activa (fondo verde, borde verde)Y se llama POST /ai/tutor/rate con { messageId, rating: "up" }Y el backend actualiza el campo rating del TutorMessage correspondienteAC-TUT-007.3 — El mensaje de bienvenida no tiene botones de rating
Dado que el panel acaba de abrirse y muestra el mensaje de bienvenidaCuando visualizo la burbuja con id "welcome"Entonces no hay botones de rating bajo ese mensajeAC-TUT-007.4 — Un rating incorrecto (mensaje de otro usuario) devuelve ok: false
Dado que intento valorar un messageId que no pertenece al usuario autenticadoCuando llamo POST /ai/tutor/rate con ese messageIdEntonces la API devuelve { ok: false } con status 200Y el rating no se modifica en la BDAC-TUT-008: Pantalla de upgrade para planes sin acceso al Tutor IA
Section titled “AC-TUT-008: Pantalla de upgrade para planes sin acceso al Tutor IA”US: US-TUT-008
AC-TUT-008.1 — Plan con aiTutor = “none” muestra CTA en lugar del chat
Dado que mi plan tiene la feature aiTutor con valor "none"Cuando abro el panel del Tutor IAEntonces veo el panel con header pero sin área de chatY hay un bloque con el texto "El Tutor IA no está incluido en tu plan actual"Y hay un botón "Ver planes →" que apunta a /pricingAC-TUT-008.2 — Un usuario sin plan definido no queda bloqueado
Dado que el hook useUserPlanFeature no ha terminado de cargar (isLoading = true)Cuando el panel se renderizaEntonces el bloque de upgrade no aparece (se espera a que cargue el plan)AC-TUT-009: Iniciar una nueva conversación
Section titled “AC-TUT-009: Iniciar una nueva conversación”US: US-TUT-009
AC-TUT-009.1 — El botón de nueva conversación limpia el chat
Dado que hay mensajes en el panel del tutorCuando hago clic en el botón de nueva conversación (↺)Entonces todos los mensajes del hilo actual se eliminan del estado ReduxY el panel muestra únicamente el mensaje de bienvenidaY el status vuelve a "idle"AC-TUT-009.2 — La nueva conversación no elimina el historial persistido en BD
Dado que inicié una nueva conversación en el clienteCuando consulto GET /ai/tutor/my-historyEntonces las conversaciones anteriores siguen presentes en la respuestaTest Cases
Section titled “Test Cases”TC-TUT-001: Panel se abre con contexto de lección y mensaje de bienvenida
Section titled “TC-TUT-001: Panel se abre con contexto de lección y mensaje de bienvenida”AC: AC-TUT-001.1, AC-TUT-002.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.pro está autenticado y en la página de una lección con moduleId y lessonId conocidos.
Pasos:
- Navegar a
/learn/lesson/{lessonId} - Hacer clic en el botón que abre el panel del tutor (
data-testid="open-tutor-panel") - Esperar a que el panel sea visible
Resultado esperado: El panel aparece con ancho 380 px. La píldora de contexto muestra el título de la lección. El primer mensaje tiene rol “assistant” y contiene el nombre de pila del usuario.
import { fixtures } from '../../fixtures/users.fixture'
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await expect(page.getByTestId('tutor-panel')).toBeVisible()await expect(page.getByTestId('tutor-context-pill')).toContainText(LESSON_TITLE)await expect(page.getByTestId('tutor-message-welcome')).toContainText('¡Hola')TC-TUT-002: Panel se cierra al pulsar el botón ✕
Section titled “TC-TUT-002: Panel se cierra al pulsar el botón ✕”AC: AC-TUT-001.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await expect(page.getByTestId('tutor-panel')).toBeVisible()await page.getByTestId('tutor-close-button').click()await expect(page.getByTestId('tutor-panel')).not.toBeVisible()TC-TUT-003: El backend recibe lessonId en la petición de stream
Section titled “TC-TUT-003: El backend recibe lessonId en la petición de stream”AC: AC-TUT-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.pro tiene token JWT válido. Se usa mockNappAI para evitar llamadas reales al LLM.
import { apiClient } from '../../utils/api-client'import { mockNappAI } from '../../utils/nappai-mock'
mockNappAI.tutor.respondWith({ tokens: ['Esta', ' es', ' la', ' respuesta'] })
const res = await apiClient.post( '/ai/tutor/chat', { lessonId: 'lesson-abc', message: '¿Puedes explicar este concepto?', conversationHistory: [], }, fixtures.pro.token,)expect(res.status).toBe(201)expect(res.data.answer).toBeDefined()TC-TUT-004: Streaming muestra tokens progresivamente
Section titled “TC-TUT-004: Streaming muestra tokens progresivamente”AC: AC-TUT-003.1, AC-TUT-003.2 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.pro autenticado. mockNappAI configurado con tokens de prueba.
Pasos:
- Abrir el panel del tutor en una lección
- Escribir una pregunta en el textarea
- Enviar con Enter o el botón de envío
- Observar la aparición del cursor parpadeante durante el stream
- Esperar a que aparezcan los botones de rating
Resultado esperado: El cursor parpadeante (data-testid="tutor-streaming-cursor") es visible durante el stream y desaparece cuando aparecen los botones de rating.
import { mockNappAI } from '../../utils/nappai-mock'
mockNappAI.tutor.respondWith({ tokens: ['Hola', ' esto', ' es', ' un', ' test'] })
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Mensaje al tutor').fill('¿Qué es esto?')await page.getByLabel('Enviar mensaje').click()await expect(page.getByTestId('tutor-streaming-cursor')).toBeVisible()await expect(page.getByTestId('tutor-rating-up')).toBeVisible({ timeout: 15_000 })await expect(page.getByTestId('tutor-streaming-cursor')).not.toBeVisible()TC-TUT-005: El textarea se deshabilita durante el streaming
Section titled “TC-TUT-005: El textarea se deshabilita durante el streaming”AC: AC-TUT-003.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
mockNappAI.tutor.respondWith({ tokens: ['Respuesta', ' larga'] })
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Mensaje al tutor').fill('Pregunta')await page.getByLabel('Enviar mensaje').click()
// Durante el stream el textarea debe estar deshabilitadoconst textarea = page.getByLabel('Mensaje al tutor')await expect(textarea).toBeDisabled()await expect(page.getByLabel('Enviar mensaje')).toBeDisabled()
// Tras finalizar el stream se vuelve a habilitarawait expect(textarea).not.toBeDisabled({ timeout: 15_000 })TC-TUT-006: Error de servicio muestra caja de error con reintento
Section titled “TC-TUT-006: Error de servicio muestra caja de error con reintento”AC: AC-TUT-003.5 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
// Simular fallo del endpoint de streamawait page.route('**/ai/tutor/stream', route => route.abort('failed'))
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Mensaje al tutor').fill('Pregunta')await page.getByLabel('Enviar mensaje').click()
await expect(page.getByTestId('tutor-error-box')).toBeVisible()await expect(page.getByTestId('tutor-error-box')).toContainText('no está disponible')await page.getByTestId('tutor-retry-button').click()await expect(page.getByTestId('tutor-error-box')).not.toBeVisible()TC-TUT-007: Historial muestra conversaciones anteriores
Section titled “TC-TUT-007: Historial muestra conversaciones anteriores”AC: AC-TUT-004.1 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.pro tiene al menos una conversación previa en BD.
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Historial de conversaciones').click()
await expect(page.getByTestId('tutor-history-drawer')).toBeVisible()await expect(page.getByTestId('tutor-history-item').first()).toBeVisible()TC-TUT-008: Historial vacío muestra mensaje informativo
Section titled “TC-TUT-008: Historial vacío muestra mensaje informativo”AC: AC-TUT-004.2 · Prioridad: Baja · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
// Usuario nuevo sin conversacionesconst res = await apiClient.get('/ai/tutor/my-history?page=1&limit=20', fixtures.free.token)expect(res.status).toBe(200)expect(res.data.sessions).toHaveLength(0)expect(res.data.total).toBe(0)expect(res.data.hasMore).toBe(false)TC-TUT-009: Barra de uso visible para plan FREE
Section titled “TC-TUT-009: Barra de uso visible para plan FREE”AC: AC-TUT-005.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
import { fixtures } from '../../fixtures/users.fixture'
// fixtures.free tiene plan con límite 3 mensajes/mesawait loginAs(page, fixtures.free)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()
await expect(page.getByTestId('tutor-usage-bar')).toBeVisible()await expect(page.getByTestId('tutor-usage-label')).toContainText('Consultas este mes')TC-TUT-010: Barra de uso no aparece para plan PRO
Section titled “TC-TUT-010: Barra de uso no aparece para plan PRO”AC: AC-TUT-005.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()
await expect(page.getByTestId('tutor-usage-bar')).not.toBeVisible()TC-TUT-011: API devuelve 429 al superar el límite mensual FREE
Section titled “TC-TUT-011: API devuelve 429 al superar el límite mensual FREE”AC: AC-TUT-006.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.free tiene exactamente 3 mensajes de rol “user” en TutorMessage este mes (límite consumido).
const res = await apiClient.post( '/ai/tutor/chat', { message: 'Cuarto mensaje', conversationHistory: [] }, fixtures.free.token,)expect(res.status).toBe(429)expect(res.data.code).toBe('TUTOR_LIMIT_REACHED')expect(res.data.used).toBe(3)expect(res.data.limit).toBe(3)expect(res.data.resetDate).toBeDefined()TC-TUT-012: CTA de upgrade reemplaza el input al alcanzar el límite
Section titled “TC-TUT-012: CTA de upgrade reemplaza el input al alcanzar el límite”AC: AC-TUT-006.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: El mock de GET /ai/tutor/monthly-usage devuelve { used: 3, limit: 3, plan: 'FREE' }.
await page.route('**/ai/tutor/monthly-usage', route => route.fulfill({ json: { used: 3, limit: 3, plan: 'FREE' } }),)
await loginAs(page, fixtures.free)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()
await expect(page.getByLabel('Mensaje al tutor')).not.toBeVisible()await expect(page.getByTestId('tutor-limit-cta')).toBeVisible()await expect(page.getByTestId('tutor-limit-cta')).toContainText('Has alcanzado el límite mensual')const link = page.getByTestId('tutor-upgrade-link')await expect(link).toHaveAttribute('href', '/pricing')TC-TUT-013: Rating “útil” actualiza el estado y llama a la API
Section titled “TC-TUT-013: Rating “útil” actualiza el estado y llama a la API”AC: AC-TUT-007.2 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
mockNappAI.tutor.respondWith({ tokens: ['Respuesta', ' de', ' prueba'] })
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Mensaje al tutor').fill('Explícame esto')await page.getByLabel('Enviar mensaje').click()await expect(page.getByTestId('tutor-rating-up')).toBeVisible({ timeout: 15_000 })
// Interceptar llamada a la API de ratingconst [rateRequest] = await Promise.all([ page.waitForRequest(req => req.url().includes('/ai/tutor/rate') && req.method() === 'POST'), page.getByTestId('tutor-rating-up').click(),])
const body = JSON.parse(rateRequest.postData() ?? '{}')expect(body.rating).toBe('up')expect(body.messageId).toBeDefined()
// El botón queda activo (color verde)await expect(page.getByTestId('tutor-rating-up')).toHaveAttribute('aria-pressed', 'true')TC-TUT-014: Rating de mensaje ajeno devuelve ok: false
Section titled “TC-TUT-014: Rating de mensaje ajeno devuelve ok: false”AC: AC-TUT-007.4 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente
// fixtures.pro intenta valorar un messageId que pertenece a fixtures.freeconst res = await apiClient.post( '/ai/tutor/rate', { messageId: FOREIGN_MESSAGE_ID, rating: 'up' }, fixtures.pro.token,)expect(res.status).toBe(200)expect(res.data.ok).toBe(false)TC-TUT-015: Plan sin Tutor IA muestra pantalla de upgrade
Section titled “TC-TUT-015: Plan sin Tutor IA muestra pantalla de upgrade”AC: AC-TUT-008.1 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Precondiciones: fixtures.free tiene aiTutor = 'none' en la definición de su plan en BD.
// Mockear la respuesta de plan-info para simular aiTutor = "none"await page.route('**/users/plan-info', route => route.fulfill({ json: { planName: 'Free', features: { aiTutor: 'none' } } }),)
await loginAs(page, fixtures.free)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()
await expect(page.getByTestId('tutor-upgrade-cta')).toBeVisible()await expect(page.getByTestId('tutor-upgrade-cta')).toContainText('no está incluido en tu plan')await expect(page.getByLabel('Mensaje al tutor')).not.toBeVisible()const upgradeLink = page.getByRole('link', { name: /ver planes/i })await expect(upgradeLink).toHaveAttribute('href', '/pricing')TC-TUT-016: Nueva conversación limpia el chat pero no el historial en BD
Section titled “TC-TUT-016: Nueva conversación limpia el chat pero no el historial en BD”AC: AC-TUT-009.1, AC-TUT-009.2 · Prioridad: Media · Tipo: API + E2E · Estado: 📋 Pendiente
Spec: apps/e2e/tests/student/ai-tutor.spec.ts
Parte E2E — limpieza del estado local:
mockNappAI.tutor.respondWith({ tokens: ['Hola', ' respuesta'] })
await loginAs(page, fixtures.pro)await page.goto(`/learn/lesson/${LESSON_ID}`)await page.getByTestId('open-tutor-panel').click()await page.getByLabel('Mensaje al tutor').fill('Primera pregunta')await page.getByLabel('Enviar mensaje').click()await expect(page.getByTestId('tutor-rating-up')).toBeVisible({ timeout: 15_000 })
// Iniciar nueva conversaciónawait page.getByLabel('Nueva conversación').click()
// Solo debe quedar el mensaje de bienvenidaconst messages = page.getByTestId('tutor-message')await expect(messages).toHaveCount(1)await expect(messages.first()).toContainText('¡Hola')Parte API — historial persiste:
// El historial en BD sigue teniendo la conversación anteriorconst res = await apiClient.get('/ai/tutor/my-history?page=1&limit=20', fixtures.pro.token)expect(res.status).toBe(200)expect(res.data.total).toBeGreaterThan(0)Última actualización: 2026-05-29 · Generado por agente QA-Docs