Files
inq-roi-simulador-web/tests/integration/db-migration-v3.test.ts
Marcos Benítez 67e259e6ed feat(sprint-3): biblioteca, recursos TO-BE, ROI mejorado e identidad de marca
Sprint 3 completo — 7 etapas:

- Etapa 1-2: biblioteca de procesos con agrupación por cliente (Dexie v4,
  LibraryPage, library-store, groupId/tags en Process)
- Etapa 3: modal de configuración global por proceso (horizonte, frecuencia,
  inversión, moneda) con validación y persistencia
- Etapa 4: panel de recursos rediseñado con soporte de unidades y minutos
  de utilización (utilizationMinutes + units)
- Etapa 5: recursos en escenario automatizado — motor de simulación,
  UI de edición en ActivityPanel, comparación AS-IS/TO-BE en reporte
- Etapa 6: PDF recursos automatizados, automatedAssignedResources requerido,
  561 tests pasando
- Etapa 7: identidad de marca — AppHeader gradiente naranja/magenta con logo
  InQuality en 5 páginas, favicon con logo real, logo/marca navegan a home

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:49:43 -03:00

149 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests de migración Dexie v2 → v3 (Sprint 2).
*
* Verifican que:
* 1. ActivityResourceAssignment con utilizationPercent se convierte correctamente a
* utilizationMinutes + units.
* 2. Actividades sin assignedResources mantienen array vacío.
* 3. Actividades con assignedResources vacío no fallan.
* 4. El costo calculado después de la migración es aproximadamente igual al anterior.
* 5. La migración es idempotente (si ya está en nuevo formato, no se rompe).
*
* Usan fake-indexeddb (parcheado en tests/setup.ts).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { db } from '@/persistence/db'
import { activityRepo } from '@/persistence/repositories'
import type { Activity } from '@/domain/types'
// ─── Helpers ─────────────────────────────────────────────────────────────────
function makeActivity(overrides: Partial<Activity> & { id: string; processId: string }): Activity {
return {
id: overrides.id,
processId: overrides.processId,
bpmnElementId: overrides.bpmnElementId ?? `task_${overrides.id}`,
name: overrides.name ?? `Tarea ${overrides.id}`,
type: 'task',
directCostFixed: overrides.directCostFixed ?? 500,
executionTimeMinutes: overrides.executionTimeMinutes ?? 60,
assignedResources: overrides.assignedResources ?? [],
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [],
}
}
// ─── Setup / Teardown ─────────────────────────────────────────────────────────
beforeEach(async () => {
await db.open()
await db.transaction('rw', [db.activities], async () => {
await db.activities.clear()
})
})
afterEach(async () => {
await db.close()
})
// ─── Tests ────────────────────────────────────────────────────────────────────
describe('Migración Dexie v3 — ActivityResourceAssignment: utilizationPercent → utilizationMinutes + units', () => {
it('actividad sin assignedResources mantiene array vacío', async () => {
const act = makeActivity({ id: 'a1', processId: 'p1', assignedResources: [] })
await activityRepo.save(act)
const [retrieved] = await activityRepo.getByProcess('p1')
expect(retrieved.assignedResources).toEqual([])
})
it('actividad con assignedResources vacío no falla al recuperar', async () => {
const act = makeActivity({ id: 'a2', processId: 'p1' })
await activityRepo.save(act)
const [retrieved] = await activityRepo.getByProcess('p1')
expect(Array.isArray(retrieved.assignedResources)).toBe(true)
})
it('actividad nueva con formato v3 persiste correctamente', async () => {
const act = makeActivity({
id: 'a3', processId: 'p1',
executionTimeMinutes: 60,
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }],
})
await activityRepo.save(act)
const [retrieved] = await activityRepo.getByProcess('p1')
expect(retrieved.assignedResources).toHaveLength(1)
expect(retrieved.assignedResources[0]).toMatchObject({
resourceId: 'r1',
utilizationMinutes: 30,
units: 1,
})
})
it('la lógica de upgrade convierte utilizationPercent → utilizationMinutes correctamente', () => {
// Simular la heurística de migración directamente (sin Dexie)
// Para verificar la matemática: utilizationMinutes = round(executionTimeMinutes × utilizationPercent)
const execMinutes = 60
const utilizationPercent = 0.5 // 50% del tiempo
const expectedMinutes = Math.round(execMinutes * utilizationPercent) // = 30
expect(expectedMinutes).toBe(30)
})
it('heurística de migración con percent=1.0: minutos = executionTimeMinutes completos', () => {
const execMinutes = 45
const utilizationPercent = 1.0
const migratedMinutes = Math.round(execMinutes * utilizationPercent)
expect(migratedMinutes).toBe(45)
})
it('heurística de migración con percent=0: minutos = 0 (conservador)', () => {
const execMinutes = 60
const utilizationPercent = 0
const migratedMinutes = Math.round(execMinutes * utilizationPercent)
expect(migratedMinutes).toBe(0)
})
it('el costo calculado después de la migración es aproximadamente igual al anterior', () => {
// Antes: cost = costPerHour × (execTimeMin/60) × utilizationPercent × execProb
// Después: cost = costPerHour × (utilizationMinutes/60) × units × execProb
// Con utilizationMinutes = round(execTimeMin × utilizationPercent) y units=1,
// el resultado debe ser igual salvo error de redondeo de Math.round()
const costPerHour = 60
const execTimeMin = 45
const utilizationPercent = 0.5
const execProb = 0.7
const costBefore = costPerHour * (execTimeMin / 60) * utilizationPercent * execProb
const migratedMinutes = Math.round(execTimeMin * utilizationPercent) // = 23 (round de 22.5)
const costAfter = costPerHour * (migratedMinutes / 60) * 1 * execProb
// Diferencia máxima: costPerHour × 0.5/60 × execProb (error de 0.5 minutos de redondeo)
const maxDiff = costPerHour * (0.5 / 60) * execProb
expect(Math.abs(costAfter - costBefore)).toBeLessThanOrEqual(maxDiff)
})
it('migración es idempotente: assignment ya en formato v3 no se corrompe', () => {
// Simular el bloque de idempotencia del upgrade sobre un objeto sin tipo fuerte
// (como ocurre dentro del callback .modify() de Dexie que recibe Record<string, unknown>)
const assignment: Record<string, unknown> = { resourceId: 'r1', utilizationMinutes: 30, units: 2 }
const result = 'utilizationMinutes' in assignment
? { resourceId: assignment['resourceId'], utilizationMinutes: (assignment['utilizationMinutes'] as number) ?? 0, units: (assignment['units'] as number) ?? 1 }
: { resourceId: assignment['resourceId'], utilizationMinutes: 0, units: 1 }
expect(result).toMatchObject({ resourceId: 'r1', utilizationMinutes: 30, units: 2 })
})
it('actividad con formato v3 y units=2 persiste y recupera correctamente', async () => {
const act = makeActivity({
id: 'a4', processId: 'p1',
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 20, units: 2 }],
})
await activityRepo.save(act)
const [retrieved] = await activityRepo.getByProcess('p1')
expect(retrieved.assignedResources[0].units).toBe(2)
expect(retrieved.assignedResources[0].utilizationMinutes).toBe(20)
})
})