Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BranchCondition, BranchOperator } from '@activepieces/shared'
|
||||
import { evaluateConditions } from '../../src/lib/handler/router-executor'
|
||||
|
||||
describe('Branch evaluateConditions', () => {
|
||||
describe('DATE_IS_AFTER', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is after second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02',
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-02',
|
||||
])('should return false when first date is before or equal to second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '1st January 2021',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:02Z',
|
||||
secondValue: '2021-01-01T00:00:01Z',
|
||||
operator: BranchOperator.DATE_IS_AFTER,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DATE_IS_BEFORE', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is before second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01',
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-02',
|
||||
])('should return false when first date is after or equal to second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '2nd January 2021',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:01Z',
|
||||
secondValue: '2021-01-01T00:00:02Z',
|
||||
operator: BranchOperator.DATE_IS_BEFORE,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DATE_IS_EQUAL', () => {
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a date',
|
||||
])('should return false when one of the values is not a date %p', (value) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: value as string,
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return true when first date is equal to second date', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01',
|
||||
secondValue: '2021-01-01',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'2021-01-01',
|
||||
'2021-01-03',
|
||||
])('should return false when first date is after or before the second date', (firstDate) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: firstDate,
|
||||
secondValue: '2021-01-02',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should return false when the date is not in a supported format', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-02T00:00:00Z',
|
||||
secondValue: '2nd January 2021',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test('should compare time', () => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: '2021-01-01T00:00:01Z',
|
||||
secondValue: '2021-01-01T00:00:01Z',
|
||||
operator: BranchOperator.DATE_IS_EQUAL,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_IS_EMPTY', () => {
|
||||
test.each([
|
||||
[],
|
||||
'[]',
|
||||
])('should return true when list is empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
[1],
|
||||
'[1]',
|
||||
])('should return false when list is not empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a list',
|
||||
{},
|
||||
])('should return false when the value is not a list %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_IS_NOT_EMPTY', () => {
|
||||
test.each([
|
||||
[1],
|
||||
'[1]',
|
||||
])('should return true when list is not empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
[],
|
||||
'[]',
|
||||
])('should return false when list is empty %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'not a list',
|
||||
{},
|
||||
])('should return false when the value is not a list %p', (input: any) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: input,
|
||||
operator: BranchOperator.LIST_IS_NOT_EMPTY,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_CONTAINS', () => {
|
||||
test.each([
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'banana', caseSensitive: false },
|
||||
{ expected: false, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: true },
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: false },
|
||||
{ expected: true, list: '["apple", "banana", "cherry"]', value: 'banana', caseSensitive: false },
|
||||
{ expected: true, list: 'apple', value: 'apple', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: '4', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: 4, caseSensitive: false },
|
||||
{ expected: true, list: [true, false, true], value: 'true', caseSensitive: false },
|
||||
{ expected: true, list: [true, false, true], value: true, caseSensitive: false },
|
||||
{ expected: true, list: ['true', 'false', 'true'], value: true, caseSensitive: false },
|
||||
{ expected: true, list: ['true', 'false', 'true'], value: 'true', caseSensitive: false },
|
||||
])('should return $expected for list $list containing $value (case sensitive: $caseSensitive)', ({ expected, list, value, caseSensitive }) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: list as any,
|
||||
secondValue: value as any,
|
||||
operator: BranchOperator.LIST_CONTAINS,
|
||||
caseSensitive,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LIST_DOES_NOT_CONTAIN', () => {
|
||||
test.each([
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: true },
|
||||
{ expected: false, list: ['apple', 'banana', 'cherry'], value: 'Banana', caseSensitive: false },
|
||||
{ expected: true, list: '["apple", "banana", "cherry"]', value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: 'apple', value: 'grape', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: '6', caseSensitive: false },
|
||||
{ expected: true, list: [1, 2, 3, 4, 5], value: 6, caseSensitive: false },
|
||||
{ expected: false, list: [true, false, true], value: 'false', caseSensitive: false },
|
||||
{ expected: false, list: [true, false, true], value: false, caseSensitive: false },
|
||||
{ expected: false, list: ['true', 'false', 'true'], value: false, caseSensitive: false },
|
||||
{ expected: false, list: ['true', 'false', 'true'], value: 'false', caseSensitive: false },
|
||||
])('should return $expected for list $list not containing $value (case sensitive: $caseSensitive)', ({ expected, list, value, caseSensitive }) => {
|
||||
const condition: BranchCondition = {
|
||||
firstValue: list as any,
|
||||
secondValue: value as any,
|
||||
operator: BranchOperator.LIST_DOES_NOT_CONTAIN,
|
||||
caseSensitive,
|
||||
}
|
||||
|
||||
expect(evaluateConditions([[condition]])).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,521 @@
|
||||
import { BranchCondition, BranchOperator, FlowRunStatus, RouterExecutionType } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildRouterWithOneCondition, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
function executeBranchActionWithOneCondition(condition: BranchCondition): Promise<FlowExecutorContext> {
|
||||
return flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({
|
||||
conditions: [condition],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
children: [
|
||||
buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
condition: true,
|
||||
},
|
||||
}),
|
||||
buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {
|
||||
condition: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
}
|
||||
|
||||
describe('flow with branching different branches', () => {
|
||||
|
||||
it('should execute branch with text contains condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_CONTAINS,
|
||||
firstValue: 'test',
|
||||
secondValue: 'TeSt',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not contain condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_CONTAIN,
|
||||
firstValue: 'test',
|
||||
secondValue: 'ExAmPlE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text exactly matches condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'TeSt',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not exactly match condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_EXACTLY_MATCH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'ExAmPlE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text starts with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_STARTS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'tE',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not start with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_START_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'eS',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text ends with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_ENDS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'sT',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not end with condition (case insensitive)', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_END_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'eS',
|
||||
caseSensitive: false,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text contains condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_CONTAINS,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not contain condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_CONTAIN,
|
||||
firstValue: 'test',
|
||||
secondValue: 'example',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text exactly matches condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not exactly match condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_EXACTLY_MATCH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'example',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text starts with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_STARTS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'te',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not start with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_START_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'es',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text ends with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_ENDS_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'st',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with text does not end with condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.TEXT_DOES_NOT_END_WITH,
|
||||
firstValue: 'test',
|
||||
secondValue: 'es',
|
||||
caseSensitive: true,
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with exists condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.EXISTS,
|
||||
firstValue: 'test',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with does not exist condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.DOES_NOT_EXIST,
|
||||
firstValue: '',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with boolean is true condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with boolean is false condition', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_FALSE,
|
||||
firstValue: '{{false}}',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with two equal numbers', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_EQUAL_TO,
|
||||
firstValue: '1',
|
||||
secondValue: '1',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with the first number greater than the second one', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_GREATER_THAN,
|
||||
firstValue: '2',
|
||||
secondValue: '1',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute branch with the first number less than the second one', async () => {
|
||||
const result = await executeBranchActionWithOneCondition(
|
||||
{
|
||||
operator: BranchOperator.NUMBER_IS_LESS_THAN,
|
||||
firstValue: '1',
|
||||
secondValue: '2',
|
||||
},
|
||||
)
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router.output).toEqual({
|
||||
branches: [
|
||||
{
|
||||
branchIndex: 1,
|
||||
branchName: 'Test Branch',
|
||||
evaluation: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip router', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({ children: [
|
||||
buildCodeAction({ name: 'echo_step', input: {}, skip: true }),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], executionType: RouterExecutionType.EXECUTE_FIRST_MATCH, skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FlowAction, FlowRunStatus } from '@activepieces/shared'
|
||||
import { codeExecutor } from '../../src/lib/handler/code-executor'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('codeExecutor', () => {
|
||||
|
||||
it('should execute code that echo parameters action successfully', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute code a code that throws an error', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'runtime',
|
||||
displayName: 'Your Action Name',
|
||||
message: expect.stringContaining('Custom Runtime Error'),
|
||||
},
|
||||
})
|
||||
expect(result.steps.runtime.status).toEqual('FAILED')
|
||||
expect(result.steps.runtime.errorMessage).toContain('Custom Runtime Error')
|
||||
})
|
||||
|
||||
it('should skip code action', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
skip: true,
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step).toBeUndefined()
|
||||
})
|
||||
it('should skip flow action', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: true,
|
||||
input: {},
|
||||
}),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.echo_step).toBeUndefined()
|
||||
expect(result.steps.echo_step_1.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { codeExecutor } from '../../src/lib/handler/code-executor'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { pieceExecutor } from '../../src/lib/handler/piece-executor'
|
||||
import { buildCodeAction, buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('code piece with error handling', () => {
|
||||
|
||||
it('should continue on failure when execute code a code that throws an error', async () => {
|
||||
const result = await codeExecutor.handle({
|
||||
action: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.runtime.status).toEqual('FAILED')
|
||||
expect(result.steps.runtime.errorMessage).toContain('Custom Runtime Error')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('piece with error handling', () => {
|
||||
|
||||
it('should continue on failure when piece fails', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'method': 'POST',
|
||||
'url': 'https://cloud.activepieces.com/api/v1/flags',
|
||||
'headers': {},
|
||||
'queryParams': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: true,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const expectedError = {
|
||||
response: {
|
||||
status: 404,
|
||||
body: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Route not found',
|
||||
},
|
||||
},
|
||||
request: {},
|
||||
}
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.send_http.status).toBe('FAILED')
|
||||
expect(result.steps.send_http.errorMessage).toEqual(JSON.stringify(expectedError, null, 2))
|
||||
|
||||
}, 10000)
|
||||
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FlowAction, FlowRunStatus, LoopStepOutput } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildSimpleLoopAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
|
||||
describe('flow with looping', () => {
|
||||
|
||||
it('should execute iterations', async () => {
|
||||
const codeAction = buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'index': '{{loop.index}}',
|
||||
},
|
||||
})
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ [4,5,6] }}',
|
||||
firstLoopAction: codeAction,
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const loopOut = result.steps.loop as LoopStepOutput
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(loopOut.output?.iterations.length).toBe(3)
|
||||
expect(loopOut.output?.index).toBe(3)
|
||||
expect(loopOut.output?.item).toBe(6)
|
||||
})
|
||||
|
||||
it('should execute iterations and fail on first iteration', async () => {
|
||||
const generateArray = buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {
|
||||
'array': '{{ [4,5,6] }}',
|
||||
},
|
||||
nextAction: buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ echo_step.array }}',
|
||||
firstLoopAction: buildCodeAction({
|
||||
name: 'runtime',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
const result = await flowExecutor.execute({
|
||||
action: generateArray,
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const loopOut = result.steps.loop as LoopStepOutput
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(loopOut.output?.iterations.length).toBe(1)
|
||||
expect(loopOut.output?.index).toBe(1)
|
||||
expect(loopOut.output?.item).toBe(4)
|
||||
})
|
||||
|
||||
it('should skip loop', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildSimpleLoopAction({ name: 'loop', loopItems: '{{ [4,5,6] }}', skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.loop).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should skip loop in flow', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildSimpleLoopAction({ name: 'loop', loopItems: '{{ [4,5,6] }}', skip: true }),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: false,
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
expect(result.steps.loop).toBeUndefined()
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import { FlowAction, FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { pieceExecutor } from '../../src/lib/handler/piece-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('pieceExecutor', () => {
|
||||
|
||||
it('should execute data mapper successfully', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute fail gracefully when pieces fail', async () => {
|
||||
const result = await pieceExecutor.handle({
|
||||
action: buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/asd',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const expectedError = {
|
||||
response: {
|
||||
status: 404,
|
||||
body: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Route not found',
|
||||
},
|
||||
},
|
||||
request: {},
|
||||
}
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.FAILED,
|
||||
failedStep: {
|
||||
name: 'send_http',
|
||||
displayName: 'Your Action Name',
|
||||
message: JSON.stringify(expectedError, null, 2),
|
||||
},
|
||||
})
|
||||
expect(result.steps.send_http.status).toBe('FAILED')
|
||||
expect(result.steps.send_http.errorMessage).toEqual(JSON.stringify(expectedError, null, 2))
|
||||
}, 10000)
|
||||
it('should skip piece action', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
input: {},
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
})
|
||||
it('should skip piece action in flow', async () => {
|
||||
const flow: FlowAction = {
|
||||
...buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
}),
|
||||
nextAction: {
|
||||
...buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {},
|
||||
skip: true,
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: flow, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.send_http).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
const failedHttpAction = buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/asd',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
})
|
||||
|
||||
const successHttpAction = buildPieceAction({
|
||||
name: 'send_http',
|
||||
pieceName: '@activepieces/piece-http',
|
||||
actionName: 'send_request',
|
||||
input: {
|
||||
'url': 'https://cloud.activepieces.com/api/v1/pieces',
|
||||
'method': 'GET',
|
||||
'headers': {},
|
||||
'body_type': 'none',
|
||||
'body': {},
|
||||
'queryParams': {},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
describe('flow retry', () => {
|
||||
const context = FlowExecutorContext.empty()
|
||||
it('should retry entire flow', async () => {
|
||||
const failedResult = await flowExecutor.execute({
|
||||
action: failedHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
const retryEntireFlow = await flowExecutor.execute({
|
||||
action: successHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(failedResult.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(retryEntireFlow.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
}, 10000)
|
||||
|
||||
it('should retry flow from failed step', async () => {
|
||||
const failedResult = await flowExecutor.execute({
|
||||
action: failedHttpAction, executionState: context, constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
const retryFromFailed = await flowExecutor.execute({
|
||||
action: successHttpAction, executionState: context, constants: generateMockEngineConstants({}),
|
||||
})
|
||||
expect(failedResult.verdict.status).toBe(FlowRunStatus.FAILED)
|
||||
expect(retryFromFailed.verdict.status).toBe(FlowRunStatus.RUNNING)
|
||||
}, 10000)
|
||||
})
|
||||
@@ -0,0 +1,275 @@
|
||||
import { BranchOperator, FlowRunStatus, LoopStepOutput, RouterExecutionType, RouterStepOutput } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { StepExecutionPath } from '../../src/lib/handler/context/step-execution-path'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildPieceAction, buildRouterWithOneCondition, buildSimpleLoopAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
|
||||
const simplePauseFlow = buildPieceAction({
|
||||
name: 'approval',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
}),
|
||||
})
|
||||
|
||||
const flawWithTwoPause = buildPieceAction({
|
||||
name: 'approval',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
nextAction: buildPieceAction({
|
||||
name: 'approval-1',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const pauseFlowWithLoopAndBranch = buildSimpleLoopAction({
|
||||
name: 'loop',
|
||||
loopItems: '{{ [false, true ] }}',
|
||||
firstLoopAction: buildRouterWithOneCondition({
|
||||
conditions: [
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: '{{ loop.item }}',
|
||||
},
|
||||
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
children: [
|
||||
simplePauseFlow,
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
describe('flow with pause', () => {
|
||||
|
||||
it('should pause and resume successfully with loops and branch', async () => {
|
||||
const pauseResult = await flowExecutor.execute({
|
||||
action: pauseFlowWithLoopAndBranch,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(pauseResult.verdict).toEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
expect(Object.keys(pauseResult.steps)).toEqual(['loop'])
|
||||
|
||||
// Verify that the first iteration (true) triggered the branch condition
|
||||
const loopOutputBeforeResume = pauseResult.steps.loop as LoopStepOutput
|
||||
expect(loopOutputBeforeResume.output?.iterations.length).toBe(2)
|
||||
expect(loopOutputBeforeResume.output?.item).toBe(true)
|
||||
expect(Object.keys(loopOutputBeforeResume.output?.iterations[0] ?? {})).toContain('router')
|
||||
|
||||
|
||||
const resumeResultTwo = await flowExecutor.execute({
|
||||
action: pauseFlowWithLoopAndBranch,
|
||||
executionState: pauseResult.setCurrentPath(StepExecutionPath.empty()).setVerdict({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
}),
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(resumeResultTwo.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
},
|
||||
)
|
||||
expect(Object.keys(resumeResultTwo.steps)).toEqual(['loop'])
|
||||
|
||||
const loopOut = resumeResultTwo.steps.loop as LoopStepOutput
|
||||
expect(Object.keys(loopOut.output?.iterations[1] ?? {})).toEqual(['router', 'approval', 'echo_step'])
|
||||
expect((loopOut.output?.iterations[0].router as RouterStepOutput).output?.branches[0].evaluation).toBe(false)
|
||||
expect((loopOut.output?.iterations[1].router as RouterStepOutput).output?.branches[0].evaluation).toBe(true)
|
||||
|
||||
|
||||
})
|
||||
|
||||
it('should pause and resume with two different steps in same flow successfully', async () => {
|
||||
const pauseResult1 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
const resumeResult1 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: pauseResult1,
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult1.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
const resumeResult2 = await flowExecutor.execute({
|
||||
action: flawWithTwoPause,
|
||||
executionState: resumeResult1.setVerdict({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
}),
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult2.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
it('should pause and resume successfully', async () => {
|
||||
const pauseResult = await flowExecutor.execute({
|
||||
action: simplePauseFlow,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(pauseResult.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
const currentState = pauseResult.currentState()
|
||||
expect(Object.keys(currentState).length).toBe(1)
|
||||
|
||||
const resumeResult = await flowExecutor.execute({
|
||||
action: simplePauseFlow,
|
||||
executionState: pauseResult,
|
||||
constants: generateMockEngineConstants({
|
||||
resumePayload: {
|
||||
queryParams: {
|
||||
action: 'approve',
|
||||
},
|
||||
body: {},
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
expect(resumeResult.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(resumeResult.currentState()).toEqual({
|
||||
'approval': {
|
||||
approved: true,
|
||||
},
|
||||
echo_step: {},
|
||||
})
|
||||
})
|
||||
|
||||
it('should pause at most one action when router has multiple branches with pause actions', async () => {
|
||||
const routerWithTwoPauseActions = buildRouterWithOneCondition({
|
||||
conditions: [
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.BOOLEAN_IS_TRUE,
|
||||
firstValue: 'true',
|
||||
},
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_ALL_MATCH,
|
||||
children: [
|
||||
buildPieceAction({
|
||||
name: 'approval_1',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'approval_2',
|
||||
pieceName: '@activepieces/piece-approval',
|
||||
actionName: 'wait_for_approval',
|
||||
input: {},
|
||||
nextAction: buildCodeAction({
|
||||
name: 'echo_step_1',
|
||||
input: {},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const result = await flowExecutor.execute({
|
||||
action: routerWithTwoPauseActions,
|
||||
executionState: FlowExecutorContext.empty().setPauseRequestId('requestId'),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.PAUSED,
|
||||
pauseMetadata: {
|
||||
response: {},
|
||||
requestId: 'requestId',
|
||||
requestIdToReply: undefined,
|
||||
'type': 'WEBHOOK',
|
||||
},
|
||||
})
|
||||
|
||||
const routerOutput = result.steps.router as RouterStepOutput
|
||||
expect(routerOutput).toBeDefined()
|
||||
expect(routerOutput.output).toBeDefined()
|
||||
|
||||
const executedBranches = routerOutput.output?.branches?.filter((branch) => branch.evaluation === true)
|
||||
expect(executedBranches).toHaveLength(2)
|
||||
|
||||
expect(result.steps.approval_1).toBeDefined()
|
||||
expect(result.steps.approval_1.status).toBe('PAUSED')
|
||||
expect(result.steps.approval_2).toBeUndefined()
|
||||
|
||||
expect(Object.keys(result.steps)).toEqual(['router', 'approval_1'])
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FlowRunStatus } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildPieceAction, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
describe('flow with response', () => {
|
||||
|
||||
it('should execute return response successfully', async () => {
|
||||
const input = {
|
||||
responseType: 'json',
|
||||
fields: {
|
||||
status: 200,
|
||||
headers: {
|
||||
'random': 'header',
|
||||
},
|
||||
body: {
|
||||
'hello': 'world',
|
||||
},
|
||||
},
|
||||
respond: 'stop',
|
||||
}
|
||||
const response = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'random': 'header',
|
||||
},
|
||||
body: {
|
||||
'hello': 'world',
|
||||
},
|
||||
}
|
||||
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildPieceAction({
|
||||
name: 'http',
|
||||
pieceName: '@activepieces/piece-webhook',
|
||||
actionName: 'return_response',
|
||||
input,
|
||||
}), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.SUCCEEDED,
|
||||
stopResponse: response,
|
||||
})
|
||||
expect(result.steps.http.output).toEqual(response)
|
||||
})
|
||||
|
||||
})
|
||||
433
activepieces-fork/packages/engine/test/handler/router-branching.test.ts
Executable file
433
activepieces-fork/packages/engine/test/handler/router-branching.test.ts
Executable file
@@ -0,0 +1,433 @@
|
||||
import { BranchCondition, BranchOperator, FlowAction, FlowRunStatus, RouterExecutionType } from '@activepieces/shared'
|
||||
import { FlowExecutorContext } from '../../src/lib/handler/context/flow-execution-context'
|
||||
import { flowExecutor } from '../../src/lib/handler/flow-executor'
|
||||
import { buildCodeAction, buildPieceAction, buildRouterWithOneCondition, generateMockEngineConstants } from './test-helper'
|
||||
|
||||
function executeRouterActionWithOneCondition(children: FlowAction[], conditions: (BranchCondition | null)[], executionType: RouterExecutionType): Promise<FlowExecutorContext> {
|
||||
return flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({
|
||||
children,
|
||||
conditions,
|
||||
executionType,
|
||||
}),
|
||||
executionState: FlowExecutorContext.empty(),
|
||||
constants: generateMockEngineConstants(),
|
||||
})
|
||||
}
|
||||
describe('router with branching different conditions', () => {
|
||||
it('should execute router with the first matching condition', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'anything',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should execute router with the all matching conditions', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
|
||||
it('should execute router but no branch will match', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
const routerOutput = result.steps.router.output as { branches: boolean[] }
|
||||
expect(routerOutput.branches).toEqual([
|
||||
{
|
||||
branchName: 'Test Branch',
|
||||
branchIndex: 1,
|
||||
evaluation: false,
|
||||
},
|
||||
{
|
||||
branchName: 'Test Branch',
|
||||
branchIndex: 2,
|
||||
evaluation: false,
|
||||
},
|
||||
])
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should execute fallback branch with first match execution type', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
expect(result.steps.fallback_mapper.output).toEqual({ 'key': 11 })
|
||||
})
|
||||
|
||||
it('should execute fallback branch with all match execution type', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'abc',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'fasc',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper).toBeUndefined()
|
||||
expect(result.steps.data_mapper_1).toBeUndefined()
|
||||
expect(result.steps.fallback_mapper.output).toEqual({ 'key': 11 })
|
||||
})
|
||||
|
||||
it('should not execute fallback branch when there is a matching condition in EXECUTE_FIRST_MATCH mode', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_FIRST_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.fallback_mapper).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not execute fallback branch when there is a matching condition in EXECUTE_ALL_MATCH mode', async () => {
|
||||
const result = await executeRouterActionWithOneCondition([
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'data_mapper_1',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 5 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildPieceAction({
|
||||
name: 'fallback_mapper',
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {
|
||||
mapping: {
|
||||
'key': '{{ 1 + 10 }}',
|
||||
},
|
||||
},
|
||||
}),
|
||||
], [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
null, // Fallback branch
|
||||
], RouterExecutionType.EXECUTE_ALL_MATCH)
|
||||
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.data_mapper.output).toEqual({ 'key': 3 })
|
||||
expect(result.steps.data_mapper_1.output).toEqual({ 'key': 6 })
|
||||
expect(result.steps.fallback_mapper).toBeUndefined()
|
||||
})
|
||||
it('should skip router', async () => {
|
||||
const result = await flowExecutor.execute({
|
||||
action: buildRouterWithOneCondition({ children: [
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {},
|
||||
}),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
], executionType: RouterExecutionType.EXECUTE_FIRST_MATCH, skip: true }), executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
})
|
||||
it('should skip router action in flow', async () => {
|
||||
const router: FlowAction = {
|
||||
...buildRouterWithOneCondition({ children: [
|
||||
buildPieceAction({
|
||||
name: 'data_mapper',
|
||||
skip: true,
|
||||
pieceName: '@activepieces/piece-data-mapper',
|
||||
actionName: 'advanced_mapping',
|
||||
input: {},
|
||||
}),
|
||||
], conditions: [
|
||||
{
|
||||
operator: BranchOperator.TEXT_EXACTLY_MATCHES,
|
||||
firstValue: 'test',
|
||||
secondValue: 'test',
|
||||
caseSensitive: false,
|
||||
},
|
||||
],
|
||||
executionType: RouterExecutionType.EXECUTE_FIRST_MATCH,
|
||||
skip: true }),
|
||||
nextAction: {
|
||||
...buildCodeAction({
|
||||
name: 'echo_step',
|
||||
skip: false,
|
||||
input: {
|
||||
'key': '{{ 1 + 2 }}',
|
||||
},
|
||||
}),
|
||||
nextAction: undefined,
|
||||
},
|
||||
}
|
||||
const result = await flowExecutor.execute({
|
||||
action: router, executionState: FlowExecutorContext.empty(), constants: generateMockEngineConstants(),
|
||||
})
|
||||
expect(result.verdict).toStrictEqual({
|
||||
status: FlowRunStatus.RUNNING,
|
||||
})
|
||||
expect(result.steps.router).toBeUndefined()
|
||||
expect(result.steps.echo_step.output).toEqual({ 'key': 3 })
|
||||
})
|
||||
})
|
||||
122
activepieces-fork/packages/engine/test/handler/test-helper.ts
Normal file
122
activepieces-fork/packages/engine/test/handler/test-helper.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { ActionErrorHandlingOptions, BranchCondition, BranchExecutionType, CodeAction, FlowAction, FlowActionType, FlowVersionState, LoopOnItemsAction, PieceAction, ProgressUpdateType, PropertyExecutionType, RouterExecutionType, RunEnvironment } from '@activepieces/shared'
|
||||
import { EngineConstants } from '../../src/lib/handler/context/engine-constants'
|
||||
|
||||
export const generateMockEngineConstants = (params?: Partial<EngineConstants>): EngineConstants => {
|
||||
return new EngineConstants(
|
||||
{
|
||||
platformId: params?.platformId ?? 'platformId',
|
||||
timeoutInSeconds: params?.timeoutInSeconds ?? 10,
|
||||
flowId: params?.flowId ?? 'flowId',
|
||||
flowVersionId: params?.flowVersionId ?? 'flowVersionId',
|
||||
flowVersionState: params?.flowVersionState ?? FlowVersionState.DRAFT,
|
||||
flowRunId: params?.flowRunId ?? 'flowRunId',
|
||||
publicApiUrl: params?.publicApiUrl ?? 'http://127.0.0.1:4200/api/',
|
||||
internalApiUrl: params?.internalApiUrl ?? 'http://127.0.0.1:3000/',
|
||||
retryConstants: params?.retryConstants ?? {
|
||||
maxAttempts: 2,
|
||||
retryExponential: 1,
|
||||
retryInterval: 1,
|
||||
},
|
||||
engineToken: params?.engineToken ?? 'engineToken',
|
||||
projectId: params?.projectId ?? 'projectId',
|
||||
triggerPieceName: params?.triggerPieceName ?? 'mcp-trigger-piece-name',
|
||||
progressUpdateType: params?.progressUpdateType ?? ProgressUpdateType.NONE,
|
||||
serverHandlerId: params?.serverHandlerId ?? null,
|
||||
httpRequestId: params?.httpRequestId ?? null,
|
||||
resumePayload: params?.resumePayload,
|
||||
runEnvironment: params?.runEnvironment ?? RunEnvironment.TESTING,
|
||||
stepNameToTest: params?.stepNameToTest ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export function buildSimpleLoopAction({
|
||||
name,
|
||||
loopItems,
|
||||
firstLoopAction,
|
||||
skip,
|
||||
}: {
|
||||
name: string
|
||||
loopItems: string
|
||||
firstLoopAction?: FlowAction
|
||||
skip?: boolean
|
||||
}): LoopOnItemsAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Loop',
|
||||
type: FlowActionType.LOOP_ON_ITEMS,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
items: loopItems,
|
||||
},
|
||||
firstLoopAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRouterWithOneCondition({ children, conditions, executionType, skip }: { children: FlowAction[], conditions: (BranchCondition | null)[], executionType: RouterExecutionType, skip?: boolean }): FlowAction {
|
||||
return {
|
||||
name: 'router',
|
||||
displayName: 'Your Router Name',
|
||||
type: FlowActionType.ROUTER,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
branches: conditions.map((condition) => {
|
||||
if (condition === null) {
|
||||
return {
|
||||
branchType: BranchExecutionType.FALLBACK,
|
||||
branchName: 'Fallback Branch',
|
||||
}
|
||||
}
|
||||
return {
|
||||
conditions: [[condition]],
|
||||
branchType: BranchExecutionType.CONDITION,
|
||||
branchName: 'Test Branch',
|
||||
}
|
||||
}),
|
||||
executionType,
|
||||
},
|
||||
children,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCodeAction({ name, input, skip, nextAction, errorHandlingOptions }: { name: 'echo_step' | 'runtime' | 'echo_step_1', input: Record<string, unknown>, skip?: boolean, errorHandlingOptions?: ActionErrorHandlingOptions, nextAction?: FlowAction }): CodeAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Your Action Name',
|
||||
type: FlowActionType.CODE,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
input,
|
||||
sourceCode: {
|
||||
packageJson: '',
|
||||
code: '',
|
||||
},
|
||||
errorHandlingOptions,
|
||||
},
|
||||
nextAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPieceAction({ name, input, skip, pieceName, actionName, nextAction, errorHandlingOptions }: { errorHandlingOptions?: ActionErrorHandlingOptions, name: string, input: Record<string, unknown>, skip?: boolean, pieceName: string, actionName: string, nextAction?: FlowAction }): PieceAction {
|
||||
return {
|
||||
name,
|
||||
displayName: 'Your Action Name',
|
||||
type: FlowActionType.PIECE,
|
||||
skip: skip ?? false,
|
||||
settings: {
|
||||
input,
|
||||
pieceName,
|
||||
pieceVersion: '1.0.0', // Not required since it's running in development mode
|
||||
actionName,
|
||||
propertySettings: Object.fromEntries(Object.entries(input).map(([key]) => [key, {
|
||||
type: PropertyExecutionType.MANUAL,
|
||||
schema: undefined,
|
||||
}])),
|
||||
errorHandlingOptions,
|
||||
},
|
||||
nextAction,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user