Panel de IA
Módulos API: ai (monitoring, credentials, routes, alerts), ai/tasks
Páginas web: AdminAIDashboardPage, AdminAIHealthPage, AdminAICredentialsPage, AdminAIRoutesPage, AdminAIMetricsPage, AdminAICostsPage, AdminAIAlertsPage, AdminAIQdrantPage
Spec Playwright: apps/e2e/tests/admin/ai-panel.spec.ts — 📋 pendiente
Contexto arquitectónico
Section titled “Contexto arquitectónico”El Panel de IA (/admin/ai/*) es territorio exclusivo del rol SUPER_ADMIN. Permite observar y controlar el microservicio apps/ai-backend, que actúa como proxy seguro hacia los proveedores LLM. La capa apps/api persiste métricas en AIUsageMetric, tareas en AITask, alertas en AIAlert y configuración en AICredential, AIRouteConfig y AIAlertConfig.
Modelos de datos relevantes
Section titled “Modelos de datos relevantes”| Modelo Prisma | Uso |
|---|---|
AITask | Tarea de generación asíncrona (quiz, lección, juego, etc.) |
AIUsageMetric | Registro de cada llamada LLM: latencia, tokens, costo, caché |
AICredential | API keys cifradas con AES-256 por proveedor |
AIRouteConfig | Mapeo useCase → provider + model + fallback |
AIAlert | Incidencias automáticas (error rate, costo excedido, proveedor caído) |
AIAlertConfig | Umbrales de alerta (singleton) |
Tipos de tareas (AITaskType)
Section titled “Tipos de tareas (AITaskType)”| Valor enum | Descripción |
|---|---|
QUIZ_GENERATION | Generación de preguntas para un módulo |
LESSON_GENERATION | Generación de contenido markdown para una lección |
SUMMARY_GENERATION | Resumen de una lección (cuasi-síncrono) |
CASE_STUDY_GENERATION | Caso práctico estructurado |
WEB_ASSET_GENERATION | Recurso HTML interactivo para una lección |
ESCAPE_ROOM_GENERATION | Blueprint de escape room |
CROSSWORD_GENERATION | Crucigrama temático |
HANGMAN_GENERATION | Ahorcado con vocabulario del módulo |
TRIVIA_GENERATION | Ronda de trivia |
ADVENTURE_GENERATION | Historia interactiva de aventura |
FLASHCARDS_GENERATION | Tarjetas de memoria |
DRAG_AND_DROP_GENERATION | Actividad drag-and-drop |
MATCHING_PAIRS_GENERATION | Emparejamiento de conceptos |
WORD_PUZZLE_GENERATION | Sopa de letras |
SCENARIO_CHALLENGE_GENERATION | Reto de escenario profesional |
SPEED_QUIZ_GENERATION | Quiz contrarreloj |
MEMORY_MATCH_GENERATION | Memory match de conceptos |
TIMELINE_CHALLENGE_GENERATION | Ordenación cronológica |
OPEN_ANSWER_EVALUATION | Evaluación de respuestas abiertas |
FLUENCY_TEST_SCORING | Análisis del AI Fluency Test |
ADAPTIVE_ENGINE | Recomendación del motor adaptativo |
QDRANT_REINDEX_FULL | Reindexado completo del vector DB |
QDRANT_REINDEX_LESSON | Reindexado de una lección |
CHURN_ANALYSIS | Predicción de abandono |
TAG_SUGGESTION | Sugerencia de tags |
PATH_BRIEF_EXTRACTION | Extracción de brief de ruta |
Casos de uso enrutables (AI_USE_CASES)
Section titled “Casos de uso enrutables (AI_USE_CASES)”useCase | Descripción | Cache |
|---|---|---|
tutor_chat | Tutor conversacional (SSE) | No |
adaptive_engine | Motor adaptativo | Sí |
content_gen_quiz | Generación de quizzes | Sí |
content_gen_summary | Resúmenes de lección | Sí |
content_gen_case | Casos prácticos | Sí |
content_gen_optimize | Optimización de prompts | Sí |
open_answer_evaluation | Evaluación de respuestas abiertas | Sí |
fluency_test_scoring | AI Fluency Test | Sí |
churn_prediction | Predicción de abandono | Sí |
semantic_search | Búsqueda semántica (bloqueado — usa embeddings) | Sí |
content_gen_web_asset | Web Assets interactivos | Sí |
conversational_quiz | Quiz conversacional | No |
Estados de tarea (AITaskStatus)
Section titled “Estados de tarea (AITaskStatus)”PENDING → RUNNING → COMPLETED / FAILED / CANCELLED
Estados de revisión (AITaskReviewStatus)
Section titled “Estados de revisión (AITaskReviewStatus)”PENDING_REVIEW → IN_REVIEW → ACCEPTED / REJECTED / REFINED
Endpoints cubiertos
Section titled “Endpoints cubiertos”| Método | Ruta | Descripción |
|---|---|---|
GET | /admin/ai/health | Estado de los proveedores y vector DB |
GET | /admin/ai/metrics/summary | KPIs del día: llamadas, costo, latencia P95, errores, fallbacks |
GET | /admin/ai/metrics/costs?period= | Desglose de costos por proveedor/modelo/useCase |
GET | /admin/ai/metrics/daily-costs?period= | Costos diarios por proveedor |
GET | /admin/ai/metrics/errors?period= | Errores por proveedor/errorCode |
GET | /admin/ai/metrics | Registros de AIUsageMetric paginados con filtros |
GET | /admin/ai/credentials | Lista de credenciales (API keys enmascaradas) |
PUT | /admin/ai/credentials/:provider | Actualizar API key (se cifra en BD) |
POST | /admin/ai/credentials/:provider/test | Test de conectividad del proveedor |
GET | /admin/ai/routes | Configuración de enrutamiento por useCase |
PATCH | /admin/ai/routes/:useCase | Actualizar proveedor/modelo para un useCase |
POST | /admin/ai/routes/restore-defaults | Restaurar todos los defaults de enrutamiento |
GET | /admin/ai/alerts | Alertas activas (o todas con ?resolved=true) |
PATCH | /admin/ai/alerts/:id/resolve | Marcar alerta como resuelta |
GET | /admin/ai/alerts/config | Umbrales de alerta |
PATCH | /admin/ai/alerts/config | Actualizar umbrales |
GET | /admin/ai/vector-db/health | Salud de Qdrant (colecciones, P95, cache hit) |
POST | /admin/ai/vector-db/reindex | Disparar reindexado completo |
GET | /admin/ai/vector-db/reindex/history | Historial de jobs de reindexado |
GET | /admin/ai/vector-db/reindex/:jobId | Estado de un job de reindexado |
User Stories
Section titled “User Stories”US-ADM-AI-001: Dashboard global del sistema IA
Section titled “US-ADM-AI-001: Dashboard global del sistema IA”Como SUPER_ADMIN quiero ver un resumen ejecutivo del estado del AIBackend en tiempo real para detectar degradaciones sin revisar cada subsistema manualmente.
Módulo: ai-monitoring · Endpoint: GET /admin/ai/metrics/summary + GET /admin/ai/health
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-001.1 — KPIs del día visibles en el dashboard
Section titled “AC-ADM-AI-001.1 — KPIs del día visibles en el dashboard”Given que soy SUPER_ADMIN y navego a /admin/aiWhen la página carga y el AIBackend respondeThen veo 5 KPIs: llamadas hoy, costo hoy (USD), latencia P95 (ms), tasa de error (%) y fallbacks activosAnd los valores se actualizan con el último dato disponible del microservicioAC-ADM-AI-001.2 — Banner de desconexión cuando el AIBackend no responde
Section titled “AC-ADM-AI-001.2 — Banner de desconexión cuando el AIBackend no responde”Given que soy SUPER_ADMIN y navego a /admin/aiWhen el endpoint GET /admin/ai/health devuelve error (5xx o timeout)Then veo un banner de advertencia: "Sin conexión con AI Backend"And los KPIs muestran "—" en lugar de valoresAnd puedo navegar a las acciones rápidas para diagnosticarAC-ADM-AI-001.3 — Gráfico de llamadas por proveedor
Section titled “AC-ADM-AI-001.3 — Gráfico de llamadas por proveedor”Given que soy SUPER_ADMIN y hay métricas del día disponiblesWhen cargo el dashboard /admin/aiThen veo un gráfico de barras horizontales con las llamadas por proveedor (anthropic, openai, gemini)And las barras están ordenadas de mayor a menorAnd cada barra muestra el nombre del proveedor y el número de llamadasUS-ADM-AI-002: Verificar el estado operativo de cada proveedor IA
Section titled “US-ADM-AI-002: Verificar el estado operativo de cada proveedor IA”Como SUPER_ADMIN quiero ver el estado (operativo / degradado / error) de cada proveedor LLM y del vector DB para actuar ante incidencias antes de que afecten a los usuarios.
Módulo: ai-monitoring · Endpoint: GET /admin/ai/health
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-002.1 — Estado de proveedores LLM en página de health
Section titled “AC-ADM-AI-002.1 — Estado de proveedores LLM en página de health”Given que soy SUPER_ADMIN y navego a /admin/ai/healthWhen la página cargaThen veo una tarjeta por cada proveedor (Anthropic, OpenAI, Gemini)And cada tarjeta muestra el estado: Operativo (verde), Degradado (naranja) o Error (rojo)And se muestran métricas adicionales: latencia promedio, tasa de error, últimos modelos activosAnd la página hace polling automático cada 30 segundosAC-ADM-AI-002.2 — Estado de Qdrant (vector DB) en página de health
Section titled “AC-ADM-AI-002.2 — Estado de Qdrant (vector DB) en página de health”Given que soy SUPER_ADMIN y navego a /admin/ai/healthWhen la página carga y Qdrant respondeThen veo el estado del vector DB con el número de colecciones activasAnd veo las colecciones individuales con vectorCount, dimensión y último accesoAnd si Qdrant tiene status "error", el chip muestra color rojoUS-ADM-AI-003: Gestionar las credenciales de los proveedores IA
Section titled “US-ADM-AI-003: Gestionar las credenciales de los proveedores IA”Como SUPER_ADMIN quiero ver, actualizar y probar las API keys de cada proveedor LLM para asegurar que el sistema puede contactar los servicios externos sin interrupciones.
Módulo: ai-credentials · Endpoints: GET /admin/ai/credentials, PUT /admin/ai/credentials/:provider, POST /admin/ai/credentials/:provider/test
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-003.1 — API keys siempre enmascaradas en la UI
Section titled “AC-ADM-AI-003.1 — API keys siempre enmascaradas en la UI”Given que soy SUPER_ADMIN y navego a /admin/ai/credentialsWhen la página carga y lista las credencialesThen veo el preview enmascarado (ej. "sk-ant-****abc123") de cada credencialAnd el valor real nunca aparece en la respuesta de la API ni en el DOMAnd existe un botón para revelar/ocultar el preview (no el valor real)AC-ADM-AI-003.2 — Actualizar una API key y que el AIBackend invalide su caché
Section titled “AC-ADM-AI-003.2 — Actualizar una API key y que el AIBackend invalide su caché”Given que soy SUPER_ADMIN en /admin/ai/credentialsWhen introduzco una nueva API key en el campo del proveedor "anthropic" y pulso GuardarThen la API cifra el valor con AES-256 y lo persiste en AICredentialAnd se llama automáticamente a /credentials/anthropic/invalidate-cache en el AIBackendAnd el AIBackend leerá la nueva key en la siguiente llamada sin necesidad de reiniciarAnd veo confirmación visual de éxitoAC-ADM-AI-003.3 — Test de conectividad de una credencial
Section titled “AC-ADM-AI-003.3 — Test de conectividad de una credencial”Given que soy SUPER_ADMIN en /admin/ai/credentialsWhen pulso el botón "Test" junto a una credencialThen se llama a POST /admin/ai/credentials/:provider/testAnd veo el resultado: status ("ok" o "error"), latencia en ms y mensaje de error si aplicaAnd el campo lastTestedAt y lastTestStatus se actualizan en BDUS-ADM-AI-004: Configurar el enrutamiento IA por caso de uso
Section titled “US-ADM-AI-004: Configurar el enrutamiento IA por caso de uso”Como SUPER_ADMIN quiero cambiar qué proveedor y modelo atiende cada caso de uso (tutor, quizzes, evaluación, etc.) para optimizar costos, latencia y calidad sin modificar código.
Módulo: ai-routes · Endpoints: GET /admin/ai/routes, PATCH /admin/ai/routes/:useCase, POST /admin/ai/routes/restore-defaults
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-004.1 — Ver todos los casos de uso con su configuración actual
Section titled “AC-ADM-AI-004.1 — Ver todos los casos de uso con su configuración actual”Given que soy SUPER_ADMIN y navego a /admin/ai/routesWhen la página cargaThen veo una fila por cada uno de los 12 casos de uso configurablesAnd cada fila muestra: useCase, proveedor principal, modelo, proveedor fallback, temperatura, caché TTL y si está activoAnd los casos de uso marcados como "locked" (ej. semantic_search) muestran un indicador de bloqueadoAC-ADM-AI-004.2 — Cambiar proveedor/modelo de un caso de uso
Section titled “AC-ADM-AI-004.2 — Cambiar proveedor/modelo de un caso de uso”Given que soy SUPER_ADMIN en /admin/ai/routesWhen modifico el proveedor de "content_gen_quiz" de "anthropic" a "openai" y guardoThen se llama a PATCH /admin/ai/routes/content_gen_quiz con el nuevo dtoAnd el AIBackend invalida su caché de rutas (POST /admin/router/reload)And en la próxima generación de quiz, el tráfico usa el modelo OpenAI configuradoAC-ADM-AI-004.3 — Restaurar defaults de enrutamiento
Section titled “AC-ADM-AI-004.3 — Restaurar defaults de enrutamiento”Given que soy SUPER_ADMIN en /admin/ai/routesWhen pulso "Restaurar defaults" y confirmoThen se llama a POST /admin/ai/routes/restore-defaultsAnd todas las rutas vuelven a su configuración originalAnd el AIBackend recibe la señal de recarga de cachéAnd veo la tabla actualizada con los valores por defectoUS-ADM-AI-005: Monitorizar métricas de uso y costos de IA
Section titled “US-ADM-AI-005: Monitorizar métricas de uso y costos de IA”Como SUPER_ADMIN quiero ver el desglose de llamadas LLM, tokens consumidos y costo estimado por período, proveedor, modelo y caso de uso para controlar el gasto y detectar anomalías.
Módulo: ai-monitoring · Endpoints: GET /admin/ai/metrics/*
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-005.1 — Desglose de costos por proveedor/modelo/useCase
Section titled “AC-ADM-AI-005.1 — Desglose de costos por proveedor/modelo/useCase”Given que soy SUPER_ADMIN y navego a /admin/ai/costs (o /admin/ai/metrics)When selecciono el período "30d"Then veo una tabla con filas agrupadas por proveedor, modelo y useCaseAnd cada fila muestra: llamadas, tokens entrada, tokens salida, costo estimado (USD) y usuarios únicosAnd la tabla está ordenada de mayor a menor costoAnd veo el total de usuarios únicos que han generado actividad IA en el períodoAC-ADM-AI-005.2 — Filtros de período aplicables a métricas
Section titled “AC-ADM-AI-005.2 — Filtros de período aplicables a métricas”Given que soy SUPER_ADMIN en la página de métricasWhen cambio el período entre "hoy", "7d", "30d", "90d" o "1y"Then la tabla se recarga con datos del período seleccionadoAnd los KPIs de resumen también reflejan el mismo períodoAC-ADM-AI-005.3 — Registro individual de llamadas paginado
Section titled “AC-ADM-AI-005.3 — Registro individual de llamadas paginado”Given que soy SUPER_ADMIN en la vista de métricas detalladasWhen cargo la lista de registros individuales de AIUsageMetricThen veo filas paginadas (50 por página) con: useCase, proveedor, modelo, latencia, tokensIn, tokensOut, costo, caché hit, fallback y errorCodeAnd puedo filtrar por proveedor, useCase y períodoAnd los registros con fallback=true o errorCode están visualmente destacadosUS-ADM-AI-006: Gestionar alertas de incidencias del sistema IA
Section titled “US-ADM-AI-006: Gestionar alertas de incidencias del sistema IA”Como SUPER_ADMIN quiero recibir y gestionar alertas automáticas cuando el sistema IA detecta anomalías (proveedor caído, costo excedido, error rate alto) para reaccionar a tiempo y mantener el SLA.
Módulo: ai-alerts · Endpoints: GET /admin/ai/alerts, PATCH /admin/ai/alerts/:id/resolve, GET|PATCH /admin/ai/alerts/config
Estado: ✅ Implementado
Prioridad: Alta
AC-ADM-AI-006.1 — Ver alertas activas con severidad diferenciada
Section titled “AC-ADM-AI-006.1 — Ver alertas activas con severidad diferenciada”Given que soy SUPER_ADMIN y navego a /admin/ai/alertsWhen existen alertas activas (isResolved=false)Then veo cada alerta como una tarjeta con color según severidad: rojo (critical), naranja (warning), azul (info)And cada tarjeta muestra: tipo de alerta, mensaje, timestamp de creación y botón "Resolver"And si no hay alertas activas, veo un estado vacío en verde indicando que todo está bienAC-ADM-AI-006.2 — Tipos de alerta reconocidos por el sistema
Section titled “AC-ADM-AI-006.2 — Tipos de alerta reconocidos por el sistema”Given que el AIBackend detecta una anomalíaWhen crea una alerta vía POST /internal/ai-alerts (ruta interna)Then el tipo debe ser uno de: PROVIDER_DOWN, HIGH_ERROR_RATE, COST_THRESHOLD_EXCEEDED, LATENCY_DEGRADED, QDRANT_UNAVAILABLE, CREDENTIAL_EXPIRING, FALLBACK_ACTIVATED, COLLECTIVE_CONFUSIONAnd la alerta queda persistida en la tabla AIAlert con isResolved=falseAnd el SUPER_ADMIN la ve en la siguiente carga de /admin/ai/alertsAC-ADM-AI-006.3 — Resolver una alerta
Section titled “AC-ADM-AI-006.3 — Resolver una alerta”Given que soy SUPER_ADMIN y veo una alerta activa tipo PROVIDER_DOWNWhen pulso el botón "Resolver" en esa alertaThen se llama a PATCH /admin/ai/alerts/:id/resolveAnd la alerta se marca con isResolved=true, resolvedBy (mi userId) y resolvedAt (timestamp)And la tarjeta desaparece de la lista de alertas activasAnd si cambio el filtro a "todas", veo la alerta con estado resueltoAC-ADM-AI-006.4 — Configurar umbrales de alerta
Section titled “AC-ADM-AI-006.4 — Configurar umbrales de alerta”Given que soy SUPER_ADMIN en /admin/ai/alertsWhen configuro dailyThresholdUsd=50 y monthlyThresholdUsd=500 y guardoThen se llama a PATCH /admin/ai/alerts/config con los nuevos valoresAnd si el costo del día supera 50 USD, el sistema genera automáticamente una alerta COST_THRESHOLD_EXCEEDEDAnd puedo configurar también notificationEmail y webhookUrl para notificaciones externasUS-ADM-AI-007: Gestionar el vector DB Qdrant
Section titled “US-ADM-AI-007: Gestionar el vector DB Qdrant”Como SUPER_ADMIN quiero ver el estado de Qdrant, sus colecciones y disparar un reindexado completo para garantizar que el tutor IA y la búsqueda semántica funcionan con información actualizada.
Módulo: ai-monitoring (proxy a AIBackend) · Endpoints: GET /admin/ai/vector-db/health, POST /admin/ai/vector-db/reindex, GET /admin/ai/vector-db/reindex/history
Estado: ✅ Implementado
Prioridad: Media
AC-ADM-AI-007.1 — Ver el estado de las colecciones de Qdrant
Section titled “AC-ADM-AI-007.1 — Ver el estado de las colecciones de Qdrant”Given que soy SUPER_ADMIN y navego a /admin/ai/vector-dbWhen Qdrant responde al health checkThen veo el estado global (ok/error), la latencia de búsqueda P95 y la tasa de cache hit de embeddingsAnd veo una tabla con cada colección: nombre, número de vectores, dimensión, último acceso y estadoAC-ADM-AI-007.2 — Disparar reindexado completo
Section titled “AC-ADM-AI-007.2 — Disparar reindexado completo”Given que soy SUPER_ADMIN en /admin/ai/vector-dbWhen pulso "Reindexar todo" y confirmoThen se llama a POST /admin/ai/vector-db/reindexAnd el AIBackend crea un job que procesa todas las lecciones publicadasAnd veo el jobId retornado y puedo seguir el progreso: totalLessons, processedLessons, totalChunks, errorsAnd el historial de reindexado muestra los últimos jobs con status queued/running/completed/failedUS-ADM-AI-008: Observar el flujo de tareas IA (AITask) generadas por los creadores
Section titled “US-ADM-AI-008: Observar el flujo de tareas IA (AITask) generadas por los creadores”Como SUPER_ADMIN quiero tener visibilidad global sobre las tareas de generación IA iniciadas por los content admins para identificar fallos recurrentes, tareas colgadas y patrones de uso inesperados.
Módulo: ai/tasks · Endpoints: GET /ai/tasks (por usuario), no existe un listado admin global actualmente
Estado: 🚧 Panel admin de tareas no implementado (tareas accesibles solo por el usuario propietario)
Prioridad: Media
AC-ADM-AI-008.1 — Flujo completo de una tarea de generación de quiz
Section titled “AC-ADM-AI-008.1 — Flujo completo de una tarea de generación de quiz”Given que un CONTENT_ADMIN crea una tarea de tipo QUIZ_GENERATION para el módulo XWhen se llama a POST /ai/tasks con type="QUIZ_GENERATION"Then la tarea se crea con status=PENDING y reviewStatus=PENDING_REVIEWAnd el procesamiento se dispara en background via setImmediate (sin BullMQ para generaciones síncronas)And el CONTENT_ADMIN puede consultar el progreso mediante GET /ai/tasks/:taskId/resultAnd al completarse, status pasa a COMPLETED y resultPayload contiene las preguntas generadasAnd el costo y tokens consumidos quedan registrados en los campos costUsd y tokensUsedAC-ADM-AI-008.2 — Cancelar una tarea en progreso
Section titled “AC-ADM-AI-008.2 — Cancelar una tarea en progreso”Given que existe una tarea con status=PENDING o status=RUNNINGWhen el propietario llama a PATCH /ai/tasks/:taskId/cancelThen el status cambia a CANCELLEDAnd si la tarea ya está COMPLETED o FAILED, la cancelación retorna error 400AC-ADM-AI-008.3 — Ciclo de refinamiento de una tarea
Section titled “AC-ADM-AI-008.3 — Ciclo de refinamiento de una tarea”Given que una tarea COMPLETED tiene reviewStatus=PENDING_REVIEWWhen el creador inicia una tarea hija (parentTaskId apunta a la tarea original)Then la tarea padre pasa a reviewStatus=REFINEDAnd la tarea hija hereda refinementCount = parentRefinementCount + 1And si el refinementCount llega a 5, la API retorna 400 "Límite de refinamientos alcanzado"Test Cases
Section titled “Test Cases”TC-ADM-AI-001 — Dashboard muestra KPIs correctamente
Section titled “TC-ADM-AI-001 — Dashboard muestra KPIs correctamente”Cubre: AC-ADM-AI-001.1 Tipo: E2E
test('TC-ADM-AI-001: dashboard AI muestra los 5 KPIs del día', async ({ page }) => { await page.goto('/admin/ai')
// Los 5 KPIs deben estar visibles await expect(page.getByText('Llamadas hoy')).toBeVisible() await expect(page.getByText('Coste hoy (USD)')).toBeVisible() await expect(page.getByText('Latencia P95')).toBeVisible() await expect(page.getByText('Tasa de error')).toBeVisible() await expect(page.getByText('Fallbacks activos')).toBeVisible()
// Los valores numéricos deben haberse cargado (no "—") await expect(page.locator('.aid-kpi-val').first()).not.toHaveText('—')})TC-ADM-AI-002 — Banner de error cuando AIBackend no responde
Section titled “TC-ADM-AI-002 — Banner de error cuando AIBackend no responde”Cubre: AC-ADM-AI-001.2 Tipo: Integration (mock)
test('TC-ADM-AI-002: banner de desconexión cuando AIBackend retorna 503', async ({ page, request }) => { // Mockear el endpoint de health para que falle await page.route('**/admin/ai/health', route => route.fulfill({ status: 503, body: JSON.stringify({ message: 'Service Unavailable' }) }), )
await page.goto('/admin/ai')
await expect(page.getByText('Sin conexión con AI Backend')).toBeVisible() // Los KPIs deben mostrar "—" al no haber datos de health const kpiValues = page.locator('.aid-kpi-val.loading') await expect(kpiValues.first()).toBeVisible()})TC-ADM-AI-003 — Solo SUPER_ADMIN accede al panel de IA
Section titled “TC-ADM-AI-003 — Solo SUPER_ADMIN accede al panel de IA”Cubre: Guard de acceso del módulo ai-monitoring
Tipo: E2E (access control)
test('TC-ADM-AI-003: usuario con rol PRO recibe 403 al intentar acceder a /admin/ai/health', async ({ request }) => { // Autenticar como usuario PRO (no SUPER_ADMIN) const proToken = await getTokenForRole(request, 'PRO')
const res = await request.get('/admin/ai/health', { headers: { Authorization: `Bearer ${proToken}` }, })
expect(res.status()).toBe(403)})TC-ADM-AI-004 — Credencial se guarda cifrada y nunca expone el valor real
Section titled “TC-ADM-AI-004 — Credencial se guarda cifrada y nunca expone el valor real”Cubre: AC-ADM-AI-003.1, AC-ADM-AI-003.2 Tipo: Integration
test('TC-ADM-AI-004: actualizar credencial retorna preview enmascarado, no el valor original', async ({ request }) => { const superAdminToken = await getTokenForRole(request, 'SUPER_ADMIN')
const updateRes = await request.put('/admin/ai/credentials/anthropic', { headers: { Authorization: `Bearer ${superAdminToken}` }, data: { value: 'sk-ant-api03-test-key-1234567890abcdef' }, })
expect(updateRes.status()).toBe(200) const body = await updateRes.json()
// El campo encryptedValue nunca debe aparecer en la respuesta expect(body).not.toHaveProperty('encryptedValue') // El preview debe estar enmascarado (contiene asteriscos) expect(body.maskedPreview).toContain('****') // El valor original no debe aparecer expect(JSON.stringify(body)).not.toContain('sk-ant-api03-test-key-1234567890abcdef')})TC-ADM-AI-005 — Test de credencial reporta latencia y status
Section titled “TC-ADM-AI-005 — Test de credencial reporta latencia y status”Cubre: AC-ADM-AI-003.3 Tipo: Integration
test('TC-ADM-AI-005: test de credencial devuelve status y latencyMs', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
const res = await request.post('/admin/ai/credentials/openai/test', { headers: { Authorization: `Bearer ${token}` }, })
expect(res.status()).toBe(201) const body = await res.json() expect(body).toHaveProperty('status') expect(['ok', 'error']).toContain(body.status) expect(body).toHaveProperty('latencyMs') expect(typeof body.latencyMs).toBe('number')})TC-ADM-AI-006 — Actualizar ruta IA propaga al AIBackend
Section titled “TC-ADM-AI-006 — Actualizar ruta IA propaga al AIBackend”Cubre: AC-ADM-AI-004.2 Tipo: Integration
test('TC-ADM-AI-006: PATCH /admin/ai/routes/:useCase actualiza y llama reload en AIBackend', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
const res = await request.patch('/admin/ai/routes/content_gen_quiz', { headers: { Authorization: `Bearer ${token}` }, data: { provider: 'openai', model: 'gpt-4o-mini', fallbackProvider: 'anthropic', fallbackModel: 'claude-haiku-4-5', temperature: 0.5, }, })
expect(res.status()).toBe(200) const body = await res.json() expect(body.useCase).toBe('content_gen_quiz') expect(body.provider).toBe('openai') expect(body.model).toBe('gpt-4o-mini')})TC-ADM-AI-007 — Crear tarea QUIZ_GENERATION y verificar estados
Section titled “TC-ADM-AI-007 — Crear tarea QUIZ_GENERATION y verificar estados”Cubre: AC-ADM-AI-008.1 Tipo: Integration
test('TC-ADM-AI-007: tarea de quiz pasa por PENDING → COMPLETED con costUsd registrado', async ({ request }) => { const token = await getTokenForRole(request, 'CONTENT_ADMIN')
// Crear tarea const createRes = await request.post('/ai/tasks', { headers: { Authorization: `Bearer ${token}` }, data: { type: 'QUIZ_GENERATION', label: 'Quiz módulo test', contextId: 'test-module-id', contextType: 'module', launchPayload: { moduleId: 'test-module-id', pathId: 'test-path-id' }, }, })
expect(createRes.status()).toBe(201) const task = await createRes.json() expect(task.status).toBe('PENDING') expect(task.reviewStatus).toBe('PENDING_REVIEW') expect(task.type).toBe('QUIZ_GENERATION')
// Esperar procesamiento y verificar resultado await expect.poll(async () => { const res = await request.get(`/ai/tasks/${task.id}/result`, { headers: { Authorization: `Bearer ${token}` }, }) const data = await res.json() return data.status }, { timeout: 30000, intervals: [2000] }).toBe('COMPLETED')
const resultRes = await request.get(`/ai/tasks/${task.id}/result`, { headers: { Authorization: `Bearer ${token}` }, }) const completed = await resultRes.json() expect(completed.costUsd).toBeGreaterThanOrEqual(0) expect(completed.tokensUsed).toBeGreaterThan(0) expect(completed.provider).toBeTruthy()})TC-ADM-AI-008 — Cancelar tarea en estado PENDING
Section titled “TC-ADM-AI-008 — Cancelar tarea en estado PENDING”Cubre: AC-ADM-AI-008.2 Tipo: Integration
test('TC-ADM-AI-008: cancelar tarea PENDING la lleva a estado CANCELLED', async ({ request }) => { const token = await getTokenForRole(request, 'CONTENT_ADMIN')
const createRes = await request.post('/ai/tasks', { headers: { Authorization: `Bearer ${token}` }, data: { type: 'QUIZ_GENERATION', label: 'Tarea a cancelar', contextId: 'x', contextType: 'module' }, }) const task = await createRes.json()
const cancelRes = await request.patch(`/ai/tasks/${task.id}/cancel`, { headers: { Authorization: `Bearer ${token}` }, })
expect(cancelRes.status()).toBe(200) const cancelled = await cancelRes.json() expect(cancelled.status).toBe('CANCELLED')})TC-ADM-AI-009 — Alerta se crea y puede resolverse
Section titled “TC-ADM-AI-009 — Alerta se crea y puede resolverse”Cubre: AC-ADM-AI-006.1, AC-ADM-AI-006.3 Tipo: Integration
test('TC-ADM-AI-009: crear alerta y resolverla actualiza isResolved', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
// Listar alertas activas iniciales const listRes = await request.get('/admin/ai/alerts', { headers: { Authorization: `Bearer ${token}` }, }) expect(listRes.status()).toBe(200) const alerts = await listRes.json()
// Si hay alertas activas, resolver la primera if (alerts.length > 0) { const alertId = alerts[0].id const resolveRes = await request.patch(`/admin/ai/alerts/${alertId}/resolve`, { headers: { Authorization: `Bearer ${token}` }, }) expect(resolveRes.status()).toBe(200) const resolved = await resolveRes.json() expect(resolved.isResolved).toBe(true) expect(resolved.resolvedBy).toBeTruthy() expect(resolved.resolvedAt).toBeTruthy() }})TC-ADM-AI-010 — Configurar umbral de alerta de costo
Section titled “TC-ADM-AI-010 — Configurar umbral de alerta de costo”Cubre: AC-ADM-AI-006.4 Tipo: Integration
test('TC-ADM-AI-010: PATCH /admin/ai/alerts/config persiste los umbrales', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
const res = await request.patch('/admin/ai/alerts/config', { headers: { Authorization: `Bearer ${token}` }, data: { dailyThresholdUsd: 25, monthlyThresholdUsd: 400, notificationEmail: 'admin@nappai.ai', }, })
expect(res.status()).toBe(200) const config = await res.json() expect(config.dailyThresholdUsd).toBe(25) expect(config.monthlyThresholdUsd).toBe(400) expect(config.notificationEmail).toBe('admin@nappai.ai')
// Verificar que GET devuelve los mismos valores const getRes = await request.get('/admin/ai/alerts/config', { headers: { Authorization: `Bearer ${token}` }, }) const loaded = await getRes.json() expect(loaded.dailyThresholdUsd).toBe(25)})TC-ADM-AI-011 — Reindexado Qdrant devuelve jobId y progreso
Section titled “TC-ADM-AI-011 — Reindexado Qdrant devuelve jobId y progreso”Cubre: AC-ADM-AI-007.2 Tipo: Integration
test('TC-ADM-AI-011: disparar reindexado retorna jobId y el historial lo incluye', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
const reindexRes = await request.post('/admin/ai/vector-db/reindex', { headers: { Authorization: `Bearer ${token}` }, })
expect(reindexRes.status()).toBe(201) const body = await reindexRes.json() expect(body).toHaveProperty('jobId') expect(typeof body.jobId).toBe('string')
// El historial debe incluir el job recién creado const historyRes = await request.get('/admin/ai/vector-db/reindex/history', { headers: { Authorization: `Bearer ${token}` }, }) const history = await historyRes.json() expect(Array.isArray(history)).toBe(true) const found = history.find((j: { jobId: string }) => j.jobId === body.jobId) expect(found).toBeTruthy()})TC-ADM-AI-012 — Métricas de costos devuelven estructura correcta
Section titled “TC-ADM-AI-012 — Métricas de costos devuelven estructura correcta”Cubre: AC-ADM-AI-005.1 Tipo: Integration
test('TC-ADM-AI-012: GET /admin/ai/metrics/costs devuelve rows y totalUniqueUsers', async ({ request }) => { const token = await getTokenForRole(request, 'SUPER_ADMIN')
const res = await request.get('/admin/ai/metrics/costs?period=30d', { headers: { Authorization: `Bearer ${token}` }, })
expect(res.status()).toBe(200) const body = await res.json() expect(body).toHaveProperty('rows') expect(Array.isArray(body.rows)).toBe(true) expect(body).toHaveProperty('totalUniqueUsers')
if (body.rows.length > 0) { const row = body.rows[0] expect(row).toHaveProperty('provider') expect(row).toHaveProperty('model') expect(row).toHaveProperty('useCase') expect(row).toHaveProperty('calls') expect(row).toHaveProperty('estimatedCostUsd') expect(row).toHaveProperty('uniqueUsers') }})TC-ADM-AI-013 — Límite de refinamientos a 5 iteraciones
Section titled “TC-ADM-AI-013 — Límite de refinamientos a 5 iteraciones”Cubre: AC-ADM-AI-008.3 Tipo: Integration
test('TC-ADM-AI-013: crear tarea hija con parentTaskId de refinementCount=4 retorna 400', async ({ request }) => { const token = await getTokenForRole(request, 'CONTENT_ADMIN')
// Crear tarea padre simulada con refinementCount=4 // (requiere setup previo con 4 refinamientos encadenados o manipulación directa de BD en test env) const parentId = await createTaskWithRefinementCount(request, token, 4)
const res = await request.post('/ai/tasks', { headers: { Authorization: `Bearer ${token}` }, data: { type: 'QUIZ_GENERATION', label: 'Refinamiento excedido', contextId: 'x', contextType: 'module', parentTaskId: parentId, }, })
expect(res.status()).toBe(400) const body = await res.json() expect(body.message).toContain('refinamientos')})