fix: parsear todos los pools en colaboraciones BPMN multi-proceso
El parser usaba querySelector() que retornaba solo el primer <bpmn:process>. En diagramas de colaboración con múltiples pools, las actividades de los pools 2..N nunca se cargaban en la store, haciendo imposible configurarlas. Ahora itera todos los <bpmn:process> hijos directos de <definitions> y combina sus nodos y flujos en un único grafo. Pools de soporte sin StartEvent propio (SAP, bandeja manual) son válidos — la validación exige al menos un StartEvent entre todos los pools del diagrama. Agrega 3 tests de regresión para el escenario de colaboración. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,49 +64,58 @@ export function parseBpmnXml(xml: string): ProcessGraph {
|
|||||||
const flows = new Map<string, ProcessGraphFlow>()
|
const flows = new Map<string, ProcessGraphFlow>()
|
||||||
const startEventIds: BpmnElementId[] = []
|
const startEventIds: BpmnElementId[] = []
|
||||||
|
|
||||||
// Buscar el proceso principal (puede estar anidado bajo definitions)
|
// Buscar TODOS los procesos del documento. Las colaboraciones tienen múltiples
|
||||||
const processEl = doc.querySelector('[*|localName="process"], process')
|
// <bpmn:process> como hijos directos de <definitions>, uno por pool.
|
||||||
if (!processEl) {
|
const definitions = doc.documentElement
|
||||||
|
const processEls = Array.from(definitions.children).filter(
|
||||||
|
(el) => normalizeLocalName(el.tagName) === 'process'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (processEls.length === 0) {
|
||||||
throw new BpmnParseError('No se encontró un elemento <process> en el BPMN')
|
throw new BpmnParseError('No se encontró un elemento <process> en el BPMN')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterar todos los hijos directos del proceso
|
// Parsear cada proceso y acumular nodos y flujos en mapas compartidos
|
||||||
for (const child of Array.from(processEl.children)) {
|
for (const processEl of processEls) {
|
||||||
const local = normalizeLocalName(child.tagName)
|
for (const child of Array.from(processEl.children)) {
|
||||||
const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown'
|
const local = normalizeLocalName(child.tagName)
|
||||||
|
const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown'
|
||||||
|
|
||||||
if (local === 'sequenceflow') {
|
if (local === 'sequenceflow') {
|
||||||
const flow: ProcessGraphFlow = {
|
const flow: ProcessGraphFlow = {
|
||||||
id: getAttr(child, 'id'),
|
id: getAttr(child, 'id'),
|
||||||
sourceRef: getAttr(child, 'sourceRef'),
|
sourceRef: getAttr(child, 'sourceRef'),
|
||||||
targetRef: getAttr(child, 'targetRef'),
|
targetRef: getAttr(child, 'targetRef'),
|
||||||
name: getAttr(child, 'name') || undefined,
|
name: getAttr(child, 'name') || undefined,
|
||||||
|
}
|
||||||
|
if (flow.id) flows.set(flow.id, flow)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if (flow.id) flows.set(flow.id, flow)
|
|
||||||
continue
|
if (type === 'unknown') continue
|
||||||
|
|
||||||
|
const id = getAttr(child, 'id')
|
||||||
|
if (!id) continue
|
||||||
|
|
||||||
|
const node: ProcessGraphNode = {
|
||||||
|
id,
|
||||||
|
name: getAttr(child, 'name'),
|
||||||
|
type,
|
||||||
|
outgoing: parseFlowRefs(child, 'outgoing'),
|
||||||
|
incoming: parseFlowRefs(child, 'incoming'),
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.set(id, node)
|
||||||
|
if (type === 'startEvent') startEventIds.push(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'unknown') continue
|
|
||||||
|
|
||||||
const id = getAttr(child, 'id')
|
|
||||||
if (!id) continue
|
|
||||||
|
|
||||||
const node: ProcessGraphNode = {
|
|
||||||
id,
|
|
||||||
name: getAttr(child, 'name'),
|
|
||||||
type,
|
|
||||||
outgoing: parseFlowRefs(child, 'outgoing'),
|
|
||||||
incoming: parseFlowRefs(child, 'incoming'),
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.set(id, node)
|
|
||||||
if (type === 'startEvent') startEventIds.push(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodes.size === 0) {
|
if (nodes.size === 0) {
|
||||||
throw new BpmnParseError('El proceso no contiene elementos reconocibles')
|
throw new BpmnParseError('El proceso no contiene elementos reconocibles')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pools de soporte (SAP, bandeja manual, etc.) pueden no tener StartEvent propio —
|
||||||
|
// exigimos al menos uno entre todos los pools del diagrama.
|
||||||
if (startEventIds.length === 0) {
|
if (startEventIds.length === 0) {
|
||||||
throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)')
|
throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)')
|
||||||
}
|
}
|
||||||
|
|||||||
42
tests/integration/bpmn-collaboration.test.ts
Normal file
42
tests/integration/bpmn-collaboration.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { parseBpmnXml, extractActivityElements } from '@/domain/bpmn-parser'
|
||||||
|
|
||||||
|
describe('Colaboración multi-pool', () => {
|
||||||
|
const xml = readFileSync(resolve(__dirname, '../proceso_carga_facturas.bpmn'), 'utf-8')
|
||||||
|
|
||||||
|
it('parsea sin error', () => {
|
||||||
|
expect(() => parseBpmnXml(xml)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extrae actividades de TODOS los pools, no solo el primero', () => {
|
||||||
|
const graph = parseBpmnXml(xml)
|
||||||
|
const acts = extractActivityElements(graph)
|
||||||
|
const ids = acts.map((a) => a.bpmnElementId)
|
||||||
|
|
||||||
|
// Pool 1 — IKData (OCR)
|
||||||
|
expect(ids).toContain('Task_OCR')
|
||||||
|
expect(ids).toContain('Task_GenerarPayload')
|
||||||
|
|
||||||
|
// Pool 2 — Robot RPA (proceso principal)
|
||||||
|
expect(ids).toContain('Task_LeerPayload')
|
||||||
|
expect(ids).toContain('Task_AbrirSAP')
|
||||||
|
expect(ids).toContain('Task_GuardarFactura')
|
||||||
|
expect(ids).toContain('Task_DerivarBandeja')
|
||||||
|
|
||||||
|
// Pool 3 — SAP (pool de soporte sin StartEvent propio)
|
||||||
|
expect(ids).toContain('Task_RecibirFactura')
|
||||||
|
|
||||||
|
// Pool 4 — Bandeja manual (pool de soporte sin StartEvent propio)
|
||||||
|
expect(ids).toContain('Task_RevisionManual')
|
||||||
|
|
||||||
|
expect(acts.length).toBeGreaterThanOrEqual(14)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tiene StartEvents de los pools principales aunque otros pools no tengan', () => {
|
||||||
|
const graph = parseBpmnXml(xml)
|
||||||
|
expect(graph.startEventIds).toContain('StartEvent_IKData')
|
||||||
|
expect(graph.startEventIds).toContain('StartEvent_Robot')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user