|
1 | 1 | import { beforeEach, describe, expect, it } from 'vitest' |
2 | 2 | import type { DAG, DAGNode } from '@/executor/dag/builder' |
| 3 | +import { buildBranchNodeId } from '@/executor/utils/subflow-utils' |
3 | 4 | import type { SerializedBlock, SerializedLoop, SerializedWorkflow } from '@/serializer/types' |
4 | 5 | import { EdgeConstructor } from './edges' |
5 | 6 |
|
@@ -133,6 +134,94 @@ describe('EdgeConstructor', () => { |
133 | 134 | }) |
134 | 135 | }) |
135 | 136 |
|
| 137 | + describe('nested subflow skip-at-start bypasses', () => { |
| 138 | + it('wires a nested loop start exit to the next sibling inside a parallel branch', () => { |
| 139 | + const parallelId = 'parallel-1' |
| 140 | + const loopId = 'loop-1' |
| 141 | + const afterId = 'after' |
| 142 | + const loopStartId = `loop-${loopId}-sentinel-start` |
| 143 | + const loopEndId = `loop-${loopId}-sentinel-end` |
| 144 | + const afterTemplateId = buildBranchNodeId(afterId, 0) |
| 145 | + const dag = createMockDAG([loopStartId, loopEndId, afterTemplateId]) |
| 146 | + dag.nodes.get(loopStartId)!.metadata = { |
| 147 | + isSentinel: true, |
| 148 | + sentinelType: 'start', |
| 149 | + subflowId: loopId, |
| 150 | + subflowType: 'loop', |
| 151 | + } |
| 152 | + dag.nodes.get(loopEndId)!.metadata = { |
| 153 | + isSentinel: true, |
| 154 | + sentinelType: 'end', |
| 155 | + subflowId: loopId, |
| 156 | + subflowType: 'loop', |
| 157 | + } |
| 158 | + dag.nodes.get(afterTemplateId)!.metadata = { |
| 159 | + isParallelBranch: true, |
| 160 | + subflowId: parallelId, |
| 161 | + subflowType: 'parallel', |
| 162 | + branchIndex: 0, |
| 163 | + } |
| 164 | + dag.loopConfigs.set(loopId, { id: loopId, nodes: [], iterations: 1 }) |
| 165 | + dag.parallelConfigs.set(parallelId, { |
| 166 | + id: parallelId, |
| 167 | + nodes: [loopId, afterId], |
| 168 | + count: 1, |
| 169 | + }) |
| 170 | + |
| 171 | + const workflow = createMockWorkflow( |
| 172 | + [createMockBlock(loopId, 'loop'), createMockBlock(afterId)], |
| 173 | + [{ source: loopId, target: afterId }] |
| 174 | + ) |
| 175 | + |
| 176 | + edgeConstructor.execute( |
| 177 | + workflow, |
| 178 | + dag, |
| 179 | + new Set([loopId, afterId]), |
| 180 | + new Set(), |
| 181 | + new Set([loopId, afterId]), |
| 182 | + new Map() |
| 183 | + ) |
| 184 | + |
| 185 | + const loopStartTargets = Array.from(dag.nodes.get(loopStartId)!.outgoingEdges.values()) |
| 186 | + expect(loopStartTargets).toContainEqual({ |
| 187 | + target: loopEndId, |
| 188 | + sourceHandle: 'loop_exit', |
| 189 | + targetHandle: undefined, |
| 190 | + }) |
| 191 | + expect(dag.nodes.get(loopEndId)!.incomingEdges).not.toContain(loopStartId) |
| 192 | + }) |
| 193 | + |
| 194 | + it('wires a parallel start exit bypass to a downstream parallel sentinel start', () => { |
| 195 | + const sourceParallelId = 'parallel-a' |
| 196 | + const targetParallelId = 'parallel-b' |
| 197 | + const sourceStartId = `parallel-${sourceParallelId}-sentinel-start` |
| 198 | + const sourceEndId = `parallel-${sourceParallelId}-sentinel-end` |
| 199 | + const targetStartId = `parallel-${targetParallelId}-sentinel-start` |
| 200 | + const targetEndId = `parallel-${targetParallelId}-sentinel-end` |
| 201 | + const dag = createMockDAG([sourceStartId, sourceEndId, targetStartId, targetEndId]) |
| 202 | + dag.parallelConfigs.set(sourceParallelId, { id: sourceParallelId, nodes: [], count: 1 }) |
| 203 | + dag.parallelConfigs.set(targetParallelId, { id: targetParallelId, nodes: [], count: 1 }) |
| 204 | + |
| 205 | + const workflow = createMockWorkflow( |
| 206 | + [ |
| 207 | + createMockBlock(sourceParallelId, 'parallel'), |
| 208 | + createMockBlock(targetParallelId, 'parallel'), |
| 209 | + ], |
| 210 | + [{ source: sourceParallelId, target: targetParallelId }] |
| 211 | + ) |
| 212 | + |
| 213 | + edgeConstructor.execute(workflow, dag, new Set(), new Set(), new Set(), new Map()) |
| 214 | + |
| 215 | + const sourceStartTargets = Array.from(dag.nodes.get(sourceStartId)!.outgoingEdges.values()) |
| 216 | + expect(sourceStartTargets).toContainEqual({ |
| 217 | + target: sourceEndId, |
| 218 | + sourceHandle: 'parallel_exit', |
| 219 | + targetHandle: undefined, |
| 220 | + }) |
| 221 | + expect(dag.nodes.get(sourceEndId)!.incomingEdges).not.toContain(sourceStartId) |
| 222 | + }) |
| 223 | + }) |
| 224 | + |
136 | 225 | describe('Condition block edge wiring', () => { |
137 | 226 | it('should wire condition block edges with proper condition prefixes', () => { |
138 | 227 | const conditionId = 'condition-1' |
@@ -930,7 +1019,6 @@ describe('EdgeConstructor', () => { |
930 | 1019 | ) |
931 | 1020 | expect(edgesToParallelStart.length).toBe(1) |
932 | 1021 | expect(edgesToParallelStart[0].sourceHandle).toBe('parallel_continue') |
933 | | - expect(edgesToParallelStart[0].isActive).toBe(false) |
934 | 1022 |
|
935 | 1023 | const parallelStartNode = dag.nodes.get(parallelSentinelStart)! |
936 | 1024 | expect(parallelStartNode.incomingEdges.has(parallelSentinelEnd)).toBe(false) |
@@ -1367,34 +1455,29 @@ describe('EdgeConstructor', () => { |
1367 | 1455 | dag.nodes.get(outerSentinelStart)!.metadata = { |
1368 | 1456 | isSentinel: true, |
1369 | 1457 | sentinelType: 'start', |
1370 | | - parallelId: outerParallelId, |
1371 | 1458 | subflowId: outerParallelId, |
1372 | 1459 | subflowType: 'parallel', |
1373 | 1460 | } |
1374 | 1461 | dag.nodes.get(outerSentinelEnd)!.metadata = { |
1375 | 1462 | isSentinel: true, |
1376 | 1463 | sentinelType: 'end', |
1377 | | - parallelId: outerParallelId, |
1378 | 1464 | subflowId: outerParallelId, |
1379 | 1465 | subflowType: 'parallel', |
1380 | 1466 | } |
1381 | 1467 | dag.nodes.get(innerSentinelStart)!.metadata = { |
1382 | 1468 | isSentinel: true, |
1383 | 1469 | sentinelType: 'start', |
1384 | | - parallelId: innerParallelId, |
1385 | 1470 | subflowId: innerParallelId, |
1386 | 1471 | subflowType: 'parallel', |
1387 | 1472 | } |
1388 | 1473 | dag.nodes.get(innerSentinelEnd)!.metadata = { |
1389 | 1474 | isSentinel: true, |
1390 | 1475 | sentinelType: 'end', |
1391 | | - parallelId: innerParallelId, |
1392 | 1476 | subflowId: innerParallelId, |
1393 | 1477 | subflowType: 'parallel', |
1394 | 1478 | } |
1395 | 1479 | dag.nodes.get(funcTemplate)!.metadata = { |
1396 | 1480 | isParallelBranch: true, |
1397 | | - parallelId: innerParallelId, |
1398 | 1481 | subflowId: innerParallelId, |
1399 | 1482 | subflowType: 'parallel', |
1400 | 1483 | branchIndex: 0, |
@@ -1494,34 +1577,29 @@ describe('EdgeConstructor', () => { |
1494 | 1577 | dag.nodes.get(loopSentinelStart)!.metadata = { |
1495 | 1578 | isSentinel: true, |
1496 | 1579 | sentinelType: 'start', |
1497 | | - loopId, |
1498 | 1580 | subflowId: loopId, |
1499 | 1581 | subflowType: 'loop', |
1500 | 1582 | } |
1501 | 1583 | dag.nodes.get(loopSentinelEnd)!.metadata = { |
1502 | 1584 | isSentinel: true, |
1503 | 1585 | sentinelType: 'end', |
1504 | | - loopId, |
1505 | 1586 | subflowId: loopId, |
1506 | 1587 | subflowType: 'loop', |
1507 | 1588 | } |
1508 | 1589 | dag.nodes.get(parallelSentinelStart)!.metadata = { |
1509 | 1590 | isSentinel: true, |
1510 | 1591 | sentinelType: 'start', |
1511 | | - parallelId: innerParallelId, |
1512 | 1592 | subflowId: innerParallelId, |
1513 | 1593 | subflowType: 'parallel', |
1514 | 1594 | } |
1515 | 1595 | dag.nodes.get(parallelSentinelEnd)!.metadata = { |
1516 | 1596 | isSentinel: true, |
1517 | 1597 | sentinelType: 'end', |
1518 | | - parallelId: innerParallelId, |
1519 | 1598 | subflowId: innerParallelId, |
1520 | 1599 | subflowType: 'parallel', |
1521 | 1600 | } |
1522 | 1601 | dag.nodes.get(funcTemplate)!.metadata = { |
1523 | 1602 | isParallelBranch: true, |
1524 | | - parallelId: innerParallelId, |
1525 | 1603 | subflowId: innerParallelId, |
1526 | 1604 | subflowType: 'parallel', |
1527 | 1605 | branchIndex: 0, |
@@ -1617,28 +1695,24 @@ describe('EdgeConstructor', () => { |
1617 | 1695 | dag.nodes.get(outerSentinelStart)!.metadata = { |
1618 | 1696 | isSentinel: true, |
1619 | 1697 | sentinelType: 'start', |
1620 | | - parallelId: outerParallelId, |
1621 | 1698 | subflowId: outerParallelId, |
1622 | 1699 | subflowType: 'parallel', |
1623 | 1700 | } |
1624 | 1701 | dag.nodes.get(outerSentinelEnd)!.metadata = { |
1625 | 1702 | isSentinel: true, |
1626 | 1703 | sentinelType: 'end', |
1627 | | - parallelId: outerParallelId, |
1628 | 1704 | subflowId: outerParallelId, |
1629 | 1705 | subflowType: 'parallel', |
1630 | 1706 | } |
1631 | 1707 | dag.nodes.get(innerSentinelStart)!.metadata = { |
1632 | 1708 | isSentinel: true, |
1633 | 1709 | sentinelType: 'start', |
1634 | | - loopId: innerLoopId, |
1635 | 1710 | subflowId: innerLoopId, |
1636 | 1711 | subflowType: 'loop', |
1637 | 1712 | } |
1638 | 1713 | dag.nodes.get(innerSentinelEnd)!.metadata = { |
1639 | 1714 | isSentinel: true, |
1640 | 1715 | sentinelType: 'end', |
1641 | | - loopId: innerLoopId, |
1642 | 1716 | subflowId: innerLoopId, |
1643 | 1717 | subflowType: 'loop', |
1644 | 1718 | } |
|
0 commit comments