Files
inq-roi-simulador-web/tests/domain/simulation-sprint3.test.ts
Marcos Benítez 892b8b3bc7
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Sprint 4 — Etapa 7 completada. Build limpio, 521/521 tests verdes. Resumen:
Tipos: Resource.processId? eliminado (vestigial); ResourceType ya tenía los 5 valores correctos, sin cambios.

resource-repo.ts: agregado getAllWithCreator() con JOIN a users!created_by (mismo patrón que Etapa 6) y tipo ResourceWithCreator. delete() ahora valida que no haya asignaciones activas en activity_resource_assignments antes de borrar — lanza error explicativo si las hay. Verifiqué ambos contra Supabase real: el JOIN funciona (FK reconocida por PostgREST) y el conteo de guarda también.

Nuevo — src/features/resources/:

resource-display.tsx — labels, colores/íconos por tipo (Lucide: User/Briefcase/Server/Wrench/Package) y ResourceTypeAvatar compartido
ResourceFormSheet.tsx — Sheet reutilizable para crear/editar, usado tanto en el catálogo como en el panel del workspace
ResourceCatalogPage.tsx — página /recursos: búsqueda, filtro por tipo, tabla con badge de tipo, costo, "creado por", editar/eliminar con confirmación
Nuevo — src/components/ui/sheet.tsx: componente shadcn Sheet (no existía), construido sobre @radix-ui/react-dialog que ya estaba instalado — no agregué dependencias nuevas.

ResourcesPanel.tsx rediseñado: ahora solo muestra recursos usados en el proceso actual (derivado de assignedResources), con conteo de actividades por recurso. CRUD completo se mudó a /recursos; el panel solo permite "Nuevo" (catálogo global) y tiene el link "Ver catálogo completo".

Router: ruta /recursos registrada.

ActivityPanel no se tocó, como indicaba el scope.
2026-06-19 00:01:46 -03:00

100 lines
4.2 KiB
TypeScript
Raw Permalink 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.
import { describe, it, expect } from 'vitest'
import { runSimulation } from '@/domain/simulation'
import type { Activity, Resource, SimulationInput } from '@/domain/types'
// BPMN de un solo nodo — execProb = 1.0, sin gateways
const SINGLE_TASK_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
<process id="proc1" isExecutable="false">
<startEvent id="start"><outgoing>f1</outgoing></startEvent>
<task id="taskA" name="Tarea A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
<endEvent id="end"><incoming>f2</incoming></endEvent>
<sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
<sequenceFlow id="f2" sourceRef="taskA" targetRef="end"/>
</process>
</definitions>`
const makeResource = (id: string, costPerHour: number): Resource => ({
id, name: `Recurso ${id}`, type: 'role', costPerHour,
})
const baseActivity = (overrides: Partial<Activity> = {}): Activity => ({
id: 'act-taskA', processId: 'proc1', bpmnElementId: 'taskA',
name: 'Tarea A', type: 'task',
directCostFixed: 0, executionTimeMinutes: 0,
assignedResources: [],
automatable: true,
automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
...overrides,
})
const baseInput = (activity: Activity, resources: Resource[] = []): SimulationInput => ({
processXml: SINGLE_TASK_BPMN,
activities: new Map([['taskA', activity]]),
gateways: new Map(),
resources: new Map(resources.map((r) => [r.id, r])),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
scenario: 'automated',
})
describe('Sprint 3 — automatedAssignedResources en motor de simulación', () => {
it('Caso 1: solo costo fijo, sin recursos TO-BE — resourceCostBreakdown vacío', () => {
const act = baseActivity({ automatedCostFixed: 100, automatedTimeMinutes: 60, automatedAssignedResources: [] })
const result = runSimulation(baseInput(act))
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(100)
expect(actResult.resourceCostBreakdown).toHaveLength(0)
})
it('Caso 2: solo recursos TO-BE — costo = costPerHour × (min/60) × units × execProb', () => {
const r1 = makeResource('r1', 30)
const act = baseActivity({
automatedCostFixed: 0, automatedTimeMinutes: 30,
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 2 }],
})
const result = runSimulation(baseInput(act, [r1]))
// 30 $/h × (60min/60) × 2 units × 1.0 execProb = 60
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(60)
expect(actResult.resourceCostBreakdown).toHaveLength(1)
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
})
it('Caso 3: mixto — costo fijo + recurso TO-BE se suman', () => {
const r1 = makeResource('r1', 60)
const act = baseActivity({
automatedCostFixed: 50, automatedTimeMinutes: 30,
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }],
})
const result = runSimulation(baseInput(act, [r1]))
// fijo=50 + 60*(30/60)*1=30 → total=80
const actResult = result.perActivity[0]
expect(actResult.expectedDirectCost).toBeCloseTo(80)
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(30)
})
it('Caso 4: actividad NO automatable — usa assignedResources, ignora automatedAssignedResources', () => {
const r1 = makeResource('r1', 60)
const r2 = makeResource('r2', 100)
const act = baseActivity({
automatable: false,
directCostFixed: 0,
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
automatedAssignedResources: [{ resourceId: 'r2', utilizationMinutes: 60, units: 1 }],
})
const result = runSimulation(baseInput(act, [r1, r2]))
// escenario automated pero automatable=false → usa r1 (assignedResources), no r2
const actResult = result.perActivity[0]
expect(actResult.resourceCostBreakdown).toHaveLength(1)
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
})
})