Fix infinite recursion in iterator loop traversal when If/Else branch loops back to enclosing iterator (#19714)
Fix a stack overflow (Maximum call stack size exceeded) in getAllStepIdsInLoop caused by an If/Else branch inside an iterator loop pointing back to the enclosing iterator. The traversal incorrectly treated the enclosing iterator as a nested iterator, calling getAllStepIdsInLoop recursively with fresh visited sets, causing infinite recursion. Add the enclosing iterator's own ID to the skip condition in traverseSteps so back-edges from If/Else branches are handled the same way as back-edges from regular nextStepIds. <img width="1054" height="723" alt="Capture d’écran 2026-04-15 à 11 00 42" src="https://github.com/user-attachments/assets/aee1477b-5059-4552-809e-7c8a34a9ec4a" />
This commit is contained in:
+110
@@ -207,6 +207,116 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('if-else branch looping back to enclosing iterator', () => {
|
||||
it('should not stack overflow when an if-else branch points back to the enclosing iterator', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', [], ['step1']),
|
||||
createMockCodeStep('step1', ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{
|
||||
id: 'branch-if',
|
||||
filterGroupId: 'fg1',
|
||||
nextStepIds: ['stepA'],
|
||||
},
|
||||
{ id: 'branch-else', nextStepIds: ['iterator1'] },
|
||||
]),
|
||||
createMockCodeStep('stepA', ['iterator1']),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
iteratorStepId: 'iterator1',
|
||||
initialLoopStepIds: ['step1'],
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining(['step1', 'ifElse1', 'stepA']),
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).not.toContain('iterator1');
|
||||
});
|
||||
|
||||
it('should handle multiple if-else branches where some loop back to iterator and others continue', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['exitStep'], ['step1']),
|
||||
createMockCodeStep('step1', ['step2']),
|
||||
createMockCodeStep('step2', ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{
|
||||
id: 'branch-skip',
|
||||
filterGroupId: 'fg1',
|
||||
nextStepIds: ['iterator1'],
|
||||
},
|
||||
{
|
||||
id: 'branch-create',
|
||||
filterGroupId: 'fg2',
|
||||
nextStepIds: ['stepCreate'],
|
||||
},
|
||||
{ id: 'branch-default', nextStepIds: [] },
|
||||
]),
|
||||
createMockCodeStep('stepCreate', ['stepSave']),
|
||||
createMockCodeStep('stepSave', ['iterator1']),
|
||||
createMockCodeStep('exitStep', []),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
iteratorStepId: 'iterator1',
|
||||
initialLoopStepIds: ['step1'],
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
'step1',
|
||||
'step2',
|
||||
'ifElse1',
|
||||
'stepCreate',
|
||||
'stepSave',
|
||||
]),
|
||||
);
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).not.toContain('iterator1');
|
||||
expect(result).not.toContain('exitStep');
|
||||
});
|
||||
|
||||
it('should handle nested iterator inside if-else branch that also has a branch looping back to outer iterator', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep('outerIterator', ['exitStep'], ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{
|
||||
id: 'branch-nested',
|
||||
filterGroupId: 'fg1',
|
||||
nextStepIds: ['innerIterator'],
|
||||
},
|
||||
{
|
||||
id: 'branch-skip',
|
||||
nextStepIds: ['outerIterator'],
|
||||
},
|
||||
]),
|
||||
createMockIteratorStep(
|
||||
'innerIterator',
|
||||
['outerIterator'],
|
||||
['innerStep'],
|
||||
),
|
||||
createMockCodeStep('innerStep', ['innerIterator']),
|
||||
createMockCodeStep('exitStep', []),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
iteratorStepId: 'outerIterator',
|
||||
initialLoopStepIds: ['ifElse1'],
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining(['ifElse1', 'innerIterator', 'innerStep']),
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).not.toContain('outerIterator');
|
||||
expect(result).not.toContain('exitStep');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty initial loop step IDs', () => {
|
||||
const steps = [createMockIteratorStep('iterator1', ['step2'], [])];
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ const traverseSteps = ({
|
||||
allStepIdsInLoop: Set<string>;
|
||||
}) => {
|
||||
for (const stepId of stepIds) {
|
||||
if (visitedStepIds.has(stepId)) {
|
||||
if (visitedStepIds.has(stepId) || stepId === iteratorStepId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user