Antes de la última revisión de etapa 4. Sprint 1. Previo al ajuste de look n feel de app, reportes, nombre y colores.

This commit is contained in:
2026-05-15 22:14:53 -03:00
parent b6f31f4371
commit 8b71c6ee13
54 changed files with 6497 additions and 261 deletions

View File

@@ -89,6 +89,9 @@ describe('runSimulation', () => {
directCostFixed: directCost,
executionTimeMinutes: minutes,
assignedResources: resources,
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
})
it('calcula costo total sin recursos — proceso lineal', () => {
@@ -208,3 +211,182 @@ describe('runSimulation', () => {
expect(result.perActivity[1].bpmnElementId).toBe('taskA')
})
})
// ─── Sprint 1: escenario 'automated' + actividades automatizables ─────────────
describe('runSimulation — scenario automated', () => {
const makeAutoActivity = (
bpmnElementId: string,
opts: {
directCost?: number; directTime?: number
automatable?: boolean; automatedCost?: number; automatedTime?: number
} = {}
): Activity => ({
id: `act-${bpmnElementId}`,
processId: 'proc1',
bpmnElementId,
name: bpmnElementId,
type: 'task',
directCostFixed: opts.directCost ?? 100,
executionTimeMinutes: opts.directTime ?? 60,
assignedResources: [],
automatable: opts.automatable ?? false,
automatedCostFixed: opts.automatedCost ?? 0,
automatedTimeMinutes: opts.automatedTime ?? 0,
})
// ─── scenario=automated con costos en cero ────────────────────────────────
it('scenario=automated con automatedCostFixed=0 y automatedTimeMinutes=0 devuelve cost=0 time=0 sin NaN', () => {
const act = makeAutoActivity('taskA', {
directCost: 500, directTime: 60,
automatable: true, automatedCost: 0, automatedTime: 0,
})
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act], ['taskB', makeAutoActivity('taskB')]]),
gateways: new Map(),
resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
scenario: 'automated',
})
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')!
expect(actResult).toBeDefined()
expect(actResult.expectedDirectCost).toBe(0)
expect(actResult.executionTimeMinutes).toBe(0)
// Verificación explícita de ausencia de NaN en todos los campos numéricos
expect(Number.isNaN(actResult.expectedDirectCost)).toBe(false)
expect(Number.isNaN(actResult.expectedIndirectCost)).toBe(false)
expect(Number.isNaN(actResult.expectedTotalCost)).toBe(false)
expect(Number.isNaN(actResult.percentOfTotal)).toBe(false)
expect(Number.isNaN(result.totalCost)).toBe(false)
})
it('actividad NO automatable mantiene costos originales en scenario=automated', () => {
const act = makeAutoActivity('taskA', {
directCost: 200, directTime: 30,
automatable: false, automatedCost: 0, automatedTime: 0,
})
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act], ['taskB', makeAutoActivity('taskB')]]),
gateways: new Map(),
resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
scenario: 'automated',
})
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')!
// automatable=false → usa costo original
expect(actResult.expectedDirectCost).toBeCloseTo(200)
expect(actResult.executionTimeMinutes).toBe(30)
})
// ─── La probabilidad de ejecución NO depende del scenario ────────────────
it('probabilidad de ejecución NO cambia entre scenario=actual y scenario=automated', () => {
// Usamos el BPMN XOR: taskB tiene prob 0.7, taskC tiene prob 0.3
const XOR_BPMN_LOCAL = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
<process id="p" isExecutable="false">
<startEvent id="start"><outgoing>f1</outgoing></startEvent>
<task id="taskA" name="A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
<exclusiveGateway id="gw1"><incoming>f2</incoming><outgoing>f3</outgoing><outgoing>f4</outgoing></exclusiveGateway>
<task id="taskB" name="B"><incoming>f3</incoming><outgoing>f5</outgoing></task>
<task id="taskC" name="C"><incoming>f4</incoming><outgoing>f6</outgoing></task>
<endEvent id="end"><incoming>f5</incoming><incoming>f6</incoming></endEvent>
<sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
<sequenceFlow id="f2" sourceRef="taskA" targetRef="gw1"/>
<sequenceFlow id="f3" sourceRef="gw1" targetRef="taskB"/>
<sequenceFlow id="f4" sourceRef="gw1" targetRef="taskC"/>
<sequenceFlow id="f5" sourceRef="taskB" targetRef="end"/>
<sequenceFlow id="f6" sourceRef="taskC" targetRef="end"/>
</process>
</definitions>`
const activities = new Map([
['taskA', makeAutoActivity('taskA', { automatable: true, automatedCost: 5, automatedTime: 2 })],
['taskB', makeAutoActivity('taskB', { automatable: true, automatedCost: 0, automatedTime: 0 })],
['taskC', makeAutoActivity('taskC', { automatable: false })],
])
const gateways = new Map([['gw1', {
id: 'gw1', processId: 'proc1', bpmnElementId: 'gw1', gatewayType: 'exclusive' as const,
branches: [
{ flowId: 'f3', targetElementId: 'taskB', probability: 0.7 },
{ flowId: 'f4', targetElementId: 'taskC', probability: 0.3 },
],
}]])
const baseInput = {
processXml: XOR_BPMN_LOCAL,
activities,
gateways,
resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
}
const resultActual = runSimulation({ ...baseInput, scenario: 'actual' })
const resultAutomated = runSimulation({ ...baseInput, scenario: 'automated' })
// taskA tiene prob 1.0 en ambos
const aActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskA')!
const aAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskA')!
expect(aActual.executionProbability).toBeCloseTo(1.0)
expect(aAuto.executionProbability).toBeCloseTo(1.0)
// taskB tiene prob 0.7 en ambos (el scenario no cambia el grafo)
const bActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskB')!
const bAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskB')!
expect(bActual.executionProbability).toBeCloseTo(0.7)
expect(bAuto.executionProbability).toBeCloseTo(0.7)
// taskC tiene prob 0.3 en ambos
const cActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskC')!
const cAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskC')!
expect(cActual.executionProbability).toBeCloseTo(0.3)
expect(cAuto.executionProbability).toBeCloseTo(0.3)
// Los costos SÍ cambian para actividades automatable=true
// taskB en actual: directCostFixed=100; en automated: automatedCostFixed=0
expect(bActual.expectedDirectCost).toBeGreaterThan(0)
expect(bAuto.expectedDirectCost).toBe(0)
// taskC no es automatable → costos iguales en ambos
expect(cActual.expectedDirectCost).toBeCloseTo(cAuto.expectedDirectCost)
})
it('NaN no aparece en ningún campo cuando TODOS los costos son cero', () => {
const activities = new Map([
['taskA', makeAutoActivity('taskA', { directCost: 0, directTime: 0, automatable: true, automatedCost: 0, automatedTime: 0 })],
['taskB', makeAutoActivity('taskB', { directCost: 0, directTime: 0, automatable: true, automatedCost: 0, automatedTime: 0 })],
])
for (const scenario of ['actual', 'automated'] as const) {
const result = runSimulation({
processXml: LINEAR_BPMN,
activities,
gateways: new Map(),
resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0.2 },
scenario,
})
expect(Number.isNaN(result.totalCost)).toBe(false)
expect(Number.isNaN(result.totalDirectCost)).toBe(false)
expect(Number.isNaN(result.totalIndirectCost)).toBe(false)
for (const act of result.perActivity) {
expect(Number.isNaN(act.expectedDirectCost)).toBe(false)
expect(Number.isNaN(act.expectedIndirectCost)).toBe(false)
expect(Number.isNaN(act.expectedTotalCost)).toBe(false)
expect(Number.isNaN(act.percentOfTotal)).toBe(false)
expect(Number.isNaN(act.executionProbability)).toBe(false)
}
}
})
})