Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eea01e787 | |||
| 12ed23ec94 | |||
| e849b67c52 |
+78
@@ -0,0 +1,78 @@
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
|
||||
import { InputHint } from '@/ui/input/components/InputHint';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import styled from '@emotion/styled';
|
||||
import { useId } from 'react';
|
||||
import { Toggle } from 'twenty-ui/input';
|
||||
|
||||
type FormBooleanFieldToggleInputProps = {
|
||||
label?: string;
|
||||
description: string;
|
||||
hint?: string;
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledToggleContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-right: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const FormBooleanFieldToggleInput = ({
|
||||
label,
|
||||
description,
|
||||
hint,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: FormBooleanFieldToggleInputProps) => {
|
||||
const instanceId = useId();
|
||||
|
||||
return (
|
||||
<FormFieldInputContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement
|
||||
preventFocusStackUpdate
|
||||
>
|
||||
<StyledDescription>{description}</StyledDescription>
|
||||
</FormFieldInputInnerContainer>
|
||||
|
||||
<StyledToggleContainer>
|
||||
<Toggle
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</StyledToggleContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
|
||||
{hint && <InputHint>{hint}</InputHint>}
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { FormBooleanFieldToggleInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldToggleInput';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
const meta: Meta<typeof FormBooleanFieldToggleInput> = {
|
||||
title: 'UI/Data/Field/Form/Input/FormBooleanFieldToggleInput',
|
||||
component: FormBooleanFieldToggleInput,
|
||||
args: {
|
||||
description: 'Continue on iteration failure',
|
||||
value: false,
|
||||
onChange: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FormBooleanFieldToggleInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Continue on iteration failure');
|
||||
|
||||
const checkbox = canvas.getByRole('checkbox');
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLabel: Story = {
|
||||
args: {
|
||||
label: 'Settings',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Settings');
|
||||
await canvas.findByText('Continue on iteration failure');
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHint: Story = {
|
||||
args: {
|
||||
hint: 'If enabled, the workflow will continue to the next iteration even if the current one fails.',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText(
|
||||
'If enabled, the workflow will continue to the next iteration even if the current one fails.',
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ToggledOn: Story = {
|
||||
args: {
|
||||
value: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const checkbox = canvas.getByRole('checkbox');
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
},
|
||||
};
|
||||
|
||||
export const TogglesValue: Story = {
|
||||
args: {
|
||||
value: false,
|
||||
onChange: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const checkbox = canvas.getByRole('checkbox');
|
||||
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
},
|
||||
};
|
||||
+2
-1
@@ -41,7 +41,8 @@ export const getWorkflowDiagramColors = ({
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'FAILED': {
|
||||
case 'FAILED':
|
||||
case 'FAILED_SAFELY': {
|
||||
return {
|
||||
selected: {
|
||||
background: theme.color.red2,
|
||||
|
||||
+2
-1
@@ -171,7 +171,8 @@ export const WorkflowRunDiagramStepNode = ({
|
||||
</StyledStatusIconsContainer>
|
||||
)}
|
||||
|
||||
{data.runStatus === StepStatus.FAILED && (
|
||||
{(data.runStatus === StepStatus.FAILED ||
|
||||
data.runStatus === StepStatus.FAILED_SAFELY) && (
|
||||
<StyledStatusIconsContainer>
|
||||
<StyledColorIcon color={theme.tag.background.red}>
|
||||
<IconX color={theme.tag.text.red} size={14} />
|
||||
|
||||
+15
-1
@@ -1,4 +1,5 @@
|
||||
import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
|
||||
import { FormBooleanFieldToggleInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldToggleInput';
|
||||
import { type FieldArrayValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
@@ -56,6 +57,8 @@ export const WorkflowEditActionIterator = ({
|
||||
const [formData, setFormData] = useState({
|
||||
items: parsedItems,
|
||||
initialLoopStepIds: action.settings.input.initialLoopStepIds || [],
|
||||
shouldContinueOnIterationFailure:
|
||||
action.settings.input.shouldContinueOnIterationFailure ?? false,
|
||||
});
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
@@ -71,6 +74,8 @@ export const WorkflowEditActionIterator = ({
|
||||
input: {
|
||||
items: updatedFormData.items,
|
||||
initialLoopStepIds: updatedFormData.initialLoopStepIds,
|
||||
shouldContinueOnIterationFailure:
|
||||
updatedFormData.shouldContinueOnIterationFailure,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -80,7 +85,7 @@ export const WorkflowEditActionIterator = ({
|
||||
|
||||
const handleFieldChange = (
|
||||
field: string,
|
||||
value: string | FieldArrayValue,
|
||||
value: string | FieldArrayValue | boolean,
|
||||
) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
@@ -104,6 +109,15 @@ export const WorkflowEditActionIterator = ({
|
||||
readonly={actionOptions.readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormBooleanFieldToggleInput
|
||||
description={t`Continue on iteration failure`}
|
||||
value={formData.shouldContinueOnIterationFailure}
|
||||
onChange={(value) =>
|
||||
handleFieldChange('shouldContinueOnIterationFailure', value)
|
||||
}
|
||||
disabled={actionOptions.readonly}
|
||||
hint={t`Will continue to the next iteration even if the current one fails`}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
|
||||
+1
@@ -486,6 +486,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
input: {
|
||||
items: [],
|
||||
initialLoopStepIds: [emptyNodeStep.id],
|
||||
shouldContinueOnIterationFailure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
export const TERMINAL_STEP_STATUSES = [
|
||||
StepStatus.SUCCESS,
|
||||
StepStatus.STOPPED,
|
||||
StepStatus.SKIPPED,
|
||||
StepStatus.FAILED_SAFELY,
|
||||
];
|
||||
+1
@@ -5,4 +5,5 @@ export type WorkflowActionOutput = {
|
||||
shouldEndWorkflowRun?: boolean;
|
||||
shouldRemainRunning?: boolean;
|
||||
shouldSkipStepExecution?: boolean;
|
||||
shouldFailSafely?: boolean;
|
||||
};
|
||||
|
||||
+48
@@ -460,6 +460,54 @@ describe('shouldExecuteChildStep', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when one parent succeeded and another is FAILED_SAFELY', () => {
|
||||
const stepInfos = {
|
||||
'parent-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'parent-2': {
|
||||
status: StepStatus.FAILED_SAFELY,
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldExecuteChildStep({
|
||||
parentSteps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when single parent is FAILED_SAFELY', () => {
|
||||
const singleParent = [
|
||||
{
|
||||
id: 'parent-1',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
} as unknown as WorkflowAction,
|
||||
];
|
||||
|
||||
const stepInfos = {
|
||||
'parent-1': {
|
||||
status: StepStatus.FAILED_SAFELY,
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldExecuteChildStep({
|
||||
parentSteps: singleParent,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false with large number of parents when not all completed', () => {
|
||||
const manyParentSteps = Array.from({ length: 25 }, (_, i) => ({
|
||||
id: `parent-${i + 1}`,
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
createMockCodeStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { shouldFailSafely } from 'src/modules/workflow/workflow-executor/utils/should-fail-safely.util';
|
||||
|
||||
describe('shouldFailSafely', () => {
|
||||
it('should return false when step has no parents', () => {
|
||||
const step = createMockCodeStep('step1');
|
||||
const steps = [step];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step,
|
||||
steps,
|
||||
stepInfos: {},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when all parents are SUCCESS', () => {
|
||||
const parentStep = createMockCodeStep('parent', ['child']);
|
||||
const childStep = createMockCodeStep('child');
|
||||
const steps = [parentStep, childStep];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: childStep,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent: { status: StepStatus.SUCCESS, result: {} },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when one parent is FAILED_SAFELY and all are terminal', () => {
|
||||
const parent1 = createMockCodeStep('parent1', ['child']);
|
||||
const parent2 = createMockCodeStep('parent2', ['child']);
|
||||
const childStep = createMockCodeStep('child');
|
||||
const steps = [parent1, parent2, childStep];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: childStep,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent1: { status: StepStatus.FAILED_SAFELY, error: 'some error' },
|
||||
parent2: { status: StepStatus.SUCCESS, result: {} },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when parent is FAILED_SAFELY but another parent is still running', () => {
|
||||
const parent1 = createMockCodeStep('parent1', ['child']);
|
||||
const parent2 = createMockCodeStep('parent2', ['child']);
|
||||
const childStep = createMockCodeStep('child');
|
||||
const steps = [parent1, parent2, childStep];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: childStep,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent1: { status: StepStatus.FAILED_SAFELY, error: 'some error' },
|
||||
parent2: { status: StepStatus.RUNNING },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when all parents are FAILED_SAFELY', () => {
|
||||
const parent1 = createMockCodeStep('parent1', ['child']);
|
||||
const parent2 = createMockCodeStep('parent2', ['child']);
|
||||
const childStep = createMockCodeStep('child');
|
||||
const steps = [parent1, parent2, childStep];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: childStep,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent1: { status: StepStatus.FAILED_SAFELY },
|
||||
parent2: { status: StepStatus.FAILED_SAFELY, error: 'err' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for FAILED_SAFELY + SKIPPED parents', () => {
|
||||
const parent1 = createMockCodeStep('parent1', ['child']);
|
||||
const parent2 = createMockCodeStep('parent2', ['child']);
|
||||
const childStep = createMockCodeStep('child');
|
||||
const steps = [parent1, parent2, childStep];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: childStep,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent1: { status: StepStatus.FAILED_SAFELY },
|
||||
parent2: { status: StepStatus.SKIPPED },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldFailSafely for iterator steps', () => {
|
||||
it('should return false for iterator with flag when failure from own loop', () => {
|
||||
const iterator = createMockIteratorStep(
|
||||
'iterator1',
|
||||
['after'],
|
||||
['stepA'],
|
||||
true,
|
||||
);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.FAILED_SAFELY, error: 'actual error' },
|
||||
stepB: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for iterator without flag when loop-back parent is FAILED_SAFELY', () => {
|
||||
const iterator = createMockIteratorStep(
|
||||
'iterator1',
|
||||
['after'],
|
||||
['stepA'],
|
||||
false,
|
||||
);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.FAILED_SAFELY, error: 'err' },
|
||||
stepB: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for unstarted iterator when external parent is FAILED_SAFELY', () => {
|
||||
const parentStep = createMockCodeStep('parent', ['iterator1']);
|
||||
const iterator = createMockIteratorStep(
|
||||
'iterator1',
|
||||
['after'],
|
||||
['stepA'],
|
||||
true,
|
||||
);
|
||||
const stepA = createMockCodeStep('stepA', ['iterator1']);
|
||||
const steps = [parentStep, iterator, stepA];
|
||||
|
||||
const result = shouldFailSafely({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
+36
-60
@@ -1,39 +1,15 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { createMockCodeStep } from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { shouldSkipStepExecution } from 'src/modules/workflow/workflow-executor/utils/should-skip-step-execution.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
describe('shouldSkipStepExecution', () => {
|
||||
const createMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: 'Mock Step',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: 'mock-function-id',
|
||||
logicFunctionInput: {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
});
|
||||
|
||||
it('should return true when all parent steps are skipped', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -52,9 +28,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return true when all parent steps are stopped', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.STOPPED },
|
||||
@@ -73,9 +49,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return true when parent steps are mix of skipped and stopped', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -94,9 +70,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when at least one parent step is successful', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -115,9 +91,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when at least one parent step is failed', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -136,9 +112,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when at least one parent step is running', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -157,9 +133,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when at least one parent step is not started', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-3']),
|
||||
createMockStep('step-2', ['step-3']),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-3']),
|
||||
createMockCodeStep('step-2', ['step-3']),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -178,9 +154,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when there are no parent steps', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockStep('step-3', []),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-2', []),
|
||||
createMockCodeStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -199,8 +175,8 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return true when single parent step is skipped', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -218,9 +194,9 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should handle undefined steps gracefully', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
undefined as unknown as WorkflowAction,
|
||||
createMockStep('step-2', []),
|
||||
createMockCodeStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -238,8 +214,8 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should return false when parent step status is pending', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.PENDING },
|
||||
@@ -257,10 +233,10 @@ describe('shouldSkipStepExecution', () => {
|
||||
|
||||
it('should work with multiple parent steps with different statuses', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-4']),
|
||||
createMockStep('step-2', ['step-4']),
|
||||
createMockStep('step-3', ['step-4']),
|
||||
createMockStep('step-4', []),
|
||||
createMockCodeStep('step-1', ['step-4']),
|
||||
createMockCodeStep('step-2', ['step-4']),
|
||||
createMockCodeStep('step-3', ['step-4']),
|
||||
createMockCodeStep('step-4', []),
|
||||
];
|
||||
|
||||
// Test case 1: All skipped - should return true
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { type StepIfElseBranch } from 'twenty-shared/workflow';
|
||||
|
||||
import { type WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
||||
import { type WorkflowIfElseActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-action-settings.type';
|
||||
import { type WorkflowIteratorActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import {
|
||||
WorkflowActionType,
|
||||
type WorkflowCodeAction,
|
||||
type WorkflowIfElseAction,
|
||||
type WorkflowIteratorAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const createMockCodeStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowCodeAction => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: WorkflowActionType.CODE,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: {},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
} as WorkflowCodeActionSettings,
|
||||
});
|
||||
|
||||
export const createMockIteratorStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
initialLoopStepIds: string[] = [],
|
||||
shouldContinueOnIterationFailure = false,
|
||||
): WorkflowIteratorAction => ({
|
||||
id,
|
||||
name: `Iterator ${id}`,
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds,
|
||||
shouldContinueOnIterationFailure,
|
||||
} as WorkflowIteratorActionInput,
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const createMockIfElseStep = (
|
||||
id: string,
|
||||
branches: StepIfElseBranch[],
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowIfElseAction => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: WorkflowActionType.IF_ELSE,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: {
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
branches,
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
} as WorkflowIfElseActionSettings,
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { TERMINAL_STEP_STATUSES } from 'src/modules/workflow/workflow-executor/constants/terminal-step-statuses.constant';
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { shouldFailSafelyIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-fail-safely-iterator-step.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const shouldFailSafely = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
}): boolean => {
|
||||
if (isWorkflowIteratorAction(step)) {
|
||||
return shouldFailSafelyIteratorStep({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
}
|
||||
|
||||
const parentSteps = steps.filter(
|
||||
(parentStep) =>
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(step.id),
|
||||
);
|
||||
|
||||
if (parentSteps.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const areAllParentsTerminal = parentSteps.every((parentStep) =>
|
||||
TERMINAL_STEP_STATUSES.includes(stepInfos[parentStep.id]?.status),
|
||||
);
|
||||
|
||||
const hasFailedSafelyParent = parentSteps.some(
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED_SAFELY,
|
||||
);
|
||||
|
||||
return areAllParentsTerminal && hasFailedSafelyParent;
|
||||
};
|
||||
+4
-3
@@ -32,8 +32,9 @@ export const shouldSkipStepExecution = ({
|
||||
}
|
||||
|
||||
return parentSteps.every(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[step.id]?.status === StepStatus.STOPPED,
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.STOPPED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED_SAFELY,
|
||||
);
|
||||
};
|
||||
|
||||
+7
-4
@@ -17,11 +17,14 @@ export const workflowShouldKeepRunning = ({
|
||||
),
|
||||
);
|
||||
|
||||
const successStepWithNotStartedExecutableChildren = steps.some(
|
||||
const completedStepWithNotStartedExecutableChildren = steps.some(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SUCCESS &&
|
||||
(stepInfos[step.id]?.status === StepStatus.SUCCESS ||
|
||||
stepInfos[step.id]?.status === StepStatus.FAILED_SAFELY) &&
|
||||
(step.nextStepIds ?? []).some((nextStepId) => {
|
||||
const nextStep = steps.find((step) => step.id === nextStepId);
|
||||
const nextStep = steps.find(
|
||||
(candidateStep) => candidateStep.id === nextStepId,
|
||||
);
|
||||
|
||||
if (!nextStep) {
|
||||
return false;
|
||||
@@ -40,6 +43,6 @@ export const workflowShouldKeepRunning = ({
|
||||
);
|
||||
|
||||
return (
|
||||
runningOrPendingStepExists || successStepWithNotStartedExecutableChildren
|
||||
runningOrPendingStepExists || completedStepWithNotStartedExecutableChildren
|
||||
);
|
||||
};
|
||||
|
||||
+6
-6
@@ -192,9 +192,9 @@ describe('IteratorWorkflowAction', () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
workflowRunWorkspaceService.getWorkflowRunOrFail
|
||||
.mockResolvedValueOnce(mockStepInfo)
|
||||
.mockResolvedValueOnce(mockStepInfo);
|
||||
workflowRunWorkspaceService.getWorkflowRunOrFail.mockResolvedValueOnce(
|
||||
mockStepInfo,
|
||||
);
|
||||
|
||||
const result = await service.execute(input);
|
||||
|
||||
@@ -243,9 +243,9 @@ describe('IteratorWorkflowAction', () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
workflowRunWorkspaceService.getWorkflowRunOrFail
|
||||
.mockResolvedValueOnce(mockStepInfo)
|
||||
.mockResolvedValueOnce(mockStepInfo);
|
||||
workflowRunWorkspaceService.getWorkflowRunOrFail.mockResolvedValueOnce(
|
||||
mockStepInfo,
|
||||
);
|
||||
|
||||
const result = await service.execute(input);
|
||||
|
||||
|
||||
+3
-8
@@ -113,6 +113,7 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
workflowRunId: runInfo.workflowRunId,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
iteratorStepId: string;
|
||||
initialLoopStepIds: string[];
|
||||
@@ -136,17 +138,10 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: Record<string, WorkflowRunStepInfo>;
|
||||
}) {
|
||||
let stepInfosToUpdate: Record<string, WorkflowRunStepInfo> = {};
|
||||
|
||||
const workflowRunToUpdate =
|
||||
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const stepInfos = workflowRunToUpdate.state.stepInfos;
|
||||
|
||||
if (!hasProcessedAllItems) {
|
||||
const subStepsInfos = await this.buildSubStepInfosReset({
|
||||
iteratorStepId,
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ export type WorkflowIteratorActionInput = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?: Array<any> | string;
|
||||
initialLoopStepIds?: string[];
|
||||
shouldContinueOnIterationFailure?: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowIteratorActionSettings = BaseWorkflowActionSettings & {
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
createMockCodeStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { findEnclosingIteratorWithContinueOnFailure } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/find-enclosing-iterator-with-continue-on-failure.util';
|
||||
|
||||
describe('findEnclosingIteratorWithContinueOnFailure', () => {
|
||||
it('should return undefined when step is not inside any iterator', () => {
|
||||
const steps = [
|
||||
createMockCodeStep('step1', ['step2']),
|
||||
createMockCodeStep('step2', []),
|
||||
];
|
||||
|
||||
const result = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: 'step1',
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when enclosing iterator does not have the flag', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['after'], ['stepA'], false),
|
||||
createMockCodeStep('stepA', ['stepB']),
|
||||
createMockCodeStep('stepB', ['iterator1']),
|
||||
createMockCodeStep('after', []),
|
||||
];
|
||||
|
||||
const result = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: 'stepA',
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the enclosing iterator when it has the flag', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['after'], ['stepA'], true),
|
||||
createMockCodeStep('stepA', ['stepB']),
|
||||
createMockCodeStep('stepB', ['iterator1']),
|
||||
createMockCodeStep('after', []),
|
||||
];
|
||||
|
||||
const result = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: 'stepA',
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result?.id).toBe('iterator1');
|
||||
});
|
||||
|
||||
it('should return the innermost iterator with the flag in nested iterators', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep(
|
||||
'outerIterator',
|
||||
['after'],
|
||||
['innerIterator'],
|
||||
true,
|
||||
),
|
||||
createMockIteratorStep(
|
||||
'innerIterator',
|
||||
['outerIterator'],
|
||||
['stepA'],
|
||||
true,
|
||||
),
|
||||
createMockCodeStep('stepA', ['stepB']),
|
||||
createMockCodeStep('stepB', ['innerIterator']),
|
||||
createMockCodeStep('after', []),
|
||||
];
|
||||
|
||||
const result = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: 'stepA',
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result?.id).toBe('innerIterator');
|
||||
});
|
||||
|
||||
it('should skip inner iterator without flag and return outer with flag', () => {
|
||||
const steps = [
|
||||
createMockIteratorStep(
|
||||
'outerIterator',
|
||||
['after'],
|
||||
['innerIterator'],
|
||||
true,
|
||||
),
|
||||
createMockIteratorStep(
|
||||
'innerIterator',
|
||||
['outerIterator'],
|
||||
['stepA'],
|
||||
false,
|
||||
),
|
||||
createMockCodeStep('stepA', ['stepB']),
|
||||
createMockCodeStep('stepB', ['innerIterator']),
|
||||
createMockCodeStep('after', []),
|
||||
];
|
||||
|
||||
const result = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: 'stepA',
|
||||
steps,
|
||||
});
|
||||
|
||||
expect(result?.id).toBe('outerIterator');
|
||||
});
|
||||
});
|
||||
+69
-143
@@ -1,90 +1,18 @@
|
||||
import { type StepIfElseBranch } from 'twenty-shared/workflow';
|
||||
|
||||
import { type WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
||||
import { type WorkflowIfElseActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-action-settings.type';
|
||||
import { type WorkflowIteratorActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
type WorkflowCodeAction,
|
||||
type WorkflowIfElseAction,
|
||||
type WorkflowIteratorAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
createMockCodeStep,
|
||||
createMockIfElseStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util';
|
||||
|
||||
describe('getAllStepIdsInLoop', () => {
|
||||
const createCodeMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[],
|
||||
): WorkflowCodeAction => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: WorkflowActionType.CODE,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: {},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
} as WorkflowCodeActionSettings,
|
||||
});
|
||||
|
||||
const createIteratorMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[],
|
||||
initialLoopStepIds: string[],
|
||||
): WorkflowIteratorAction => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: (initialLoopStepIds
|
||||
? ({ initialLoopStepIds } as WorkflowIteratorActionInput)
|
||||
: {}) as WorkflowIteratorActionInput,
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createIfElseMockStep = (
|
||||
id: string,
|
||||
branches: StepIfElseBranch[],
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowIfElseAction => ({
|
||||
id,
|
||||
name: `Step ${id}`,
|
||||
type: WorkflowActionType.IF_ELSE,
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
settings: {
|
||||
input: {
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
branches,
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
} as WorkflowIfElseActionSettings,
|
||||
});
|
||||
|
||||
describe('simple loop scenarios', () => {
|
||||
it('should return all step IDs in a simple linear loop', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['step3']),
|
||||
createCodeMockStep('step3', ['step4']),
|
||||
createCodeMockStep('step4', ['iterator1']), // loops back
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['step3']),
|
||||
createMockCodeStep('step3', ['step4']),
|
||||
createMockCodeStep('step4', ['iterator1']), // loops back
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -97,12 +25,12 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle loop with branching paths that converge', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['step3', 'step4']),
|
||||
createCodeMockStep('step3', ['step5']),
|
||||
createCodeMockStep('step4', ['step5']),
|
||||
createCodeMockStep('step5', ['iterator1']), // loops back
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['step3', 'step4']),
|
||||
createMockCodeStep('step3', ['step5']),
|
||||
createMockCodeStep('step4', ['step5']),
|
||||
createMockCodeStep('step5', ['iterator1']), // loops back
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -115,11 +43,11 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle loop with branching paths that converge to the iterator', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['step3', 'step4']),
|
||||
createCodeMockStep('step3', ['iterator1']),
|
||||
createCodeMockStep('step4', ['iterator1']),
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['step3', 'step4']),
|
||||
createMockCodeStep('step3', ['iterator1']),
|
||||
createMockCodeStep('step4', ['iterator1']),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -132,11 +60,11 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle multiple entry points to the loop', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2', 'step3'], []),
|
||||
createCodeMockStep('step2', ['step4']),
|
||||
createCodeMockStep('step3', ['step4']),
|
||||
createCodeMockStep('step4', ['iterator1']), // loops back
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2', 'step3'], []),
|
||||
createMockCodeStep('step2', ['step4']),
|
||||
createMockCodeStep('step3', ['step4']),
|
||||
createMockCodeStep('step4', ['iterator1']), // loops back
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -151,13 +79,13 @@ describe('getAllStepIdsInLoop', () => {
|
||||
|
||||
describe('nested iterator scenarios', () => {
|
||||
it('should handle a nested iterator within a loop', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['nested_iterator']),
|
||||
createIteratorMockStep('nested_iterator', ['step5'], ['step3']),
|
||||
createCodeMockStep('step3', ['step4']),
|
||||
createCodeMockStep('step4', ['nested_iterator']), // loops back to nested iterator
|
||||
createCodeMockStep('step5', ['iterator1']), // loops back to main iterator
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['nested_iterator']),
|
||||
createMockIteratorStep('nested_iterator', ['step5'], ['step3']),
|
||||
createMockCodeStep('step3', ['step4']),
|
||||
createMockCodeStep('step4', ['nested_iterator']), // loops back to nested iterator
|
||||
createMockCodeStep('step5', ['iterator1']), // loops back to main iterator
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -176,15 +104,15 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle multiple levels of nested iterators', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['nested_iterator1']),
|
||||
createIteratorMockStep('nested_iterator1', ['step6'], ['step3']),
|
||||
createCodeMockStep('step3', ['nested_iterator2']),
|
||||
createIteratorMockStep('nested_iterator2', ['step5'], ['step4']),
|
||||
createCodeMockStep('step4', ['nested_iterator2']), // loops back to nested iterator2
|
||||
createCodeMockStep('step5', ['nested_iterator1']), // loops back to nested iterator1
|
||||
createCodeMockStep('step6', ['iterator1']), // loops back to main iterator
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['nested_iterator1']),
|
||||
createMockIteratorStep('nested_iterator1', ['step6'], ['step3']),
|
||||
createMockCodeStep('step3', ['nested_iterator2']),
|
||||
createMockIteratorStep('nested_iterator2', ['step5'], ['step4']),
|
||||
createMockCodeStep('step4', ['nested_iterator2']), // loops back to nested iterator2
|
||||
createMockCodeStep('step5', ['nested_iterator1']), // loops back to nested iterator1
|
||||
createMockCodeStep('step6', ['iterator1']), // loops back to main iterator
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -207,14 +135,14 @@ describe('getAllStepIdsInLoop', () => {
|
||||
|
||||
describe('if-else scenarios', () => {
|
||||
it('should include steps in all if-else branches within a loop', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', [], ['ifElse1']),
|
||||
createIfElseMockStep('ifElse1', [
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', [], ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{ id: 'branch-if', filterGroupId: 'fg1', nextStepIds: ['stepA'] },
|
||||
{ id: 'branch-else', nextStepIds: ['stepB'] },
|
||||
]),
|
||||
createCodeMockStep('stepA', ['iterator1']),
|
||||
createCodeMockStep('stepB', ['iterator1']),
|
||||
createMockCodeStep('stepA', ['iterator1']),
|
||||
createMockCodeStep('stepB', ['iterator1']),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -230,15 +158,15 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should include deeply nested steps inside if-else branches', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', [], ['ifElse1']),
|
||||
createIfElseMockStep('ifElse1', [
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', [], ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{ id: 'branch-if', filterGroupId: 'fg1', nextStepIds: ['stepA'] },
|
||||
{ id: 'branch-else', nextStepIds: ['stepB'] },
|
||||
]),
|
||||
createCodeMockStep('stepA', ['stepC']),
|
||||
createCodeMockStep('stepB', ['stepC']),
|
||||
createCodeMockStep('stepC', ['iterator1']),
|
||||
createMockCodeStep('stepA', ['stepC']),
|
||||
createMockCodeStep('stepB', ['stepC']),
|
||||
createMockCodeStep('stepC', ['iterator1']),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -254,16 +182,16 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle if-else with steps before and after within a loop', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', [], ['step1']),
|
||||
createCodeMockStep('step1', ['ifElse1']),
|
||||
createIfElseMockStep('ifElse1', [
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', [], ['step1']),
|
||||
createMockCodeStep('step1', ['ifElse1']),
|
||||
createMockIfElseStep('ifElse1', [
|
||||
{ id: 'branch-if', filterGroupId: 'fg1', nextStepIds: ['stepA'] },
|
||||
{ id: 'branch-else', nextStepIds: ['stepB'] },
|
||||
]),
|
||||
createCodeMockStep('stepA', ['step2']),
|
||||
createCodeMockStep('stepB', ['step2']),
|
||||
createCodeMockStep('step2', ['iterator1']),
|
||||
createMockCodeStep('stepA', ['step2']),
|
||||
createMockCodeStep('stepB', ['step2']),
|
||||
createMockCodeStep('step2', ['iterator1']),
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -281,9 +209,7 @@ describe('getAllStepIdsInLoop', () => {
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty initial loop step IDs', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
];
|
||||
const steps = [createMockIteratorStep('iterator1', ['step2'], [])];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
iteratorStepId: 'iterator1',
|
||||
@@ -295,9 +221,9 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should handle steps with no nextStepIds', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', []), // no nextStepIds
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', []), // no nextStepIds
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
@@ -310,11 +236,11 @@ describe('getAllStepIdsInLoop', () => {
|
||||
});
|
||||
|
||||
it('should prevent infinite loops with circular references', () => {
|
||||
const steps: WorkflowAction[] = [
|
||||
createIteratorMockStep('iterator1', ['step2'], []),
|
||||
createCodeMockStep('step2', ['step3']),
|
||||
createCodeMockStep('step3', ['step4']),
|
||||
createCodeMockStep('step4', ['step2']), // circular reference
|
||||
const steps = [
|
||||
createMockIteratorStep('iterator1', ['step2'], []),
|
||||
createMockCodeStep('step2', ['step3']),
|
||||
createMockCodeStep('step3', ['step4']),
|
||||
createMockCodeStep('step4', ['step2']), // circular reference
|
||||
];
|
||||
|
||||
const result = getAllStepIdsInLoop({
|
||||
|
||||
+86
-121
@@ -1,13 +1,15 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
createMockCodeStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { shouldExecuteIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-execute-iterator-step.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
// Mock the getAllStepIdsInLoop utility
|
||||
jest.mock(
|
||||
'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util',
|
||||
() => ({
|
||||
@@ -20,64 +22,21 @@ const { getAllStepIdsInLoop } = jest.requireMock(
|
||||
);
|
||||
|
||||
describe('shouldExecuteIteratorStep', () => {
|
||||
const createMockIteratorStep = (
|
||||
id: string,
|
||||
initialLoopStepIds: string[] = [],
|
||||
): WorkflowIteratorAction => ({
|
||||
id,
|
||||
name: 'Iterator Step',
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds,
|
||||
items: [],
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
});
|
||||
|
||||
const createMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: 'Mock Step',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: 'mock-function-id',
|
||||
logicFunctionInput: {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when the step has not been started', () => {
|
||||
it('should return true when all parent steps are successful', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockStep('step-3', ['step-4']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-3', ['step-4']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -104,13 +63,14 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false when some parent steps not in loop have failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -130,13 +90,14 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false when some parent steps are not started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -156,10 +117,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return true even if loop step is not started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -180,10 +141,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return true when there are no parent steps targeting the iterator', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1');
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-2', []),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -203,9 +164,9 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should handle undefined steps gracefully', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
undefined as unknown as WorkflowAction,
|
||||
iteratorStep,
|
||||
];
|
||||
@@ -225,16 +186,16 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should work correctly with multiple steps targeting the same iterator', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
'step-3',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2', 'step-3'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockStep('step-3', ['iterator-1']),
|
||||
createMockStep('step-4', ['step-5']), // Not targeting iterator
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-3', ['iterator-1']),
|
||||
createMockCodeStep('step-4', ['step-5']), // Not targeting iterator
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -258,10 +219,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
|
||||
describe('when the step has been started', () => {
|
||||
it('should return true if all the steps targeting the iterator have been successful', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -282,10 +243,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false if some of the steps targeting the iterator have failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -306,10 +267,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false if some of the steps targeting the iterator are still running', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -330,8 +291,8 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return true when there are no steps targeting the iterator', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const steps = [createMockStep('step-1', ['step-2']), iteratorStep];
|
||||
const iteratorStep = createMockIteratorStep('iterator-1');
|
||||
const steps = [createMockCodeStep('step-1', ['step-2']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.RUNNING }, // Iterator has been started
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
@@ -349,10 +310,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should check all steps targeting iterator including loop steps when iterator has been started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop and targeting iterator
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop but targeting iterator
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop and targeting iterator
|
||||
createMockCodeStep('step-2', ['iterator-1']), // Not in loop but targeting iterator
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -373,10 +334,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false when loop step has NOT_STARTED status and iterator has been started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -399,10 +360,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty step info for parent steps', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -423,17 +384,17 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should work with complex loop structures', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
'step-3',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2', 'step-3'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // In loop
|
||||
createMockStep('step-3', ['iterator-1']), // In loop
|
||||
createMockStep('step-4', ['iterator-1']), // Not in loop
|
||||
createMockStep('step-5', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-3', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-4', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-5', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -456,15 +417,16 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
});
|
||||
|
||||
it('should return false with complex loop when one non-loop parent is not successful', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // In loop
|
||||
createMockStep('step-3', ['iterator-1']), // Not in loop
|
||||
createMockStep('step-4', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-3', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-4', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -501,7 +463,10 @@ describe('shouldExecuteIteratorStep', () => {
|
||||
},
|
||||
} as WorkflowIteratorAction;
|
||||
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
createMockCodeStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { shouldFailSafelyIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-fail-safely-iterator-step.util';
|
||||
|
||||
describe('shouldFailSafelyIteratorStep', () => {
|
||||
describe('unstarted iterator', () => {
|
||||
it('should return false when external parent is SUCCESS', () => {
|
||||
const parent = createMockCodeStep('parent', ['iterator1']);
|
||||
const iterator = createMockIteratorStep('iterator1', [], ['stepA'], true);
|
||||
|
||||
const stepA = createMockCodeStep('stepA', ['iterator1']);
|
||||
const steps = [parent, iterator, stepA];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent: { status: StepStatus.SUCCESS, result: {} },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when external parent is FAILED_SAFELY', () => {
|
||||
const parent = createMockCodeStep('parent', ['iterator1']);
|
||||
const iterator = createMockIteratorStep('iterator1', [], ['stepA'], true);
|
||||
const stepA = createMockCodeStep('stepA', ['iterator1']);
|
||||
const steps = [parent, iterator, stepA];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
parent: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no external parents', () => {
|
||||
const iterator = createMockIteratorStep('iterator1', [], ['stepA'], true);
|
||||
const stepA = createMockCodeStep('stepA', ['iterator1']);
|
||||
const steps = [iterator, stepA];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('started iterator with flag', () => {
|
||||
it('should return false when failure originated from own loop', () => {
|
||||
const iterator = createMockIteratorStep('iterator1', [], ['stepA'], true);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.FAILED_SAFELY, error: 'actual error' },
|
||||
stepB: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when loop-back parents have no FAILED_SAFELY', () => {
|
||||
const iterator = createMockIteratorStep('iterator1', [], ['stepA'], true);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.SUCCESS, result: {} },
|
||||
stepB: { status: StepStatus.SUCCESS, result: {} },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('started iterator without flag', () => {
|
||||
it('should return true when loop-back parent is FAILED_SAFELY', () => {
|
||||
const iterator = createMockIteratorStep(
|
||||
'iterator1',
|
||||
[],
|
||||
['stepA'],
|
||||
false,
|
||||
);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.FAILED_SAFELY, error: 'err' },
|
||||
stepB: { status: StepStatus.FAILED_SAFELY },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when loop-back parents are not all terminal', () => {
|
||||
const iterator = createMockIteratorStep(
|
||||
'iterator1',
|
||||
[],
|
||||
['stepA'],
|
||||
false,
|
||||
);
|
||||
const stepA = createMockCodeStep('stepA', ['stepB']);
|
||||
const stepB = createMockCodeStep('stepB', ['iterator1']);
|
||||
const steps = [iterator, stepA, stepB];
|
||||
|
||||
const result = shouldFailSafelyIteratorStep({
|
||||
step: iterator,
|
||||
steps,
|
||||
stepInfos: {
|
||||
iterator1: { status: StepStatus.RUNNING },
|
||||
stepA: { status: StepStatus.FAILED_SAFELY, error: 'err' },
|
||||
stepB: { status: StepStatus.RUNNING },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+74
-100
@@ -1,13 +1,15 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
createMockCodeStep,
|
||||
createMockIteratorStep,
|
||||
} from 'src/modules/workflow/workflow-executor/utils/create-mock-workflow-steps.util';
|
||||
import { shouldSkipIteratorStepExecution } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-skip-iterator-step-execution.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
// Mock the getAllStepIdsInLoop utility
|
||||
jest.mock(
|
||||
'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util',
|
||||
() => ({
|
||||
@@ -20,60 +22,16 @@ const { getAllStepIdsInLoop } = jest.requireMock(
|
||||
);
|
||||
|
||||
describe('shouldSkipIteratorStepExecution', () => {
|
||||
const createMockIteratorStep = (
|
||||
id: string,
|
||||
initialLoopStepIds: string[] = [],
|
||||
): WorkflowIteratorAction => ({
|
||||
id,
|
||||
name: 'Iterator Step',
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds,
|
||||
items: [],
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
});
|
||||
|
||||
const createMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: 'Mock Step',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: 'mock-function-id',
|
||||
logicFunctionInput: {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when the iterator has not been started', () => {
|
||||
it('should return true when all parent steps are skipped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -93,10 +51,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return true when all parent steps are stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -116,10 +74,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return true when parent steps are mix of skipped and stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -139,10 +97,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is successful', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -162,10 +120,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -185,10 +143,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is not started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -208,10 +166,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should only check parent steps not in loop', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -231,10 +189,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when there are no parent steps', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1');
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockCodeStep('step-1', ['step-2']),
|
||||
createMockCodeStep('step-2', []),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -254,11 +212,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should handle undefined steps gracefully', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
undefined as unknown as WorkflowAction,
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -278,15 +236,16 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should work correctly with multiple steps targeting the same iterator', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [
|
||||
'step-1',
|
||||
'step-2',
|
||||
]);
|
||||
const iteratorStep = createMockIteratorStep(
|
||||
'iterator-1',
|
||||
[],
|
||||
['step-1', 'step-2'],
|
||||
);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // In loop
|
||||
createMockStep('step-3', ['iterator-1']), // Not in loop
|
||||
createMockStep('step-4', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-3', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-4', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -308,11 +267,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when one non-loop parent is not skipped/stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
createMockStep('step-3', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-1', ['iterator-1']), // In loop
|
||||
createMockCodeStep('step-2', ['iterator-1']), // Not in loop
|
||||
createMockCodeStep('step-3', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -335,10 +294,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
|
||||
describe('when the iterator has been started', () => {
|
||||
it('should return false when iterator has been started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -359,10 +318,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false even when all parent steps are skipped/stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
createMockCodeStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
@@ -383,8 +342,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when iterator is pending', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.PENDING },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -402,8 +364,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when iterator is skipped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.SKIPPED },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -421,8 +386,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should return false when iterator failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', [], ['step-1']);
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.FAILED },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
@@ -457,7 +425,10 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
},
|
||||
} as WorkflowIteratorAction;
|
||||
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
@@ -475,8 +446,11 @@ describe('shouldSkipIteratorStepExecution', () => {
|
||||
});
|
||||
|
||||
it('should handle empty initialLoopStepIds array', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const iteratorStep = createMockIteratorStep('iterator-1');
|
||||
const steps = [
|
||||
createMockCodeStep('step-1', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const findEnclosingIteratorWithContinueOnFailure = ({
|
||||
failedStepId,
|
||||
steps,
|
||||
}: {
|
||||
failedStepId: string;
|
||||
steps: WorkflowAction[];
|
||||
}): WorkflowIteratorAction | undefined => {
|
||||
const iteratorSteps = steps.filter(isWorkflowIteratorAction);
|
||||
|
||||
const candidates = iteratorSteps
|
||||
.filter(
|
||||
(iterator) =>
|
||||
iterator.settings.input.initialLoopStepIds &&
|
||||
iterator.settings.input.initialLoopStepIds.length > 0,
|
||||
)
|
||||
.map((iterator) => ({
|
||||
iterator,
|
||||
loopStepIds: getAllStepIdsInLoop({
|
||||
iteratorStepId: iterator.id,
|
||||
initialLoopStepIds: iterator.settings.input.initialLoopStepIds!,
|
||||
steps,
|
||||
}),
|
||||
}))
|
||||
.filter(({ loopStepIds }) => loopStepIds.includes(failedStepId))
|
||||
.sort((a, b) => a.loopStepIds.length - b.loopStepIds.length);
|
||||
|
||||
return candidates.find(
|
||||
({ iterator }) =>
|
||||
iterator.settings.input.shouldContinueOnIterationFailure === true,
|
||||
)?.iterator;
|
||||
};
|
||||
+25
-2
@@ -1,6 +1,7 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { TERMINAL_STEP_STATUSES } from 'src/modules/workflow/workflow-executor/constants/terminal-step-statuses.constant';
|
||||
import { shouldExecuteChildStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-child-step.util';
|
||||
import { stepHasBeenStarted } from 'src/modules/workflow/workflow-executor/utils/step-has-been-started.util';
|
||||
import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util';
|
||||
@@ -34,13 +35,35 @@ export const shouldExecuteIteratorStep = ({
|
||||
: [];
|
||||
|
||||
const parentSteps = stepsTargetingIterator.filter(
|
||||
(step) => !stepIdsInLoop.includes(step.id),
|
||||
(parentStep) => !stepIdsInLoop.includes(parentStep.id),
|
||||
);
|
||||
|
||||
const stepsToCheck = stepHasBeenStarted(step.id, stepInfos)
|
||||
? stepsTargetingIterator
|
||||
: parentSteps;
|
||||
|
||||
// When iterator has been started and has the continue-on-failure flag,
|
||||
// allow re-execution even if all loop-back parents are FAILED_SAFELY/SKIPPED
|
||||
// (i.e. don't require at least one SUCCESS parent)
|
||||
if (
|
||||
stepHasBeenStarted(step.id, stepInfos) &&
|
||||
step.settings.input.shouldContinueOnIterationFailure
|
||||
) {
|
||||
const hasFailureFromOwnLoop = stepIdsInLoop.some(
|
||||
(loopStepId) =>
|
||||
stepInfos[loopStepId]?.status === StepStatus.FAILED_SAFELY &&
|
||||
isDefined(stepInfos[loopStepId]?.error),
|
||||
);
|
||||
|
||||
if (hasFailureFromOwnLoop) {
|
||||
const areAllParentsTerminal = stepsToCheck.every((parentStep) =>
|
||||
TERMINAL_STEP_STATUSES.includes(stepInfos[parentStep.id]?.status),
|
||||
);
|
||||
|
||||
return areAllParentsTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
return shouldExecuteChildStep({
|
||||
parentSteps: stepsToCheck,
|
||||
stepInfos,
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { TERMINAL_STEP_STATUSES } from 'src/modules/workflow/workflow-executor/constants/terminal-step-statuses.constant';
|
||||
import { stepHasBeenStarted } from 'src/modules/workflow/workflow-executor/utils/step-has-been-started.util';
|
||||
import { getAllStepIdsInLoop } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const shouldFailSafelyIteratorStep = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
step: WorkflowIteratorAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
}): boolean => {
|
||||
const stepsTargetingIterator = steps.filter(
|
||||
(parentStep) =>
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(step.id),
|
||||
);
|
||||
|
||||
const initialLoopStepIds = step.settings.input.initialLoopStepIds;
|
||||
|
||||
const stepIdsInLoop = isDefined(initialLoopStepIds)
|
||||
? getAllStepIdsInLoop({
|
||||
iteratorStepId: step.id,
|
||||
initialLoopStepIds,
|
||||
steps,
|
||||
})
|
||||
: [];
|
||||
|
||||
const externalParentSteps = stepsTargetingIterator.filter(
|
||||
(parentStep) => !stepIdsInLoop.includes(parentStep.id),
|
||||
);
|
||||
|
||||
if (!stepHasBeenStarted(step.id, stepInfos)) {
|
||||
if (externalParentSteps.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const areAllExternalParentsTerminal = externalParentSteps.every(
|
||||
(parentStep) =>
|
||||
TERMINAL_STEP_STATUSES.includes(stepInfos[parentStep.id]?.status),
|
||||
);
|
||||
|
||||
const hasFailedSafelyExternalParent = externalParentSteps.some(
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED_SAFELY,
|
||||
);
|
||||
|
||||
return areAllExternalParentsTerminal && hasFailedSafelyExternalParent;
|
||||
}
|
||||
|
||||
const areAllParentsTerminal = stepsTargetingIterator.every((parentStep) =>
|
||||
TERMINAL_STEP_STATUSES.includes(stepInfos[parentStep.id]?.status),
|
||||
);
|
||||
|
||||
if (!areAllParentsTerminal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasFailedSafelyParent = stepsTargetingIterator.some(
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED_SAFELY,
|
||||
);
|
||||
|
||||
if (!hasFailedSafelyParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the iterator has the flag, check whether the failure originated from
|
||||
// its own loop. If yes, the iterator handles it (re-execute) — not fail safely.
|
||||
if (step.settings.input.shouldContinueOnIterationFailure) {
|
||||
const hasFailureFromOwnLoop = stepIdsInLoop.some(
|
||||
(loopStepId) =>
|
||||
stepInfos[loopStepId]?.status === StepStatus.FAILED_SAFELY &&
|
||||
isDefined(stepInfos[loopStepId]?.error),
|
||||
);
|
||||
|
||||
if (hasFailureFromOwnLoop) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
+4
-3
@@ -41,8 +41,9 @@ export const shouldSkipIteratorStepExecution = ({
|
||||
}
|
||||
|
||||
return parentSteps.every(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[step.id]?.status === StepStatus.STOPPED,
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.STOPPED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED_SAFELY,
|
||||
);
|
||||
};
|
||||
|
||||
+96
@@ -486,6 +486,102 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nextStepIds for a fail-safe iterator instead of entering the loop', async () => {
|
||||
const step = {
|
||||
id: 'iterator-1',
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
nextStepIds: ['after-loop'],
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['loop-step-1'],
|
||||
},
|
||||
},
|
||||
} as WorkflowAction;
|
||||
|
||||
const result = await service.getNextStepIdsToExecute({
|
||||
executedStep: step,
|
||||
executedStepOutput: {
|
||||
shouldFailSafely: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextStepIdsToExecute: ['after-loop'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nextStepIdsToFailSafely for all branches when if-else is fail-safe', async () => {
|
||||
const step = {
|
||||
id: 'if-else-1',
|
||||
type: WorkflowActionType.IF_ELSE,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
branches: [
|
||||
{
|
||||
id: 'branch-if',
|
||||
filterGroupId: 'fg1',
|
||||
nextStepIds: ['step-a'],
|
||||
},
|
||||
{
|
||||
id: 'branch-else',
|
||||
nextStepIds: ['step-b'],
|
||||
},
|
||||
],
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
},
|
||||
},
|
||||
} as unknown as WorkflowAction;
|
||||
|
||||
const result = await service.getNextStepIdsToExecute({
|
||||
executedStep: step,
|
||||
executedStepOutput: {
|
||||
shouldFailSafely: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextStepIdsToFailSafely: ['step-a', 'step-b'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nextStepIdsToSkip for all branches when if-else has no matching branch', async () => {
|
||||
const step = {
|
||||
id: 'if-else-1',
|
||||
type: WorkflowActionType.IF_ELSE,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
branches: [
|
||||
{
|
||||
id: 'branch-if',
|
||||
filterGroupId: 'fg1',
|
||||
nextStepIds: ['step-a'],
|
||||
},
|
||||
{
|
||||
id: 'branch-else',
|
||||
nextStepIds: ['step-b'],
|
||||
},
|
||||
],
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
},
|
||||
},
|
||||
} as unknown as WorkflowAction;
|
||||
|
||||
const result = await service.getNextStepIdsToExecute({
|
||||
executedStep: step,
|
||||
executedStepOutput: {
|
||||
shouldSkipStepExecution: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextStepIdsToSkip: ['step-a', 'step-b'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip multiple non-matching branches for if-else with many branches', async () => {
|
||||
const step = {
|
||||
id: 'if-else-1',
|
||||
|
||||
+92
-32
@@ -28,6 +28,7 @@ import {
|
||||
type WorkflowExecutorInput,
|
||||
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util';
|
||||
import { shouldFailSafely } from 'src/modules/workflow/workflow-executor/utils/should-fail-safely.util';
|
||||
import { shouldSkipStepExecution } from 'src/modules/workflow/workflow-executor/utils/should-skip-step-execution.util';
|
||||
import { workflowShouldFail } from 'src/modules/workflow/workflow-executor/utils/workflow-should-fail.util';
|
||||
import { workflowShouldKeepRunning } from 'src/modules/workflow/workflow-executor/utils/workflow-should-keep-running.util';
|
||||
@@ -35,6 +36,7 @@ import { isWorkflowIfElseAction } from 'src/modules/workflow/workflow-executor/w
|
||||
import { type WorkflowIfElseResult } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-result.type';
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { WorkflowIteratorResult } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-result.type';
|
||||
import { findEnclosingIteratorWithContinueOnFailure } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/find-enclosing-iterator-with-continue-on-failure.util';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { RUN_WORKFLOW_JOB_NAME } from 'src/modules/workflow/workflow-runner/constants/run-workflow-job-name';
|
||||
import { type RunWorkflowJobData } from 'src/modules/workflow/workflow-runner/types/run-workflow-job-data.type';
|
||||
@@ -125,6 +127,27 @@ export class WorkflowExecutorWorkspaceService {
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (isDefined(actionOutput.error)) {
|
||||
const enclosingIterator = findEnclosingIteratorWithContinueOnFailure({
|
||||
failedStepId: stepId,
|
||||
steps,
|
||||
});
|
||||
|
||||
if (isDefined(enclosingIterator)) {
|
||||
actionOutput.shouldFailSafely = true;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
shouldFailSafely({
|
||||
step: stepToExecute,
|
||||
steps,
|
||||
stepInfos,
|
||||
})
|
||||
) {
|
||||
actionOutput = {
|
||||
shouldFailSafely: true,
|
||||
};
|
||||
} else if (
|
||||
shouldSkipStepExecution({
|
||||
step: stepToExecute,
|
||||
@@ -139,9 +162,10 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
const isError = isDefined(actionOutput.error);
|
||||
const isError =
|
||||
isDefined(actionOutput.error) && !actionOutput.shouldFailSafely;
|
||||
|
||||
if (!isError) {
|
||||
if (!isError && !actionOutput.shouldFailSafely) {
|
||||
this.sendWorkflowNodeRunEvent(workspaceId, workflowRun.workflowId);
|
||||
}
|
||||
|
||||
@@ -169,15 +193,16 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
const { nextStepIdsToExecute, nextStepIdsToSkip } =
|
||||
const { nextStepIdsToExecute, nextStepIdsToSkip, nextStepIdsToFailSafely } =
|
||||
await this.getNextStepIdsToExecute({
|
||||
executedStep: stepToExecute,
|
||||
executedStepOutput: actionOutput,
|
||||
});
|
||||
|
||||
if (isDefined(nextStepIdsToSkip) && nextStepIdsToSkip.length > 0) {
|
||||
await this.skipStepsAndContinue({
|
||||
stepIdsToSkip: nextStepIdsToSkip,
|
||||
if (isDefined(nextStepIdsToSkip) || isDefined(nextStepIdsToFailSafely)) {
|
||||
await this.skipAndFailSafelyStepsThenContinue({
|
||||
stepIdsToSkip: nextStepIdsToSkip ?? [],
|
||||
stepIdsToFailSafely: nextStepIdsToFailSafely ?? [],
|
||||
steps,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
@@ -205,6 +230,7 @@ export class WorkflowExecutorWorkspaceService {
|
||||
}): Promise<{
|
||||
nextStepIdsToExecute?: string[];
|
||||
nextStepIdsToSkip?: string[];
|
||||
nextStepIdsToFailSafely?: string[];
|
||||
}> {
|
||||
const isIteratorStep = isWorkflowIteratorAction(executedStep);
|
||||
|
||||
@@ -213,7 +239,10 @@ export class WorkflowExecutorWorkspaceService {
|
||||
| WorkflowIteratorResult
|
||||
| undefined;
|
||||
|
||||
if (!iteratorStepResult?.hasProcessedAllItems) {
|
||||
if (
|
||||
!iteratorStepResult?.hasProcessedAllItems &&
|
||||
!executedStepOutput.shouldFailSafely
|
||||
) {
|
||||
const nextStepIdsToExecute = isString(
|
||||
executedStep.settings.input.initialLoopStepIds,
|
||||
)
|
||||
@@ -229,9 +258,9 @@ export class WorkflowExecutorWorkspaceService {
|
||||
| WorkflowIfElseResult
|
||||
| undefined;
|
||||
|
||||
if (ifElseResult?.matchingBranchId) {
|
||||
const branches = executedStep.settings.input.branches;
|
||||
const branches = executedStep.settings.input.branches;
|
||||
|
||||
if (ifElseResult?.matchingBranchId) {
|
||||
const matchingBranch = branches.find(
|
||||
(branch) => branch.id === ifElseResult.matchingBranchId,
|
||||
);
|
||||
@@ -246,6 +275,16 @@ export class WorkflowExecutorWorkspaceService {
|
||||
(branch) => branch.nextStepIds,
|
||||
),
|
||||
};
|
||||
} else if (executedStepOutput.shouldFailSafely) {
|
||||
return {
|
||||
nextStepIdsToFailSafely: branches.flatMap(
|
||||
(branch) => branch.nextStepIds,
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
nextStepIdsToSkip: branches.flatMap((branch) => branch.nextStepIds),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +386,7 @@ export class WorkflowExecutorWorkspaceService {
|
||||
const isStopped = actionOutput.shouldEndWorkflowRun ?? false;
|
||||
const isNotFinished = actionOutput.shouldRemainRunning ?? false;
|
||||
const isSkipped = actionOutput.shouldSkipStepExecution ?? false;
|
||||
const isFailedSafely = actionOutput.shouldFailSafely ?? false;
|
||||
|
||||
let stepInfo: WorkflowRunStepInfo;
|
||||
|
||||
@@ -364,6 +404,11 @@ export class WorkflowExecutorWorkspaceService {
|
||||
status: StepStatus.RUNNING,
|
||||
result: actionOutput?.result,
|
||||
};
|
||||
} else if (isFailedSafely) {
|
||||
stepInfo = {
|
||||
status: StepStatus.FAILED_SAFELY,
|
||||
error: actionOutput?.error,
|
||||
};
|
||||
} else if (isSuccess) {
|
||||
stepInfo = {
|
||||
status: StepStatus.SUCCESS,
|
||||
@@ -388,7 +433,8 @@ export class WorkflowExecutorWorkspaceService {
|
||||
});
|
||||
|
||||
return {
|
||||
shouldProcessNextSteps: isSuccess || isStopped || isSkipped,
|
||||
shouldProcessNextSteps:
|
||||
isSuccess || isStopped || isSkipped || isFailedSafely,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -444,42 +490,56 @@ export class WorkflowExecutorWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
private async skipStepsAndContinue({
|
||||
async skipAndFailSafelyStepsThenContinue({
|
||||
stepIdsToSkip,
|
||||
stepIdsToFailSafely,
|
||||
steps,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
executedStepsCount,
|
||||
}: {
|
||||
stepIdsToSkip: string[];
|
||||
stepIdsToFailSafely: string[];
|
||||
steps: WorkflowAction[];
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
executedStepsCount: number;
|
||||
}) {
|
||||
await Promise.all(
|
||||
stepIdsToSkip.map(async (stepId) => {
|
||||
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||
stepId,
|
||||
stepInfo: { status: StepStatus.SKIPPED },
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
const stepInfos: Record<string, WorkflowRunStepInfo> = {};
|
||||
|
||||
const skippedStep = steps.find((step) => step.id === stepId);
|
||||
const skippedStepNextStepIds = skippedStep?.nextStepIds ?? [];
|
||||
for (const stepId of stepIdsToSkip) {
|
||||
stepInfos[stepId] = { status: StepStatus.SKIPPED };
|
||||
}
|
||||
|
||||
if (skippedStepNextStepIds.length > 0) {
|
||||
await this.executeFromSteps({
|
||||
stepIds: skippedStepNextStepIds,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
shouldComputeWorkflowRunStatus: false,
|
||||
executedStepsCount,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const stepId of stepIdsToFailSafely) {
|
||||
stepInfos[stepId] = { status: StepStatus.FAILED_SAFELY };
|
||||
}
|
||||
|
||||
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfos({
|
||||
stepInfos,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const nextStepIds = new Set<string>();
|
||||
|
||||
for (const stepId of [...stepIdsToSkip, ...stepIdsToFailSafely]) {
|
||||
const step = steps.find((step) => step.id === stepId);
|
||||
|
||||
for (const nextStepId of step?.nextStepIds ?? []) {
|
||||
nextStepIds.add(nextStepId);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStepIds.size > 0) {
|
||||
await this.executeFromSteps({
|
||||
stepIds: Array.from(nextStepIds),
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
shouldComputeWorkflowRunStatus: false,
|
||||
executedStepsCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async continueExecutionFromStepInAnotherJob({
|
||||
|
||||
+30
-7
@@ -156,13 +156,19 @@ export class RunWorkflowJob {
|
||||
const lastExecutedStepOutput =
|
||||
workflowRun.state?.stepInfos[lastExecutedStepId];
|
||||
|
||||
const { nextStepIdsToExecute } =
|
||||
const { nextStepIdsToExecute, nextStepIdsToSkip, nextStepIdsToFailSafely } =
|
||||
await this.workflowExecutorWorkspaceService.getNextStepIdsToExecute({
|
||||
executedStep: lastExecutedStep,
|
||||
executedStepOutput: lastExecutedStepOutput,
|
||||
});
|
||||
|
||||
if (!isDefined(nextStepIdsToExecute) || nextStepIdsToExecute.length === 0) {
|
||||
const hasStepsToSkipOrFailSafely =
|
||||
isDefined(nextStepIdsToSkip) || isDefined(nextStepIdsToFailSafely);
|
||||
|
||||
const hasStepsToExecute =
|
||||
isDefined(nextStepIdsToExecute) && nextStepIdsToExecute.length > 0;
|
||||
|
||||
if (!hasStepsToSkipOrFailSafely && !hasStepsToExecute) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
@@ -172,11 +178,28 @@ export class RunWorkflowJob {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workflowExecutorWorkspaceService.executeFromSteps({
|
||||
stepIds: nextStepIdsToExecute,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
const steps = workflowRun.state?.flow?.steps ?? [];
|
||||
|
||||
if (hasStepsToSkipOrFailSafely) {
|
||||
await this.workflowExecutorWorkspaceService.skipAndFailSafelyStepsThenContinue(
|
||||
{
|
||||
stepIdsToSkip: nextStepIdsToSkip ?? [],
|
||||
stepIdsToFailSafely: nextStepIdsToFailSafely ?? [],
|
||||
steps,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
executedStepsCount: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (hasStepsToExecute) {
|
||||
await this.workflowExecutorWorkspaceService.executeFromSteps({
|
||||
stepIds: nextStepIdsToExecute,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async incrementTriggerMetrics({
|
||||
|
||||
+12
-4
@@ -277,13 +277,21 @@ export class WorkflowRunWorkspaceService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const existingStepInfos = workflowRunToUpdate.state?.stepInfos ?? {};
|
||||
|
||||
const mergedStepInfos = { ...existingStepInfos };
|
||||
|
||||
for (const [stepId, info] of Object.entries(stepInfos)) {
|
||||
mergedStepInfos[stepId] = {
|
||||
...(existingStepInfos[stepId] || {}),
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
const partialUpdate = {
|
||||
state: {
|
||||
...workflowRunToUpdate.state,
|
||||
stepInfos: {
|
||||
...workflowRunToUpdate.state?.stepInfos,
|
||||
...stepInfos,
|
||||
},
|
||||
stepInfos: mergedStepInfos,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -20,5 +20,6 @@ export const workflowIteratorActionSettingsSchema =
|
||||
])
|
||||
.optional(),
|
||||
initialLoopStepIds: z.array(z.string()).optional(),
|
||||
shouldContinueOnIterationFailure: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum StepStatus {
|
||||
SUCCESS = 'SUCCESS',
|
||||
STOPPED = 'STOPPED',
|
||||
FAILED = 'FAILED',
|
||||
FAILED_SAFELY = 'FAILED_SAFELY',
|
||||
PENDING = 'PENDING',
|
||||
SKIPPED = 'SKIPPED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user