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:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -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)
})
})
})

View File

@@ -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()
})
})

View File

@@ -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 })
})
})

View File

@@ -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)
})

View File

@@ -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 })
})
})

View File

@@ -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()
})
})

View File

@@ -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)
})

View File

@@ -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'])
})
})

View File

@@ -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)
})
})

View 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 })
})
})

View 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,
}
}