Skip to content

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


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.

Modelo PrismaUso
AITaskTarea de generación asíncrona (quiz, lección, juego, etc.)
AIUsageMetricRegistro de cada llamada LLM: latencia, tokens, costo, caché
AICredentialAPI keys cifradas con AES-256 por proveedor
AIRouteConfigMapeo useCase → provider + model + fallback
AIAlertIncidencias automáticas (error rate, costo excedido, proveedor caído)
AIAlertConfigUmbrales de alerta (singleton)
Valor enumDescripción
QUIZ_GENERATIONGeneración de preguntas para un módulo
LESSON_GENERATIONGeneración de contenido markdown para una lección
SUMMARY_GENERATIONResumen de una lección (cuasi-síncrono)
CASE_STUDY_GENERATIONCaso práctico estructurado
WEB_ASSET_GENERATIONRecurso HTML interactivo para una lección
ESCAPE_ROOM_GENERATIONBlueprint de escape room
CROSSWORD_GENERATIONCrucigrama temático
HANGMAN_GENERATIONAhorcado con vocabulario del módulo
TRIVIA_GENERATIONRonda de trivia
ADVENTURE_GENERATIONHistoria interactiva de aventura
FLASHCARDS_GENERATIONTarjetas de memoria
DRAG_AND_DROP_GENERATIONActividad drag-and-drop
MATCHING_PAIRS_GENERATIONEmparejamiento de conceptos
WORD_PUZZLE_GENERATIONSopa de letras
SCENARIO_CHALLENGE_GENERATIONReto de escenario profesional
SPEED_QUIZ_GENERATIONQuiz contrarreloj
MEMORY_MATCH_GENERATIONMemory match de conceptos
TIMELINE_CHALLENGE_GENERATIONOrdenación cronológica
OPEN_ANSWER_EVALUATIONEvaluación de respuestas abiertas
FLUENCY_TEST_SCORINGAnálisis del AI Fluency Test
ADAPTIVE_ENGINERecomendación del motor adaptativo
QDRANT_REINDEX_FULLReindexado completo del vector DB
QDRANT_REINDEX_LESSONReindexado de una lección
CHURN_ANALYSISPredicción de abandono
TAG_SUGGESTIONSugerencia de tags
PATH_BRIEF_EXTRACTIONExtracción de brief de ruta
useCaseDescripciónCache
tutor_chatTutor conversacional (SSE)No
adaptive_engineMotor adaptativo
content_gen_quizGeneración de quizzes
content_gen_summaryResúmenes de lección
content_gen_caseCasos prácticos
content_gen_optimizeOptimización de prompts
open_answer_evaluationEvaluación de respuestas abiertas
fluency_test_scoringAI Fluency Test
churn_predictionPredicción de abandono
semantic_searchBúsqueda semántica (bloqueado — usa embeddings)
content_gen_web_assetWeb Assets interactivos
conversational_quizQuiz conversacionalNo

PENDINGRUNNINGCOMPLETED / FAILED / CANCELLED

PENDING_REVIEWIN_REVIEWACCEPTED / REJECTED / REFINED


MétodoRutaDescripción
GET/admin/ai/healthEstado de los proveedores y vector DB
GET/admin/ai/metrics/summaryKPIs 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/metricsRegistros de AIUsageMetric paginados con filtros
GET/admin/ai/credentialsLista de credenciales (API keys enmascaradas)
PUT/admin/ai/credentials/:providerActualizar API key (se cifra en BD)
POST/admin/ai/credentials/:provider/testTest de conectividad del proveedor
GET/admin/ai/routesConfiguración de enrutamiento por useCase
PATCH/admin/ai/routes/:useCaseActualizar proveedor/modelo para un useCase
POST/admin/ai/routes/restore-defaultsRestaurar todos los defaults de enrutamiento
GET/admin/ai/alertsAlertas activas (o todas con ?resolved=true)
PATCH/admin/ai/alerts/:id/resolveMarcar alerta como resuelta
GET/admin/ai/alerts/configUmbrales de alerta
PATCH/admin/ai/alerts/configActualizar umbrales
GET/admin/ai/vector-db/healthSalud de Qdrant (colecciones, P95, cache hit)
POST/admin/ai/vector-db/reindexDisparar reindexado completo
GET/admin/ai/vector-db/reindex/historyHistorial de jobs de reindexado
GET/admin/ai/vector-db/reindex/:jobIdEstado de un job de reindexado

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/ai
When la página carga y el AIBackend responde
Then veo 5 KPIs: llamadas hoy, costo hoy (USD), latencia P95 (ms), tasa de error (%) y fallbacks activos
And los valores se actualizan con el último dato disponible del microservicio

AC-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/ai
When 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 valores
And puedo navegar a las acciones rápidas para diagnosticar

AC-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 disponibles
When cargo el dashboard /admin/ai
Then veo un gráfico de barras horizontales con las llamadas por proveedor (anthropic, openai, gemini)
And las barras están ordenadas de mayor a menor
And cada barra muestra el nombre del proveedor y el número de llamadas

US-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/health
When la página carga
Then 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 activos
And la página hace polling automático cada 30 segundos

AC-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/health
When la página carga y Qdrant responde
Then veo el estado del vector DB con el número de colecciones activas
And veo las colecciones individuales con vectorCount, dimensión y último acceso
And si Qdrant tiene status "error", el chip muestra color rojo

US-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/credentials
When la página carga y lista las credenciales
Then veo el preview enmascarado (ej. "sk-ant-****abc123") de cada credencial
And el valor real nunca aparece en la respuesta de la API ni en el DOM
And 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/credentials
When introduzco una nueva API key en el campo del proveedor "anthropic" y pulso Guardar
Then la API cifra el valor con AES-256 y lo persiste en AICredential
And se llama automáticamente a /credentials/anthropic/invalidate-cache en el AIBackend
And el AIBackend leerá la nueva key en la siguiente llamada sin necesidad de reiniciar
And veo confirmación visual de éxito

AC-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/credentials
When pulso el botón "Test" junto a una credencial
Then se llama a POST /admin/ai/credentials/:provider/test
And veo el resultado: status ("ok" o "error"), latencia en ms y mensaje de error si aplica
And el campo lastTestedAt y lastTestStatus se actualizan en BD

US-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/routes
When la página carga
Then veo una fila por cada uno de los 12 casos de uso configurables
And cada fila muestra: useCase, proveedor principal, modelo, proveedor fallback, temperatura, caché TTL y si está activo
And los casos de uso marcados como "locked" (ej. semantic_search) muestran un indicador de bloqueado

AC-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/routes
When modifico el proveedor de "content_gen_quiz" de "anthropic" a "openai" y guardo
Then se llama a PATCH /admin/ai/routes/content_gen_quiz con el nuevo dto
And 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 configurado

AC-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/routes
When pulso "Restaurar defaults" y confirmo
Then se llama a POST /admin/ai/routes/restore-defaults
And todas las rutas vuelven a su configuración original
And el AIBackend recibe la señal de recarga de caché
And veo la tabla actualizada con los valores por defecto

US-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 useCase
And cada fila muestra: llamadas, tokens entrada, tokens salida, costo estimado (USD) y usuarios únicos
And la tabla está ordenada de mayor a menor costo
And veo el total de usuarios únicos que han generado actividad IA en el período

AC-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étricas
When cambio el período entre "hoy", "7d", "30d", "90d" o "1y"
Then la tabla se recarga con datos del período seleccionado
And los KPIs de resumen también reflejan el mismo período

AC-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 detalladas
When cargo la lista de registros individuales de AIUsageMetric
Then veo filas paginadas (50 por página) con: useCase, proveedor, modelo, latencia, tokensIn, tokensOut, costo, caché hit, fallback y errorCode
And puedo filtrar por proveedor, useCase y período
And los registros con fallback=true o errorCode están visualmente destacados

US-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/alerts
When 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á bien

AC-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ía
When 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_CONFUSION
And la alerta queda persistida en la tabla AIAlert con isResolved=false
And el SUPER_ADMIN la ve en la siguiente carga de /admin/ai/alerts
Given que soy SUPER_ADMIN y veo una alerta activa tipo PROVIDER_DOWN
When pulso el botón "Resolver" en esa alerta
Then se llama a PATCH /admin/ai/alerts/:id/resolve
And la alerta se marca con isResolved=true, resolvedBy (mi userId) y resolvedAt (timestamp)
And la tarjeta desaparece de la lista de alertas activas
And si cambio el filtro a "todas", veo la alerta con estado resuelto

AC-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/alerts
When configuro dailyThresholdUsd=50 y monthlyThresholdUsd=500 y guardo
Then se llama a PATCH /admin/ai/alerts/config con los nuevos valores
And si el costo del día supera 50 USD, el sistema genera automáticamente una alerta COST_THRESHOLD_EXCEEDED
And puedo configurar también notificationEmail y webhookUrl para notificaciones externas

US-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-db
When Qdrant responde al health check
Then veo el estado global (ok/error), la latencia de búsqueda P95 y la tasa de cache hit de embeddings
And veo una tabla con cada colección: nombre, número de vectores, dimensión, último acceso y estado

AC-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-db
When pulso "Reindexar todo" y confirmo
Then se llama a POST /admin/ai/vector-db/reindex
And el AIBackend crea un job que procesa todas las lecciones publicadas
And veo el jobId retornado y puedo seguir el progreso: totalLessons, processedLessons, totalChunks, errors
And el historial de reindexado muestra los últimos jobs con status queued/running/completed/failed

US-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 X
When se llama a POST /ai/tasks con type="QUIZ_GENERATION"
Then la tarea se crea con status=PENDING y reviewStatus=PENDING_REVIEW
And 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/result
And al completarse, status pasa a COMPLETED y resultPayload contiene las preguntas generadas
And el costo y tokens consumidos quedan registrados en los campos costUsd y tokensUsed

AC-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=RUNNING
When el propietario llama a PATCH /ai/tasks/:taskId/cancel
Then el status cambia a CANCELLED
And si la tarea ya está COMPLETED o FAILED, la cancelación retorna error 400

AC-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_REVIEW
When el creador inicia una tarea hija (parentTaskId apunta a la tarea original)
Then la tarea padre pasa a reviewStatus=REFINED
And la tarea hija hereda refinementCount = parentRefinementCount + 1
And si el refinementCount llega a 5, la API retorna 400 "Límite de refinamientos alcanzado"

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')
})