1
0
Fork 0
mirror of synced 2024-09-18 02:08:34 +12:00

Merge pull request #14562 from Budibase/tests/automation-branching-tests

Update logic when no branch condition is met
This commit is contained in:
Peter Clement 2024-09-11 16:16:22 +01:00 committed by GitHub
commit df54cddac9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 463 additions and 383 deletions

View file

@ -0,0 +1,199 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import { Table, AutomationStatus } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("Branching automations", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
it("should run a multiple nested branching automation", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.serverLog({ text: "Starting automation" })
.branch({
topLevelBranch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1" }).branch({
branch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.1" }),
condition: {
equal: { "steps.1.success": true },
},
},
branch2: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.2" }),
condition: {
equal: { "steps.1.success": false },
},
},
}),
condition: {
equal: { "steps.1.success": true },
},
},
topLevelBranch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
equal: { "steps.1.success": false },
},
},
})
.run()
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
})
it("should execute correct branch based on string equality", async () => {
const builder = createAutomationBuilder({
name: "String Equality Branching",
})
const results = await builder
.appAction({ fields: { status: "active" } })
.branch({
activeBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
condition: {
equal: { "trigger.fields.status": "active" },
},
},
inactiveBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Inactive user" }),
condition: {
equal: { "trigger.fields.status": "inactive" },
},
},
})
.run()
expect(results.steps[0].outputs.status).toContain(
"activeBranch branch taken"
)
expect(results.steps[1].outputs.message).toContain("Active user")
})
it("should handle multiple conditions with AND operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple AND Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "active", role: "admin" } })
.branch({
activeAdminBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active admin user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
otherBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Other user" }),
condition: {
notEqual: { "trigger.fields.status": "active" },
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Active admin user")
})
it("should handle multiple conditions with OR operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.branch({
specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "test" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ notEqual: { "trigger.fields.status": "active" } },
{ notEqual: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Special user")
})
it("should stop the branch automation when no conditions are met", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.createRow({ row: { name: "Test", tableId: table._id } })
.branch({
specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "new" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.serverLog({ text: "Test" })
.run()
expect(results.steps[1].outputs.status).toEqual(
AutomationStatus.NO_CONDITION_MET
)
expect(results.steps[2]).toBeUndefined()
})
})

View file

@ -0,0 +1,245 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import {
Table,
LoopStepType,
CreateRowStepOutputs,
ServerLogStepOutputs,
} from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("Loop automations", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
it("should run an automation with a trigger, loop, and create row step", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach((output: any, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
})
})
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.queryRows({
tableId: table._id!,
})
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.rows.0._id}}" })
.run()
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
expect(results.steps[2].outputs.message).toContain("ro_ta")
})
it("if an incorrect type is passed to the loop it should return an error", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: "1, 2, 3",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual({
success: false,
status: "INCORRECT_TYPE",
})
})
it("ensure the loop stops if the failure condition is reached", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"],
failure: "test2",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual(
expect.objectContaining({
status: "FAILURE_CONDITION_MET",
success: false,
})
)
})
it("should run an automation where a loop is successfully run twice", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.loop({
option: LoopStepType.STRING,
binding: "Message 1,Message 2,Message 3",
})
.serverLog({ text: "{{loop.currentItem}}" })
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(2)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach(
(output: CreateRowStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
}
)
expect(results.steps[1].outputs.iterations).toBe(3)
expect(results.steps[1].outputs.items).toHaveLength(3)
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
})
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.3.iterations}}" })
.run()
// We want to ensure that bindings are corr
expect(results.steps[1].outputs.message).toContain("- 3")
expect(results.steps[3].outputs.message).toContain("- 3")
})
})

View file

@ -1,397 +1,19 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import {
Table,
LoopStepType,
CreateRowStepOutputs,
ServerLogStepOutputs,
FieldType,
} from "@budibase/types"
import { LoopStepType, FieldType } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { DatabaseName } from "../../../integrations/tests/utils"
describe("Automation Scenarios", () => {
let config = setup.getConfig(),
table: Table
let config = setup.getConfig()
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
describe("Branching automations", () => {
it("should run a multiple nested branching automation", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.serverLog({ text: "Starting automation" })
.branch({
topLevelBranch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1" }).branch({
branch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.1" }),
condition: {
equal: { "steps.1.success": true },
},
},
branch2: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.2" }),
condition: {
equal: { "steps.1.success": false },
},
},
}),
condition: {
equal: { "steps.1.success": true },
},
},
topLevelBranch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
equal: { "steps.1.success": false },
},
},
})
.run()
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
})
it("should execute correct branch based on string equality", async () => {
const builder = createAutomationBuilder({
name: "String Equality Branching",
})
const results = await builder
.appAction({ fields: { status: "active" } })
.branch({
activeBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active user" }),
condition: {
equal: { "trigger.fields.status": "active" },
},
},
inactiveBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Inactive user" }),
condition: {
equal: { "trigger.fields.status": "inactive" },
},
},
})
.run()
expect(results.steps[0].outputs.status).toContain(
"activeBranch branch taken"
)
expect(results.steps[1].outputs.message).toContain("Active user")
})
it("should handle multiple conditions with AND operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple AND Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "active", role: "admin" } })
.branch({
activeAdminBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active admin user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
otherBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Other user" }),
condition: {
notEqual: { "trigger.fields.status": "active" },
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Active admin user")
})
it("should handle multiple conditions with OR operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.branch({
specialBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "test" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ notEqual: { "trigger.fields.status": "active" } },
{ notEqual: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Special user")
})
})
describe("Loop automations", () => {
it("should run an automation with a trigger, loop, and create row step", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach((output: any, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
})
})
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.queryRows({
tableId: table._id!,
})
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.rows.0._id}}" })
.run()
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
expect(results.steps[2].outputs.message).toContain("ro_ta")
})
it("if an incorrect type is passed to the loop it should return an error", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: "1, 2, 3",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual({
success: false,
status: "INCORRECT_TYPE",
})
})
it("ensure the loop stops if the failure condition is reached", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"],
failure: "test2",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual(
expect.objectContaining({
status: "FAILURE_CONDITION_MET",
success: false,
})
)
})
it("should run an automation where a loop is successfully run twice", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.loop({
option: LoopStepType.STRING,
binding: "Message 1,Message 2,Message 3",
})
.serverLog({ text: "{{loop.currentItem}}" })
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(2)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach(
(output: CreateRowStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
}
)
expect(results.steps[1].outputs.iterations).toBe(3)
expect(results.steps[1].outputs.items).toHaveLength(3)
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
})
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.3.iterations}}" })
.run()
// We want to ensure that bindings are corr
expect(results.steps[1].outputs.message).toContain("- 3")
expect(results.steps[3].outputs.message).toContain("- 3")
})
})
describe("Row Automations", () => {
it("should trigger an automation which then creates a row", async () => {
const table = await config.createTable()

View file

@ -323,7 +323,9 @@ class Orchestrator {
} else if (step.stepId === AutomationActionStepId.LOOP) {
stepIndex = await this.executeLoopStep(step, steps, stepIndex)
} else {
await this.executeStep(step)
if (!this.stopped) {
await this.executeStep(step)
}
stepIndex++
}
}
@ -465,7 +467,7 @@ class Orchestrator {
for (const branch of branches) {
const condition = await this.evaluateBranchCondition(branch.condition)
if (condition) {
let branchStatus = {
const branchStatus = {
status: `${branch.name} branch taken`,
success: true,
}
@ -480,9 +482,20 @@ class Orchestrator {
const branchSteps = children?.[branch.name] || []
await this.executeSteps(branchSteps)
break
return
}
}
this.stopped = true
this.updateExecutionOutput(
branchStep.id,
branchStep.stepId,
branchStep.inputs,
{
success: false,
status: AutomationStatus.NO_CONDITION_MET,
}
)
}
private async evaluateBranchCondition(

View file

@ -179,6 +179,7 @@ export enum AutomationStatus {
ERROR = "error",
STOPPED = "stopped",
STOPPED_ERROR = "stopped_error",
NO_CONDITION_MET = "No branch condition met",
}
export enum AutomationStoppedReason {