Enable filters in iterators (#15017)
Filters should not cut the whole workflow. These should only stop the branch. This PR: - adds a new skipped status - when a filter stops, it still goes to the next step - the next step will execute if there is at least a successful step - if only skipped step, it will be skipped as well It allows to use filters in iterators. https://github.com/user-attachments/assets/1cfca052-55c0-4ce5-9eb8-63736618d082
This commit is contained in:
+1
@@ -4,4 +4,5 @@ export type WorkflowActionOutput = {
|
||||
pendingEvent?: boolean;
|
||||
shouldEndWorkflowRun?: boolean;
|
||||
shouldRemainRunning?: boolean;
|
||||
shouldSkipStepExecution?: boolean;
|
||||
};
|
||||
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
|
||||
describe('canExecuteStep', () => {
|
||||
const steps = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
it('should return true if all parents succeeded', () => {
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = canExecuteStep({
|
||||
stepInfos,
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if one parent is not succeeded', () => {
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if step has already ran', () => {
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.PENDING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
canExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if workflowRun is not RUNNING', () => {
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
for (const workflowRunStatus of [
|
||||
WorkflowRunStatus.FAILED,
|
||||
WorkflowRunStatus.ENQUEUED,
|
||||
WorkflowRunStatus.COMPLETED,
|
||||
WorkflowRunStatus.NOT_STARTED,
|
||||
]) {
|
||||
const result = canExecuteStep({
|
||||
stepInfos,
|
||||
steps,
|
||||
stepId: 'step-3',
|
||||
workflowRunStatus,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
describe('shouldExecuteStep', () => {
|
||||
const steps = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
it('should return true if all parents succeeded', () => {
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos,
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if one parent is not succeeded', () => {
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if step has already ran', () => {
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.PENDING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if workflowRun is not RUNNING', () => {
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
for (const workflowRunStatus of [
|
||||
WorkflowRunStatus.FAILED,
|
||||
WorkflowRunStatus.ENQUEUED,
|
||||
WorkflowRunStatus.COMPLETED,
|
||||
WorkflowRunStatus.NOT_STARTED,
|
||||
]) {
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos,
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return true when step has no parent steps', () => {
|
||||
const stepsWithoutParents = [
|
||||
{
|
||||
id: 'step-1',
|
||||
name: 'Mock Step',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'mock-function-id',
|
||||
serverlessFunctionVersion: 'mock-function-version',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos,
|
||||
steps: stepsWithoutParents,
|
||||
step: stepsWithoutParents[0],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when one parent is RUNNING', () => {
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined parent steps gracefully', () => {
|
||||
const stepsWithUndefined = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-3'],
|
||||
},
|
||||
undefined as unknown as WorkflowAction,
|
||||
{
|
||||
id: 'step-3',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
const stepInfos = {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos,
|
||||
steps: stepsWithUndefined,
|
||||
step: stepsWithUndefined[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when at least one parent is FAILED', () => {
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.FAILED,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.NOT_STARTED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work correctly with multiple successful parents', () => {
|
||||
const multiParentSteps = [
|
||||
{
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-4'],
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-4'],
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: ['step-4'],
|
||||
},
|
||||
{
|
||||
id: 'step-4',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
nextStepIds: [],
|
||||
},
|
||||
] as WorkflowAction[];
|
||||
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
'step-3': { status: StepStatus.SUCCESS },
|
||||
'step-4': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos,
|
||||
steps: multiParentSteps,
|
||||
step: multiParentSteps[3],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when step status is SKIPPED', () => {
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.SKIPPED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when step status is STOPPED', () => {
|
||||
const result = shouldExecuteStep({
|
||||
stepInfos: {
|
||||
'step-1': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-2': {
|
||||
status: StepStatus.SUCCESS,
|
||||
},
|
||||
'step-3': {
|
||||
status: StepStatus.STOPPED,
|
||||
},
|
||||
},
|
||||
steps,
|
||||
step: steps[2],
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
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';
|
||||
|
||||
describe('shouldSkipStepExecution', () => {
|
||||
const createMockStep = (
|
||||
id: string,
|
||||
nextStepIds: string[] = [],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: 'Mock Step',
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'mock-function-id',
|
||||
serverlessFunctionVersion: 'mock-function-version',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.STOPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.FAILED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.RUNNING },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.NOT_STARTED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there are no parent steps', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
createMockStep('step-3', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when single parent step is skipped', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[1],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle undefined steps gracefully', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
undefined as unknown as WorkflowAction,
|
||||
createMockStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[2],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when parent step status is pending', () => {
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.PENDING },
|
||||
'step-2': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
const result = shouldSkipStepExecution({
|
||||
step: steps[1],
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
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', []),
|
||||
];
|
||||
|
||||
// Test case 1: All skipped - should return true
|
||||
expect(
|
||||
shouldSkipStepExecution({
|
||||
step: steps[3],
|
||||
steps,
|
||||
stepInfos: {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
'step-3': { status: StepStatus.SKIPPED },
|
||||
'step-4': { status: StepStatus.NOT_STARTED },
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
// Test case 2: Mixed skipped/stopped - should return true
|
||||
expect(
|
||||
shouldSkipStepExecution({
|
||||
step: steps[3],
|
||||
steps,
|
||||
stepInfos: {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
'step-3': { status: StepStatus.SKIPPED },
|
||||
'step-4': { status: StepStatus.NOT_STARTED },
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
// Test case 3: One success among skipped - should return false
|
||||
expect(
|
||||
shouldSkipStepExecution({
|
||||
step: steps[3],
|
||||
steps,
|
||||
stepInfos: {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
'step-3': { status: StepStatus.SKIPPED },
|
||||
'step-4': { status: StepStatus.NOT_STARTED },
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
// Test case 4: One failed among skipped - should return false
|
||||
expect(
|
||||
shouldSkipStepExecution({
|
||||
step: steps[3],
|
||||
steps,
|
||||
stepInfos: {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.FAILED },
|
||||
'step-3': { status: StepStatus.SKIPPED },
|
||||
'step-4': { status: StepStatus.NOT_STARTED },
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
+21
-14
@@ -4,48 +4,55 @@ import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { stepHasBeenStarted } from 'src/modules/workflow/workflow-executor/utils/step-has-been-started.util';
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { canExecuteIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/can-execute-iterator-step.util';
|
||||
import { shouldExecuteIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-execute-iterator-step.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const canExecuteStep = ({
|
||||
stepId,
|
||||
export const shouldExecuteStep = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
workflowRunStatus,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
stepId: string;
|
||||
workflowRunStatus: WorkflowRunStatus;
|
||||
}) => {
|
||||
if (workflowRunStatus !== WorkflowRunStatus.RUNNING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const step = steps.find((step) => step.id === stepId);
|
||||
|
||||
if (!step) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isWorkflowIteratorAction(step)) {
|
||||
return canExecuteIteratorStep({
|
||||
return shouldExecuteIteratorStep({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
}
|
||||
|
||||
if (stepHasBeenStarted(stepId, stepInfos)) {
|
||||
if (stepHasBeenStarted(step.id, stepInfos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentSteps = steps.filter(
|
||||
(parentStep) =>
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(stepId),
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(step.id),
|
||||
);
|
||||
|
||||
return parentSteps.every(
|
||||
if (parentSteps.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasSuccessfulParentStep = parentSteps.some(
|
||||
(parentStep) => stepInfos[parentStep.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
|
||||
const areAllParentsCompleted = parentSteps.every(
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.SUCCESS ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.STOPPED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.SKIPPED,
|
||||
);
|
||||
|
||||
return hasSuccessfulParentStep && areAllParentsCompleted;
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { shouldSkipIteratorStepExecution } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-skip-iterator-step-execution.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const shouldSkipStepExecution = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
}) => {
|
||||
if (isWorkflowIteratorAction(step)) {
|
||||
return shouldSkipIteratorStepExecution({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
}
|
||||
|
||||
const parentSteps = steps.filter(
|
||||
(parentStep) =>
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(step.id),
|
||||
);
|
||||
|
||||
if (parentSteps.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parentSteps.every(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[step.id]?.status === StepStatus.STOPPED,
|
||||
);
|
||||
};
|
||||
+15
-8
@@ -1,8 +1,8 @@
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const workflowShouldKeepRunning = ({
|
||||
stepInfos,
|
||||
@@ -20,16 +20,23 @@ export const workflowShouldKeepRunning = ({
|
||||
const successStepWithNotStartedExecutableChildren = steps.some(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SUCCESS &&
|
||||
(step.nextStepIds ?? []).some(
|
||||
(nextStepId) =>
|
||||
(step.nextStepIds ?? []).some((nextStepId) => {
|
||||
const nextStep = steps.find((step) => step.id === nextStepId);
|
||||
|
||||
if (!nextStep) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
stepInfos[nextStepId]?.status === StepStatus.NOT_STARTED &&
|
||||
canExecuteStep({
|
||||
stepId: nextStepId,
|
||||
shouldExecuteStep({
|
||||
step: nextStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||
}),
|
||||
),
|
||||
})
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+159
-14
@@ -1,6 +1,6 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { canExecuteIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/can-execute-iterator-step.util';
|
||||
import { shouldExecuteIteratorStep } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/should-execute-iterator-step.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
@@ -19,7 +19,7 @@ const { getAllStepIdsInLoop } = jest.requireMock(
|
||||
'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util',
|
||||
);
|
||||
|
||||
describe('canExecuteIteratorStep', () => {
|
||||
describe('shouldExecuteIteratorStep', () => {
|
||||
const createMockIteratorStep = (
|
||||
id: string,
|
||||
initialLoopStepIds: string[] = [],
|
||||
@@ -90,7 +90,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
// Mock getAllStepIdsInLoop to return the loop step IDs
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1', 'step-2']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -121,7 +121,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-2']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -147,7 +147,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -171,7 +171,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
// Only step-1 is in the loop
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -194,7 +194,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue([]);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -216,7 +216,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -247,7 +247,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1', 'step-2', 'step-3']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -273,7 +273,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -297,7 +297,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -321,7 +321,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -340,7 +340,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue([]);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -364,7 +364,7 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = canExecuteIteratorStep({
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -372,5 +372,150 @@ describe('canExecuteIteratorStep', () => {
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when loop step has NOT_STARTED status and iterator has been started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.RUNNING }, // Iterator has been started
|
||||
'step-1': { status: StepStatus.NOT_STARTED }, // Loop step not started
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty step info for parent steps', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
// step-2 doesn't have info
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
// Should return false because step-2 info is undefined
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with complex loop structures', () => {
|
||||
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
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS }, // Loop steps can be any status
|
||||
'step-2': { status: StepStatus.FAILED },
|
||||
'step-3': { status: StepStatus.NOT_STARTED },
|
||||
'step-4': { status: StepStatus.SUCCESS }, // Non-loop steps must be successful
|
||||
'step-5': { status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1', 'step-2', 'step-3']);
|
||||
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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 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
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
'step-3': { status: StepStatus.SUCCESS },
|
||||
'step-4': { status: StepStatus.FAILED }, // This should prevent execution
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1', 'step-2']);
|
||||
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle when iterator has no initialLoopStepIds defined', () => {
|
||||
const iteratorStep = {
|
||||
...createMockIteratorStep('iterator-1'),
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: undefined,
|
||||
items: [],
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
} as WorkflowIteratorAction;
|
||||
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
const result = shouldExecuteIteratorStep({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
// getAllStepIdsInLoop should not be called if initialLoopStepIds is undefined
|
||||
expect(getAllStepIdsInLoop).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
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',
|
||||
() => ({
|
||||
getAllStepIdsInLoop: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const { getAllStepIdsInLoop } = jest.requireMock(
|
||||
'src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util',
|
||||
);
|
||||
|
||||
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: {
|
||||
serverlessFunctionId: 'mock-function-id',
|
||||
serverlessFunctionVersion: 'mock-function-version',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
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 steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when all parent steps are stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.STOPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when parent steps are mix of skipped and stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is successful', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SUCCESS },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.FAILED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when at least one parent step is not started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.NOT_STARTED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should only check parent steps not in loop', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']), // In loop
|
||||
createMockStep('step-2', ['iterator-1']), // Not in loop
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS }, // This should be ignored
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when there are no parent steps', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['step-2']),
|
||||
createMockStep('step-2', []),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue([]);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined steps gracefully', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
undefined as unknown as WorkflowAction,
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should work correctly with multiple steps targeting the same iterator', () => {
|
||||
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
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS }, // Loop step, should be ignored
|
||||
'step-2': { status: StepStatus.SUCCESS }, // Loop step, should be ignored
|
||||
'step-3': { status: StepStatus.SKIPPED },
|
||||
'step-4': { status: StepStatus.STOPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1', 'step-2']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when one non-loop parent is not skipped/stopped', () => {
|
||||
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
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SUCCESS }, // Loop step, ignored
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
'step-3': { status: StepStatus.RUNNING }, // This should make it return false
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the iterator has been started', () => {
|
||||
it('should return false when iterator has been started', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.RUNNING }, // Iterator has been started
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false even when all parent steps are skipped/stopped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [
|
||||
createMockStep('step-1', ['iterator-1']),
|
||||
createMockStep('step-2', ['iterator-1']),
|
||||
iteratorStep,
|
||||
];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.SUCCESS }, // Iterator has been started
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
'step-2': { status: StepStatus.STOPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when iterator is pending', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.PENDING },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when iterator is skipped', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.SKIPPED },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when iterator failed', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', ['step-1']);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'iterator-1': { status: StepStatus.FAILED },
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue(['step-1']);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases with initialLoopStepIds', () => {
|
||||
it('should handle undefined initialLoopStepIds', () => {
|
||||
const iteratorStep = {
|
||||
...createMockIteratorStep('iterator-1'),
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: undefined,
|
||||
items: [],
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
} as WorkflowIteratorAction;
|
||||
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue([]);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getAllStepIdsInLoop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty initialLoopStepIds array', () => {
|
||||
const iteratorStep = createMockIteratorStep('iterator-1', []);
|
||||
const steps = [createMockStep('step-1', ['iterator-1']), iteratorStep];
|
||||
const stepInfos = {
|
||||
'step-1': { status: StepStatus.SKIPPED },
|
||||
};
|
||||
|
||||
getAllStepIdsInLoop.mockReturnValue([]);
|
||||
|
||||
const result = shouldSkipIteratorStepExecution({
|
||||
step: iteratorStep,
|
||||
steps,
|
||||
stepInfos,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
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 shouldExecuteIteratorStep = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
}: {
|
||||
step: WorkflowIteratorAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
}) => {
|
||||
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 parentSteps = stepsTargetingIterator.filter(
|
||||
(step) => !stepIdsInLoop.includes(step.id),
|
||||
);
|
||||
|
||||
const stepsToCheck = stepHasBeenStarted(step.id, stepInfos)
|
||||
? stepsTargetingIterator
|
||||
: parentSteps;
|
||||
|
||||
if (stepsToCheck.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasSuccessfulParentStep = stepsToCheck.some(
|
||||
(parentStep) => stepInfos[parentStep.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
|
||||
const hasFailedNorNotStartedOrRunningParentStep = stepsToCheck.some(
|
||||
(parentStep) =>
|
||||
stepInfos[parentStep.id]?.status === StepStatus.FAILED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.NOT_STARTED ||
|
||||
stepInfos[parentStep.id]?.status === StepStatus.RUNNING,
|
||||
);
|
||||
|
||||
return hasSuccessfulParentStep && !hasFailedNorNotStartedOrRunningParentStep;
|
||||
};
|
||||
+20
-21
@@ -8,7 +8,7 @@ import {
|
||||
type WorkflowIteratorAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const canExecuteIteratorStep = ({
|
||||
export const shouldSkipIteratorStepExecution = ({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
@@ -22,28 +22,27 @@ export const canExecuteIteratorStep = ({
|
||||
isDefined(parentStep) && parentStep.nextStepIds?.includes(step.id),
|
||||
);
|
||||
|
||||
// If the step has been started, we need to check if all the steps targeting the iterator have been successful.
|
||||
if (stepHasBeenStarted(step.id, stepInfos)) {
|
||||
return stepsTargetingIterator.every(
|
||||
(step) => stepInfos[step.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
} else {
|
||||
const initialLoopStepIds = step.settings.input.initialLoopStepIds;
|
||||
const initialLoopStepIds = step.settings.input.initialLoopStepIds;
|
||||
|
||||
const stepIdsInLoop = isDefined(initialLoopStepIds)
|
||||
? getAllStepIdsInLoop({
|
||||
iteratorStepId: step.id,
|
||||
initialLoopStepIds,
|
||||
steps,
|
||||
})
|
||||
: [];
|
||||
const stepIdsInLoop = isDefined(initialLoopStepIds)
|
||||
? getAllStepIdsInLoop({
|
||||
iteratorStepId: step.id,
|
||||
initialLoopStepIds,
|
||||
steps,
|
||||
})
|
||||
: [];
|
||||
|
||||
const parentSteps = stepsTargetingIterator.filter(
|
||||
(step) => !stepIdsInLoop.includes(step.id),
|
||||
);
|
||||
const parentSteps = stepsTargetingIterator.filter(
|
||||
(step) => !stepIdsInLoop.includes(step.id),
|
||||
);
|
||||
|
||||
return parentSteps.every(
|
||||
(step) => stepInfos[step.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
if (stepHasBeenStarted(step.id, stepInfos) || parentSteps.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parentSteps.every(
|
||||
(step) =>
|
||||
stepInfos[step.id]?.status === StepStatus.SKIPPED ||
|
||||
stepInfos[step.id]?.status === StepStatus.STOPPED,
|
||||
);
|
||||
};
|
||||
+5
-5
@@ -8,7 +8,7 @@ import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/bil
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
@@ -17,15 +17,15 @@ import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
|
||||
jest.mock(
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
|
||||
'src/modules/workflow/workflow-executor/utils/should-execute-step.util',
|
||||
() => {
|
||||
const actual = jest.requireActual(
|
||||
'src/modules/workflow/workflow-executor/utils/can-execute-step.util',
|
||||
'src/modules/workflow/workflow-executor/utils/should-execute-step.util',
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
canExecuteStep: jest.fn().mockReturnValue(true), // default behavior
|
||||
shouldExecuteStep: jest.fn().mockReturnValue(true), // default behavior
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -325,7 +325,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
});
|
||||
|
||||
it('should return if step should not be executed', async () => {
|
||||
(canExecuteStep as jest.Mock).mockReturnValueOnce(false);
|
||||
(shouldExecuteStep as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
await service.executeFromSteps({
|
||||
workflowRunId: mockWorkflowRunId,
|
||||
|
||||
+81
-38
@@ -6,6 +6,7 @@ import {
|
||||
getWorkflowRunContext,
|
||||
StepStatus,
|
||||
WorkflowRunStepInfo,
|
||||
WorkflowRunStepInfos,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||
@@ -22,7 +23,8 @@ import {
|
||||
type WorkflowBranchExecutorInput,
|
||||
type WorkflowExecutorInput,
|
||||
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.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';
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
@@ -91,51 +93,35 @@ export class WorkflowExecutorWorkspaceService {
|
||||
return;
|
||||
}
|
||||
|
||||
let actionOutput: WorkflowActionOutput;
|
||||
|
||||
if (
|
||||
!canExecuteStep({
|
||||
stepId,
|
||||
shouldExecuteStep({
|
||||
step: stepToExecute,
|
||||
steps,
|
||||
stepInfos,
|
||||
workflowRunStatus: workflowRun.status,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let actionOutput: WorkflowActionOutput;
|
||||
|
||||
if (await this.canBillWorkflowNodeExecution(workspaceId)) {
|
||||
const workflowAction = this.workflowActionFactory.get(stepToExecute.type);
|
||||
|
||||
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||
stepId,
|
||||
stepInfo: {
|
||||
...stepInfos[stepId],
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
actionOutput = await this.executeStep({
|
||||
step: stepToExecute,
|
||||
steps,
|
||||
stepInfos,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
try {
|
||||
actionOutput = await workflowAction.execute({
|
||||
currentStepId: stepId,
|
||||
steps,
|
||||
context: getWorkflowRunContext(stepInfos),
|
||||
runInfo: {
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
actionOutput = {
|
||||
error: error.message ?? 'Execution result error, no data or error',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
} else if (
|
||||
shouldSkipStepExecution({
|
||||
step: stepToExecute,
|
||||
steps,
|
||||
stepInfos,
|
||||
})
|
||||
) {
|
||||
actionOutput = {
|
||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
||||
shouldSkipStepExecution: true,
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const isError = isDefined(actionOutput.error);
|
||||
@@ -266,8 +252,9 @@ export class WorkflowExecutorWorkspaceService {
|
||||
}): Promise<{ shouldProcessNextSteps: boolean }> {
|
||||
const isPendingEvent = actionOutput.pendingEvent;
|
||||
const isSuccess = isDefined(actionOutput.result);
|
||||
const isStopped = actionOutput.shouldEndWorkflowRun;
|
||||
const isNotFinished = actionOutput.shouldRemainRunning;
|
||||
const isStopped = actionOutput.shouldEndWorkflowRun ?? false;
|
||||
const isNotFinished = actionOutput.shouldRemainRunning ?? false;
|
||||
const isSkipped = actionOutput.shouldSkipStepExecution ?? false;
|
||||
|
||||
let stepInfo: WorkflowRunStepInfo;
|
||||
|
||||
@@ -290,6 +277,10 @@ export class WorkflowExecutorWorkspaceService {
|
||||
status: StepStatus.SUCCESS,
|
||||
result: actionOutput?.result,
|
||||
};
|
||||
} else if (isSkipped) {
|
||||
stepInfo = {
|
||||
status: StepStatus.SKIPPED,
|
||||
};
|
||||
} else {
|
||||
stepInfo = {
|
||||
status: StepStatus.FAILED,
|
||||
@@ -305,7 +296,59 @@ export class WorkflowExecutorWorkspaceService {
|
||||
});
|
||||
|
||||
return {
|
||||
shouldProcessNextSteps: isSuccess && !isStopped,
|
||||
shouldProcessNextSteps: isSuccess || isStopped || isSkipped,
|
||||
};
|
||||
}
|
||||
|
||||
private async executeStep({
|
||||
step,
|
||||
steps,
|
||||
stepInfos,
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
steps: WorkflowAction[];
|
||||
stepInfos: WorkflowRunStepInfos;
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const canBill = await this.canBillWorkflowNodeExecution(workspaceId);
|
||||
|
||||
if (!canBill) {
|
||||
return {
|
||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
const stepId = step.id;
|
||||
|
||||
const workflowAction = this.workflowActionFactory.get(step.type);
|
||||
|
||||
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||
stepId,
|
||||
stepInfo: {
|
||||
...stepInfos[stepId],
|
||||
status: StepStatus.RUNNING,
|
||||
},
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
try {
|
||||
return await workflowAction.execute({
|
||||
currentStepId: stepId,
|
||||
steps,
|
||||
context: getWorkflowRunContext(stepInfos),
|
||||
runInfo: {
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error.message ?? 'Execution result error, no data or error',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user