Skip to content

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


MétodoEndpointDescripción
POST/ai/tutor/streamEnviar mensaje — responde en SSE palabra a palabra
POST/ai/tutor/chatEnviar mensaje — respuesta completa sin streaming (legacy)
POST/ai/tutor/rateValorar un mensaje del tutor (thumbs up / thumbs down)
GET/ai/tutor/suggestionsSugerencias de inicio de conversación por lección
GET/ai/tutor/monthly-usageUso mensual del usuario y límite de su plan
GET/ai/tutor/my-usageEstadísticas completas de actividad (dashboard widget)
GET/ai/tutor/my-historyHistorial paginado de conversaciones
GET/ai/tutor/session-infoProveedor y modelo IA activos para tutor_chat

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


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 IA
Y estoy en la página de una lección activa
Cuando abro el panel del Tutor IA
Entonces el panel se muestra con ancho de 380 px a la derecha de la lección
Y veo un mensaje de bienvenida del tutor con mi nombre de pila
Y el panel muestra el título y número de la lección activa en una píldora de contexto

AC-TUT-001.2 — Panel se puede cerrar

Dado que el panel del Tutor IA está abierto
Cuando hago clic en el botón de cerrar (✕)
Entonces el panel desaparece y la lección vuelve a ocupar el ancho completo

AC-TUT-001.3 — Panel no disponible sin autenticación

Dado que no tengo sesión activa
Cuando intento acceder directamente a la ruta de lección
Entonces soy redirigido al login antes de ver el panel del tutor

AC-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 IA
Entonces el store Redux registra moduleId y lessonId de la lección activa
Y la píldora de contexto muestra el número y título de la lección

AC-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 LLM

AC-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 pregunta
Entonces la petición no incluye lessonId o lo incluye vacío
Y el tutor responde en modo general sin contexto de lección

AC-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 escrita
Cuando envío el mensaje
Entonces aparece una burbuja del tutor con puntos de escritura (typing dots) inmediatamente
Y a medida que llegan eventos SSE de tipo "chunk", el texto de la burbuja crece palabra a palabra
Y se muestra un cursor parpadeante al final del texto mientras el stream sigue activo

AC-TUT-003.2 — Al recibir el evento “done”, el mensaje queda finalizado

Dado que se está recibiendo un stream del tutor
Cuando llega el evento SSE de tipo "done" con messageId
Entonces el cursor parpadeante desaparece
Y aparecen los botones de rating (👍 / 👎) bajo la burbuja del asistente
Y el campo de texto se habilita para escribir la siguiente pregunta

AC-TUT-003.3 — El campo de texto se deshabilita durante el streaming

Dado que hay un stream de respuesta en curso
Cuando el estado Redux es "streaming"
Entonces el textarea aparece con opacidad reducida y no acepta nuevas entradas
Y el botón de enviar está deshabilitado

AC-TUT-003.4 — Aviso de respuesta lenta tras 8 segundos

Dado que el backend no ha enviado el primer chunk en 8 segundos
Cuando el timer de lentitud expira
Entonces aparece el mensaje "Esto está tardando un poco más de lo normal..."
Y el aviso desaparece cuando llega el primer chunk

AC-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 >= 400
Cuando 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ío

AC-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 abierto
Cuando hago clic en el botón de historial (☰)
Entonces aparece un drawer con la lista de conversaciones anteriores
Y cada conversación muestra el nombre del módulo, la fecha y el número de mensajes
Y el primer mensaje del usuario se muestra como preview truncado

AC-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 tutor
Cuando abre el drawer de historial
Entonces ve el texto "No tienes conversaciones anteriores"
Y no aparecen tarjetas de sesión

AC-TUT-004.3 — El historial se cierra al pulsar el botón de cierre

Dado que el drawer de historial está abierto
Cuando hago clic en el botón ✕ del drawer
Entonces el drawer desaparece y vuelvo a ver el chat activo

AC-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á abierto
Entonces veo una barra de progreso en la parte inferior del panel
Y 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á abierto
Entonces no se muestra la barra de uso mensual

AC-TUT-005.3 — La API reporta correctamente el uso acumulado

Dado que he enviado mensajes al tutor este mes
Cuando consulto GET /ai/tutor/monthly-usage
Entonces la respuesta incluye "used", "limit" y "plan"
Y "used" refleja el número de TutorMessages con role="user" desde el primer día del mes

AC-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-usage
Entonces el textarea y el botón de enviar desaparecen
Y aparece un bloque con el texto "Has alcanzado el límite mensual"
Y hay un enlace "Ver planes ↗" que apunta a /pricing

AC-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 mes
Cuando intenta enviar un cuarto mensaje vía POST /ai/tutor/stream
Entonces la API devuelve status 429
Y 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 mes
Cuando el usuario FREE consulta GET /ai/tutor/monthly-usage
Entonces "used" es 0
Y puede volver a enviar mensajes normalmente

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 recibido
Cuando visualizo la burbuja de respuesta
Entonces 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 inactivo
Cuando 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 correspondiente

AC-TUT-007.3 — El mensaje de bienvenida no tiene botones de rating

Dado que el panel acaba de abrirse y muestra el mensaje de bienvenida
Cuando visualizo la burbuja con id "welcome"
Entonces no hay botones de rating bajo ese mensaje

AC-TUT-007.4 — Un rating incorrecto (mensaje de otro usuario) devuelve ok: false

Dado que intento valorar un messageId que no pertenece al usuario autenticado
Cuando llamo POST /ai/tutor/rate con ese messageId
Entonces la API devuelve { ok: false } con status 200
Y el rating no se modifica en la BD

AC-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 IA
Entonces veo el panel con header pero sin área de chat
Y 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 /pricing

AC-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 renderiza
Entonces 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 tutor
Cuando hago clic en el botón de nueva conversación (↺)
Entonces todos los mensajes del hilo actual se eliminan del estado Redux
Y el panel muestra únicamente el mensaje de bienvenida
Y 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 cliente
Cuando consulto GET /ai/tutor/my-history
Entonces las conversaciones anteriores siguen presentes en la respuesta

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:

  1. Navegar a /learn/lesson/{lessonId}
  2. Hacer clic en el botón que abre el panel del tutor (data-testid="open-tutor-panel")
  3. 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:

  1. Abrir el panel del tutor en una lección
  2. Escribir una pregunta en el textarea
  3. Enviar con Enter o el botón de envío
  4. Observar la aparición del cursor parpadeante durante el stream
  5. 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 deshabilitado
const textarea = page.getByLabel('Mensaje al tutor')
await expect(textarea).toBeDisabled()
await expect(page.getByLabel('Enviar mensaje')).toBeDisabled()
// Tras finalizar el stream se vuelve a habilitar
await 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 stream
await 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 conversaciones
const 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/mes
await 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 rating
const [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.free
const 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ón
await page.getByLabel('Nueva conversación').click()
// Solo debe quedar el mensaje de bienvenida
const 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 anterior
const 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