Skip to content

Jobs y Plataforma

Módulos API: scheduled-jobs · apps/api/src/modules/scheduled-jobs/ Páginas web: AdminScheduledJobsPage · apps/web/src/pages/admin/platform/AdminScheduledJobsPage.tsx RTK Query: adminJobsApi · apps/web/src/store/api/adminJobsApi.ts Spec Playwright: apps/e2e/tests/admin/jobs.spec.ts — 📋 pendiente


MétodoEndpointDescripción
GET/admin/jobsListado de todos los jobs con las últimas 5 ejecuciones cada uno
GET/admin/jobs/:slugDetalle de un job con las últimas 10 ejecuciones
GET/admin/jobs/:slug/statusEstado en tiempo real: isRunning, lastStatus, lastRunAt, nextRunAt
GET/admin/jobs/:slug/executions?limit=NHistorial de ejecuciones paginado (máx. 100)
PATCH/admin/jobs/:slugActualizar cronExpr, timezone, isEnabled, retentionDays, config, notes
POST/admin/jobs/:slug/runLanzar el job manualmente (fire-and-forget, crea JobExecution(RUNNING))
GET/admin/jobs/export-executions?slug=XExportar ejecuciones de un job como JSON

Todos los endpoints requieren rol SUPER_ADMIN. Cualquier otro rol recibe 403 Forbidden.


US-ADM-JOB-001: Ver todos los cron jobs registrados

Section titled “US-ADM-JOB-001: Ver todos los cron jobs registrados”

Como SUPER_ADMIN Quiero ver en una sola pantalla todos los cron jobs registrados en la plataforma con su estado, schedule y último resultado Para tener visibilidad centralizada del estado operacional de las tareas automatizadas

Criterios: AC-ADM-JOB-001 · Casos: TC-ADM-JOB-001, TC-ADM-JOB-002 · Prioridad: Alta · ✅ Implementado


US-ADM-JOB-002: Ver historial de ejecuciones de un job

Section titled “US-ADM-JOB-002: Ver historial de ejecuciones de un job”

Como SUPER_ADMIN Quiero expandir un job para ver el historial detallado de sus ejecuciones: estado, duración, items procesados y mensajes de error Para diagnosticar fallos recurrentes o verificar que un job se ejecutó correctamente

Criterios: AC-ADM-JOB-002 · Casos: TC-ADM-JOB-003 · Prioridad: Alta · ✅ Implementado


US-ADM-JOB-003: Desactivar un job sin tocar código

Section titled “US-ADM-JOB-003: Desactivar un job sin tocar código”

Como SUPER_ADMIN Quiero desactivar un cron job desde la UI usando el toggle de la tarjeta Para suspender temporalmente una tarea automatizada sin necesidad de redeploy ni modificación de código

Criterios: AC-ADM-JOB-003 · Casos: TC-ADM-JOB-004, TC-ADM-JOB-005 · Prioridad: Alta · ✅ Implementado


US-ADM-JOB-004: Reactivar un job desactivado

Section titled “US-ADM-JOB-004: Reactivar un job desactivado”

Como SUPER_ADMIN Quiero reactivar un job previamente desactivado usando el mismo toggle Para restaurar la ejecución automática de la tarea sin redeploy

Criterios: AC-ADM-JOB-004 · Casos: TC-ADM-JOB-006 · Prioridad: Alta · ✅ Implementado


US-ADM-JOB-005: Ejecutar un job manualmente fuera de su schedule

Section titled “US-ADM-JOB-005: Ejecutar un job manualmente fuera de su schedule”

Como SUPER_ADMIN Quiero pulsar “Run” en un job para lanzarlo inmediatamente fuera de su horario programado Para probar que el job funciona correctamente o ejecutarlo ante una necesidad puntual

Criterios: AC-ADM-JOB-005 · Casos: TC-ADM-JOB-007, TC-ADM-JOB-008 · Prioridad: Alta · ✅ Implementado


US-ADM-JOB-006: Configurar la expresión cron y parámetros de un job

Section titled “US-ADM-JOB-006: Configurar la expresión cron y parámetros de un job”

Como SUPER_ADMIN Quiero editar la expresión cron, zona horaria, retención de logs y notas de un job desde un modal Para ajustar la frecuencia de ejecución o añadir contexto sin modificar el código fuente

Criterios: AC-ADM-JOB-006 · Casos: TC-ADM-JOB-009 · Prioridad: Media · ✅ Implementado


US-ADM-JOB-007: Filtrar y buscar jobs por categoría o nombre

Section titled “US-ADM-JOB-007: Filtrar y buscar jobs por categoría o nombre”

Como SUPER_ADMIN Quiero filtrar la lista de jobs por categoría (engagement, billing, ai, etc.) y buscar por nombre o slug Para localizar rápidamente un job específico en una lista extensa

Criterios: AC-ADM-JOB-007 · Casos: TC-ADM-JOB-010 · Prioridad: Media · ✅ Implementado


US-ADM-JOB-008: Acceso denegado a roles no autorizados

Section titled “US-ADM-JOB-008: Acceso denegado a roles no autorizados”

Como sistema de seguridad Quiero que cualquier rol que no sea SUPER_ADMIN reciba 403 Forbidden al intentar acceder a los endpoints de jobs Para garantizar que la gestión de tareas automatizadas queda reservada al administrador del sistema

Criterios: AC-ADM-JOB-008 · Casos: TC-ADM-JOB-011 · Prioridad: Alta · ✅ Implementado


AC-ADM-JOB-001: Ver todos los cron jobs registrados

Section titled “AC-ADM-JOB-001: Ver todos los cron jobs registrados”

US: US-ADM-JOB-001

AC-ADM-JOB-001.1 — Listado completo con estado y schedule

Dado que soy SUPER_ADMIN autenticado
Cuando navego a /admin/platform/jobs
Entonces veo una tarjeta por cada job registrado en la plataforma
Y cada tarjeta muestra el nombre, slug, categoría y expresión cron
Y cada tarjeta muestra el estado actual: OK, Error, En curso, Deshabilitado o Omitido
Y el panel KPI superior muestra el total de jobs, activos/OK, con errores y en ejecución

AC-ADM-JOB-001.2 — El listado se actualiza automáticamente

Dado que estoy en /admin/platform/jobs
Cuando han pasado 30 segundos desde la última carga
Entonces el panel refresca el listado automáticamente sin recargar la página

AC-ADM-JOB-001.3 — Estado visual correcto por resultado

Dado que un job tiene lastStatus = "FAILED"
Cuando veo su tarjeta en el listado
Entonces el indicador de estado es rojo y el label muestra "Error"
Dado que un job tiene isEnabled = false
Cuando veo su tarjeta en el listado
Entonces la tarjeta aparece con opacidad reducida y el label muestra "Deshabilitado"
Dado que un job está actualmente en ejecución
Cuando veo su tarjeta en el listado
Entonces el indicador pulsa en azul y el label muestra "Ejecutando ahora"

AC-ADM-JOB-002: Ver historial de ejecuciones

Section titled “AC-ADM-JOB-002: Ver historial de ejecuciones”

US: US-ADM-JOB-002

AC-ADM-JOB-002.1 — Tabla de ejecuciones al expandir la tarjeta

Dado que estoy en /admin/platform/jobs
Cuando hago clic en la tarjeta de un job
Entonces la tarjeta se expande y muestra la tabla de últimas ejecuciones
Y cada fila de la tabla contiene: fecha de inicio, estado, items procesados, duración y notas/error
Y la tabla muestra las 5 ejecuciones más recientes ordenadas por fecha descendente

AC-ADM-JOB-002.2 — Ejecución fallida muestra mensaje de error

Dado que un job tiene una ejecución con status = "FAILED"
Cuando expando la tarjeta del job
Entonces la fila de esa ejecución muestra el badge rojo "Error"
Y la columna "Notas" muestra el errorMessage registrado por failExecution()

AC-ADM-JOB-002.3 — Ejecución manual se identifica en triggeredBy

Dado que un job fue lanzado manualmente por un admin
Cuando expando la tarjeta del job
Entonces la columna "Notas" de esa ejecución muestra "▶ manual:<adminId>"

AC-ADM-JOB-002.4 — Panel de estadísticas muestra última duración y resultado

Dado que un job tiene ejecuciones previas registradas
Cuando expando su tarjeta
Entonces el panel "Estadísticas" muestra la duración de la última ejecución
Y muestra el badge de color correspondiente al último estado (COMPLETED/FAILED/SKIPPED)

AC-ADM-JOB-003: Desactivar un job desde la UI

Section titled “AC-ADM-JOB-003: Desactivar un job desde la UI”

US: US-ADM-JOB-003

AC-ADM-JOB-003.1 — Toggle desactiva el job inmediatamente

Dado que un job tiene isEnabled = true
Cuando deslizo el toggle de la tarjeta hacia la posición apagada
Entonces la API recibe PATCH /admin/jobs/:slug con { isEnabled: false }
Y la tarjeta refleja el estado deshabilitado (opacidad reducida, label "Deshabilitado")
Y aparece un toast de confirmación "Job X deshabilitado"

AC-ADM-JOB-003.2 — Job desactivado no se ejecuta en el siguiente cron tick

Dado que un job tiene isEnabled = false en BD
Cuando llega el horario programado del job (cron tick)
Entonces el job no ejecuta ninguna lógica de negocio
Y no se crea ningún registro en JobExecution
Y isEnabled() en JobExecutionTrackerService retorna false

AC-ADM-JOB-003.3 — Botón “Run” deshabilitado cuando el job está inactivo

Dado que un job tiene isEnabled = false
Cuando veo la tarjeta del job en el listado
Entonces el botón "▶ Run" está deshabilitado y no se puede pulsar

AC-ADM-JOB-004: Reactivar un job desactivado

Section titled “AC-ADM-JOB-004: Reactivar un job desactivado”

US: US-ADM-JOB-004

AC-ADM-JOB-004.1 — Toggle reactiva el job

Dado que un job tiene isEnabled = false
Cuando deslizo el toggle hacia la posición encendida
Entonces la API recibe PATCH /admin/jobs/:slug con { isEnabled: true }
Y la tarjeta vuelve a mostrarse activa (opacidad normal, estado "OK")
Y aparece un toast "Job X habilitado ✓"

AC-ADM-JOB-004.2 — Cache invalidado al cambiar estado

Dado que el tracker tiene el estado cached como disabled para un job
Cuando el admin reactiva el job desde la UI
Entonces el PATCH a la API invalida el cache del tracker para ese slug
Y el próximo cron tick del job llamará a isEnabled() que consultará la BD y obtendrá true

AC-ADM-JOB-005: Ejecutar un job manualmente

Section titled “AC-ADM-JOB-005: Ejecutar un job manualmente”

US: US-ADM-JOB-005

AC-ADM-JOB-005.1 — Ejecución manual lanza el job y retorna execution id

Dado que soy SUPER_ADMIN y el job está habilitado y no está ejecutándose
Cuando pulso el botón "▶ Run" en la tarjeta del job
Entonces la API recibe POST /admin/jobs/:slug/run
Y la API crea una JobExecution con status = "RUNNING" y triggeredBy = "manual:<adminId>"
Y la respuesta contiene { executionId, status: "RUNNING", triggeredBy }
Y el botón cambia a "⟳ Corriendo" y se deshabilita mientras el job está activo

AC-ADM-JOB-005.2 — Toast de finalización cuando el job completa

Dado que lancé manualmente un job que está ejecutándose
Cuando el job finaliza (status cambia de RUNNING a COMPLETED o FAILED)
Entonces el frontend detecta el cambio vía polling de /admin/jobs/:slug/status
Y aparece el toast '✓ "NombreJob" completado' si fue COMPLETED
Y el listado de jobs se refresca automáticamente

AC-ADM-JOB-005.3 — Conflicto si el job ya está ejecutándose

Dado que el job ya tiene una ejecución con status = "RUNNING"
Cuando el admin intenta lanzarlo manualmente de nuevo
Entonces la API retorna 409 Conflict con mensaje indicando que el job ya está en ejecución
Y el frontend muestra un toast de error

AC-ADM-JOB-005.4 — Handler no registrado retorna 404

Dado que el handler del job no está registrado en ScheduledJobsHandlerRegistry
(por ejemplo, el servidor acaba de reiniciarse y el módulo aún no inicializó)
Cuando el admin pulsa "▶ Run"
Entonces la API retorna 404 con el mensaje correspondiente

AC-ADM-JOB-006: Configurar expresión cron y parámetros

Section titled “AC-ADM-JOB-006: Configurar expresión cron y parámetros”

US: US-ADM-JOB-006

AC-ADM-JOB-006.1 — Modal de edición muestra valores actuales del job

Dado que estoy en /admin/platform/jobs
Cuando pulso el botón ⚙ de la tarjeta de un job
Entonces se abre el modal de configuración con los valores actuales pre-cargados:
la expresión cron, zona horaria, días de retención y notas del job

AC-ADM-JOB-006.2 — Guardar cambios persiste en BD e invalida cache

Dado que he modificado la expresión cron a "0 3 * * *" en el modal
Cuando pulso "Guardar cambios"
Entonces la API recibe PATCH /admin/jobs/:slug con los nuevos valores
Y el tracker invalida el cache para ese slug
Y aparece el toast '✓ Configuración de "slug" guardada'
Y la tarjeta refleja la nueva expresión cron

AC-ADM-JOB-006.3 — Hint muestra descripción legible del cron

Dado que estoy en el modal de edición
Cuando escribo "0 2 * * *" en el campo de expresión cron
Entonces el hint debajo del campo muestra "todos los días a las 02:00 UTC"

US: US-ADM-JOB-007

AC-ADM-JOB-007.1 — Filtro por categoría

Dado que estoy en /admin/platform/jobs y hay jobs de múltiples categorías
Cuando hago clic en el chip "💳 Facturación"
Entonces solo se muestran los jobs cuya category = "billing"
Y el chip queda resaltado con color primario

AC-ADM-JOB-007.2 — Búsqueda por nombre o slug

Dado que escribo "notif" en el campo de búsqueda
Entonces el listado se filtra en tiempo real mostrando solo los jobs
cuyo slug, nombre o descripción contienen "notif" (insensible a mayúsculas)

AC-ADM-JOB-007.3 — Sin resultados muestra mensaje vacío

Dado que aplico un filtro que no coincide con ningún job
Entonces veo el mensaje "No hay jobs que coincidan con el filtro."
Y no hay tarjetas vacías ni errores de consola

AC-ADM-JOB-008: Control de acceso — solo SUPER_ADMIN

Section titled “AC-ADM-JOB-008: Control de acceso — solo SUPER_ADMIN”

US: US-ADM-JOB-008

AC-ADM-JOB-008.1 — Roles no autorizados reciben 403

Dado que soy un usuario con rol FREE, STARTER, PRO, ELITE, CORPORATE_USER,
CORPORATE_ADMIN o CONTENT_ADMIN
Cuando realizo cualquier request a /admin/jobs (GET, PATCH, POST)
Entonces la API responde con 403 Forbidden
Y el cuerpo de la respuesta no revela información del sistema de jobs

AC-ADM-JOB-008.2 — Request sin autenticar recibe 401

Dado que realizo una request a /admin/jobs sin incluir el header Authorization
Entonces la API responde con 401 Unauthorized

AC-ADM-JOB-008.3 — SUPER_ADMIN puede acceder a todos los endpoints

Dado que soy SUPER_ADMIN autenticado con JWT válido
Cuando accedo a GET /admin/jobs, PATCH /admin/jobs/:slug o POST /admin/jobs/:slug/run
Entonces la API responde con 200/201 y los datos correspondientes

TC-ADM-JOB-001: Listado de jobs carga correctamente

Section titled “TC-ADM-JOB-001: Listado de jobs carga correctamente”

AC: AC-ADM-JOB-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

Precondiciones: Al menos un ScheduledJob existe en BD (seed aplicado).

import { fixtures } from '../../fixtures/users.fixture'
import { apiClient } from '../../utils/api-client'
// Nota: fixtures no incluye superAdmin — se debe añadir o usar token directo
test('SUPER_ADMIN puede listar todos los jobs', async () => {
const res = await apiClient.get('/admin/jobs', superAdminToken)
expect(res.status).toBe(200)
expect(Array.isArray(res.data)).toBe(true)
expect(res.data.length).toBeGreaterThan(0)
// Cada job tiene los campos obligatorios
const job = res.data[0]
expect(job).toHaveProperty('slug')
expect(job).toHaveProperty('name')
expect(job).toHaveProperty('cronExpr')
expect(job).toHaveProperty('isEnabled')
expect(job).toHaveProperty('lastStatus')
expect(job).toHaveProperty('executions')
expect(Array.isArray(job.executions)).toBe(true)
})

TC-ADM-JOB-002: KPIs del panel reflejan el estado real

Section titled “TC-ADM-JOB-002: KPIs del panel reflejan el estado real”

AC: AC-ADM-JOB-001.1 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('El panel KPI muestra el conteo correcto de jobs activos', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/platform/jobs')
// Verificar que las tarjetas KPI están presentes
await expect(page.getByText('Total jobs')).toBeVisible()
await expect(page.getByText('Activos / OK')).toBeVisible()
await expect(page.getByText('Con errores')).toBeVisible()
await expect(page.getByText('En ejecución')).toBeVisible()
})

TC-ADM-JOB-003: Historial de ejecuciones — tabla y campos

Section titled “TC-ADM-JOB-003: Historial de ejecuciones — tabla y campos”

AC: AC-ADM-JOB-002.1, AC-ADM-JOB-002.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

Precondiciones: El job test-slug tiene al menos una ejecución registrada en BD.

test('Historial de ejecuciones retorna estructura correcta', async () => {
const res = await apiClient.get('/admin/jobs/test-slug/executions?limit=10', superAdminToken)
expect(res.status).toBe(200)
expect(Array.isArray(res.data)).toBe(true)
if (res.data.length > 0) {
const exec = res.data[0]
expect(exec).toHaveProperty('id')
expect(exec).toHaveProperty('status')
expect(exec).toHaveProperty('startedAt')
expect(exec).toHaveProperty('durationMs')
expect(exec).toHaveProperty('triggeredBy')
expect(['RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED']).toContain(exec.status)
}
})
test('Historial con limit respeta el máximo de 100', async () => {
const res = await apiClient.get('/admin/jobs/test-slug/executions?limit=200', superAdminToken)
expect(res.status).toBe(200)
expect(res.data.length).toBeLessThanOrEqual(100)
})

TC-ADM-JOB-004: Desactivar job — API y estado persistido

Section titled “TC-ADM-JOB-004: Desactivar job — API y estado persistido”

AC: AC-ADM-JOB-003.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('PATCH isEnabled=false desactiva el job en BD', async () => {
// Setup: asegurar que el job está habilitado
await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken)
// Desactivar
const res = await apiClient.patch('/admin/jobs/test-slug', { isEnabled: false }, superAdminToken)
expect(res.status).toBe(200)
expect(res.data.isEnabled).toBe(false)
// Verificar que la BD refleja el cambio
const status = await apiClient.get('/admin/jobs/test-slug/status', superAdminToken)
// isEnabled no está en JobStatusDto — verificar via getJob
const job = await apiClient.get('/admin/jobs/test-slug', superAdminToken)
expect(job.data.isEnabled).toBe(false)
// Teardown: restaurar estado
await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken)
})

TC-ADM-JOB-005: Job desactivado — botón Run deshabilitado en UI

Section titled “TC-ADM-JOB-005: Job desactivado — botón Run deshabilitado en UI”

AC: AC-ADM-JOB-003.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

Precondiciones: Existe un job con isEnabled = false.

test('Botón Run está deshabilitado para job inactivo', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/platform/jobs')
// Encontrar la tarjeta del job deshabilitado
const disabledCard = page.locator('.ajs-card.disabled').first()
await expect(disabledCard).toBeVisible()
// El botón Run debe estar deshabilitado
const runBtn = disabledCard.locator('button', { hasText: 'Run' })
await expect(runBtn).toBeDisabled()
})

AC: AC-ADM-JOB-004.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('PATCH isEnabled=true reactiva el job', async () => {
// Setup: desactivar primero
await apiClient.patch('/admin/jobs/test-slug', { isEnabled: false }, superAdminToken)
// Reactivar
const res = await apiClient.patch('/admin/jobs/test-slug', { isEnabled: true }, superAdminToken)
expect(res.status).toBe(200)
expect(res.data.isEnabled).toBe(true)
})

TC-ADM-JOB-007: Ejecución manual — retorna execution con status RUNNING

Section titled “TC-ADM-JOB-007: Ejecución manual — retorna execution con status RUNNING”

AC: AC-ADM-JOB-005.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

Precondiciones: El handler del job test-slug está registrado (aplicación corriendo).

test('POST /run crea JobExecution con status RUNNING', async () => {
const res = await apiClient.post('/admin/jobs/test-slug/run', {}, superAdminToken)
expect(res.status).toBe(201)
expect(res.data).toHaveProperty('executionId')
expect(res.data.status).toBe('RUNNING')
expect(res.data.triggeredBy).toMatch(/^manual:/)
})

TC-ADM-JOB-008: Conflicto al lanzar job ya en ejecución

Section titled “TC-ADM-JOB-008: Conflicto al lanzar job ya en ejecución”

AC: AC-ADM-JOB-005.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('Segundo POST /run retorna 409 si el job ya está RUNNING', async () => {
// Lanzar el job — puede completarse muy rápido; este test requiere un job con latencia
// En tests, usar un job de mantenimiento con duración conocida
// Opción alternativa: crear un registro RUNNING en BD directamente via seed de test
const res = await apiClient.post('/admin/jobs/test-long-running-slug/run', {}, superAdminToken)
if (res.status === 201) {
// Si el primero fue exitoso, intentar un segundo inmediatamente
const res2 = await apiClient.post('/admin/jobs/test-long-running-slug/run', {}, superAdminToken)
// Puede ser 409 si aún está RUNNING, o 201 si ya terminó (job muy rápido)
expect([201, 409]).toContain(res2.status)
if (res2.status === 409) {
expect(res2.data.message).toMatch(/already running/i)
}
}
})

TC-ADM-JOB-009: Actualizar expresión cron del job

Section titled “TC-ADM-JOB-009: Actualizar expresión cron del job”

AC: AC-ADM-JOB-006.2 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('PATCH cronExpr actualiza la configuración del job', async () => {
const originalJob = await apiClient.get('/admin/jobs/test-slug', superAdminToken)
const originalCron = originalJob.data.cronExpr
const newCron = '0 3 * * *'
const res = await apiClient.patch(
'/admin/jobs/test-slug',
{ cronExpr: newCron, timezone: 'UTC', retentionDays: 30 },
superAdminToken
)
expect(res.status).toBe(200)
expect(res.data.cronExpr).toBe(newCron)
// Teardown: restaurar cron original
await apiClient.patch('/admin/jobs/test-slug', { cronExpr: originalCron }, superAdminToken)
})

TC-ADM-JOB-010: Filtrado por categoría en UI

Section titled “TC-ADM-JOB-010: Filtrado por categoría en UI”

AC: AC-ADM-JOB-007.1 · Prioridad: Baja · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

test('Chip de categoría filtra la lista de jobs visible', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/platform/jobs')
// Activar filtro "Facturación"
await page.locator('.ajs-chip', { hasText: 'Facturación' }).click()
// Verificar que los chips de otras categorías no aparecen en las tarjetas visibles
const visibleCards = page.locator('.ajs-card')
const count = await visibleCards.count()
for (let i = 0; i < count; i++) {
await expect(visibleCards.nth(i).locator('.ajs-badge-pr')).toContainText('Facturación')
}
})

TC-ADM-JOB-011: Control de acceso — roles no autorizados reciben 403

Section titled “TC-ADM-JOB-011: Control de acceso — roles no autorizados reciben 403”

AC: AC-ADM-JOB-008.1, AC-ADM-JOB-008.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/admin/jobs.spec.ts

import { fixtures } from '../../fixtures/users.fixture'
import { apiClient } from '../../utils/api-client'
const unauthorizedFixtures = [
fixtures.free,
fixtures.starter,
fixtures.pro,
fixtures.elite,
fixtures.corpUser,
fixtures.corpAdmin,
fixtures.contentAdmin,
]
test.describe('GET /admin/jobs — acceso denegado a roles no autorizados', () => {
for (const fixture of unauthorizedFixtures) {
test(`role ${fixture.role} recibe 403`, async () => {
const res = await apiClient.get('/admin/jobs', fixture.token)
expect(res.status).toBe(403)
})
}
})
test('Request sin token recibe 401', async () => {
const res = await apiClient.get('/admin/jobs', null)
expect(res.status).toBe(401)
})
test('PATCH sin token recibe 401', async () => {
const res = await apiClient.patch('/admin/jobs/any-slug', { isEnabled: false }, null)
expect(res.status).toBe(401)
})
test('POST /run sin token recibe 401', async () => {
const res = await apiClient.post('/admin/jobs/any-slug/run', {}, null)
expect(res.status).toBe(401)
})

Modelo PrismaCampos relevantes para tests
ScheduledJobslug (único), isEnabled, lastStatus, lastRunAt, cronExpr, timezone
JobExecutionstatus (RUNNING/COMPLETED/FAILED/SKIPPED), durationMs, itemsProcessed, errorMessage, triggeredBy

Comportamiento del tracker en condiciones extremas

Section titled “Comportamiento del tracker en condiciones extremas”
CondiciónComportamiento esperado
BD no disponible en isEnabled()Fail-open: el job ejecuta de todas formas
BD no disponible en startExecution()Retorna null; el job ejecuta sin registrar en BD
Cache TTL de isEnabled60 segundos — el toggle del admin invalida el cache inmediatamente via invalidateCache(slug)
Job sin handler registrado (cold start)POST /run retorna 404 con mensaje descriptivo

Los tests de esta área requieren un fixture superAdmin que no existe actualmente en apps/e2e/fixtures/users.fixture.ts. Antes de implementar la suite, añadir:

apps/e2e/fixtures/users.fixture.ts
superAdmin: {
email: 'super.admin@test.fluency.com',
password: 'TestPassword123!',
role: 'SUPER_ADMIN',
token: '',
},

Y crear el usuario correspondiente en el seed de test (apps/e2e/fixtures/auth.setup.ts).