Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions packages/components/nodes/agentflow/Condition/Condition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const { nodeClass: Condition_Agentflow } = require('./Condition')
import { INodeData } from '../../../src/Interface'

// Helper function to create a valid INodeData object for Condition node
function createConditionNodeData(id: string, conditions: any[]): INodeData {
return {
id: id,
label: 'Condition',
name: 'conditionAgentflow',
type: 'Condition',
icon: 'condition.svg',
version: 1.0,
category: 'Agent Flows',
baseClasses: ['Condition'],
inputs: {
conditions: conditions
}
}
}

describe('Condition Agentflow - Regex Operation', () => {
let nodeClass: any

beforeEach(() => {
nodeClass = new Condition_Agentflow()
})

describe('Valid regex patterns', () => {
it('should match when regex pattern matches value1', async () => {
const conditions = [
{ type: 'string', value1: 'hello world', operation: 'regex', value2: 'hello' }
]
const nodeData = createConditionNodeData('test-regex-1', conditions)
const result = await nodeClass.run(nodeData, '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should not match when regex pattern does not match value1', async () => {
const conditions = [
{ type: 'string', value1: 'hello world', operation: 'regex', value2: '^world' }
]
const nodeData = createConditionNodeData('test-regex-2', conditions)
const result = await nodeClass.run(nodeData, '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBeUndefined()
expect(result.output.conditions[1].isFulfilled).toBe(true)
})

it('should match digits with [0-9]+ pattern', async () => {
const conditions = [
{ type: 'string', value1: 'test123abc', operation: 'regex', value2: '[0-9]+' }
]
const result = await nodeClass.run(createConditionNodeData('test-3', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should match email pattern', async () => {
const conditions = [
{ type: 'string', value1: '[email protected]', operation: 'regex', value2: '^[a-zA-Z0-9.]+@[a-zA-Z0-9.]+$' }
]
const result = await nodeClass.run(createConditionNodeData('test-4', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})
})

describe('Invalid regex patterns', () => {
it('should return false (not crash) for invalid regex pattern', async () => {
const conditions = [
{ type: 'string', value1: 'test', operation: 'regex', value2: '[invalid(' }
]
const result = await nodeClass.run(createConditionNodeData('test-invalid', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBeUndefined()
expect(result.output.conditions[1].isFulfilled).toBe(true)
})
})

describe('Edge cases', () => {
it('should handle empty value1', async () => {
const conditions = [
{ type: 'string', value1: '', operation: 'regex', value2: '.*' }
]
const result = await nodeClass.run(createConditionNodeData('test-empty', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle null value1', async () => {
const conditions = [
{ type: 'string', value1: null, operation: 'regex', value2: 'test' }
]
const result = await nodeClass.run(createConditionNodeData('test-null', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBeUndefined()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make this test more robust and align with other tests for non-matching conditions (e.g., lines 44-45), you should also assert that the else condition is fulfilled.

            expect(result.output.conditions[0].isFulfilled).toBeUndefined()
            expect(result.output.conditions[1].isFulfilled).toBe(true)

expect(result.output.conditions[1].isFulfilled).toBe(true)
})

it('should be case-sensitive by default', async () => {
const conditions = [
{ type: 'string', value1: 'Hello', operation: 'regex', value2: 'hello' }
]
const result = await nodeClass.run(createConditionNodeData('test-case', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBeUndefined()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to other tests for non-matching conditions, it would be good to also assert that the else condition is fulfilled to ensure the fallback logic is working as expected.

            expect(result.output.conditions[0].isFulfilled).toBeUndefined()
            expect(result.output.conditions[1].isFulfilled).toBe(true)

expect(result.output.conditions[1].isFulfilled).toBe(true)
})
})

describe('Flowise input escaping', () => {
it('should unescape brackets in pattern', async () => {
const conditions = [
{ type: 'string', value1: 'test123abc', operation: 'regex', value2: '\\[0-9\\]+' }
]
const result = await nodeClass.run(createConditionNodeData('test-brackets', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should unescape double-backslash pattern', async () => {
const conditions = [
{ type: 'string', value1: 'test123abc', operation: 'regex', value2: '\\\\d+' }
]
const result = await nodeClass.run(createConditionNodeData('test-backslash', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should unescape escaped asterisk', async () => {
// User typed go*al, Flowise stored as go\*al
const conditions = [
{ type: 'string', value1: 'goooal', operation: 'regex', value2: 'go\\*al' }
]
const result = await nodeClass.run(createConditionNodeData('test-escaped-asterisk', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})
})

describe('Complex regex patterns - all metacharacters', () => {
it('should handle ^ (start anchor)', async () => {
const conditions = [{ type: 'string', value1: 'hello world', operation: 'regex', value2: '^hello' }]
const result = await nodeClass.run(createConditionNodeData('test-caret', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle $ (end anchor)', async () => {
const conditions = [{ type: 'string', value1: 'hello world', operation: 'regex', value2: 'world$' }]
const result = await nodeClass.run(createConditionNodeData('test-dollar', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle . (any character)', async () => {
const conditions = [{ type: 'string', value1: 'cat', operation: 'regex', value2: 'c.t' }]
const result = await nodeClass.run(createConditionNodeData('test-dot', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle * (zero or more)', async () => {
const conditions = [{ type: 'string', value1: 'goooal', operation: 'regex', value2: 'go*al' }]
const result = await nodeClass.run(createConditionNodeData('test-star', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle + (one or more)', async () => {
const conditions = [{ type: 'string', value1: 'goooal', operation: 'regex', value2: 'go+al' }]
const result = await nodeClass.run(createConditionNodeData('test-plus', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle ? (zero or one)', async () => {
const conditions = [{ type: 'string', value1: 'color', operation: 'regex', value2: 'colou?r' }]
const result = await nodeClass.run(createConditionNodeData('test-question', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle | (alternation)', async () => {
const conditions = [{ type: 'string', value1: 'cat', operation: 'regex', value2: 'cat|dog' }]
const result = await nodeClass.run(createConditionNodeData('test-pipe', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle () (grouping)', async () => {
const conditions = [{ type: 'string', value1: 'abcabc', operation: 'regex', value2: '(abc)+' }]
const result = await nodeClass.run(createConditionNodeData('test-parens', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle {} (quantifier)', async () => {
const conditions = [{ type: 'string', value1: 'aaa', operation: 'regex', value2: 'a{3}' }]
const result = await nodeClass.run(createConditionNodeData('test-braces', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle complex email pattern', async () => {
const conditions = [{
type: 'string',
value1: '[email protected]',
operation: 'regex',
value2: '^[a-z]+@[a-z]+\\.(com|org)$'
}]
const result = await nodeClass.run(createConditionNodeData('test-complex', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle URL pattern', async () => {
const conditions = [{
type: 'string',
value1: 'https://example.com/path?query=1',
operation: 'regex',
value2: '^https?://[a-z.]+/.*$'
}]
const result = await nodeClass.run(createConditionNodeData('test-url', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})

it('should handle UUID pattern', async () => {
const conditions = [{
type: 'string',
value1: '550e8400-e29b-41d4-a716-446655440000',
operation: 'regex',
value2: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
}]
const result = await nodeClass.run(createConditionNodeData('test-uuid', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})
})
})
20 changes: 20 additions & 0 deletions packages/components/nodes/agentflow/Condition/Condition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { CommonType, ICommonObject, ICondition, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import removeMarkdown from 'remove-markdown'

/**
* Unescapes a regex pattern that was escaped by Flowise input handling.
* Flowise escapes these characters: \ → \\, [ → \[, ] → \], * → \*
* We reverse this to get the user's intended regex pattern.
*/
const unescapeRegexPattern = (escaped: string): string => {
return escaped
.replace(/\\\\/g, '\0') // Preserve intentional backslashes
.replace(/\\([[\]*])/g, '$1') // Unescape only: [ ] *
.replace(/\0/g, '\\') // Restore preserved backslashes
}

class Condition_Agentflow implements INode {
label: string
name: string
Expand Down Expand Up @@ -275,6 +287,14 @@ class Condition_Agentflow implements INode {
smaller: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) < (Number(value2) || 0),
smallerEqual: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) <= (Number(value2) || 0),
startsWith: (value1: CommonType, value2: CommonType) => (value1 as string).startsWith(value2 as string),
regex: (value1: CommonType, value2: CommonType) => {
try {
const pattern = unescapeRegexPattern((value2 || '').toString())
return new RegExp(pattern).test((value1 || '').toString())
} catch {
return false
}
},
isEmpty: (value1: CommonType) => [undefined, null, ''].includes(value1 as string),
notEmpty: (value1: CommonType) => ![undefined, null, ''].includes(value1 as string)
}
Expand Down