Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
218 changes: 218 additions & 0 deletions packages/components/nodes/agentflow/Condition/Condition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
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)

})

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)

})
})

describe('Flowise input escaping', () => {
// Flowise escapes special characters: \ → \\, [ → \[, ] → \]
// Our regex handler unescapes these

it('should unescape brackets in pattern', async () => {
// User typed [0-9]+, Flowise stored as \[0-9\]+
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 () => {
// User typed \d+, Flowise stored as \\d+ (2 backslashes)
// In JS string literal: '\\\\d+' represents 2 actual backslashes + d+
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)
})
})

describe('Complex regex patterns - all metacharacters', () => {
// Proving that Flowise does NOT escape: ^, $, ., *, +, ?, (, ), {, }, |

it('should handle ^ (start anchor)', async () => {
const conditions = [{ type: 'string', value1: 'hello world', operation: 'regex', value2: '^hello' }]
const result = await new Condition_Agentflow().run(createConditionNodeData('test-caret', conditions), '', { agentflowRuntime: { state: {} } })
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For consistency with the other test suites in this file, please use the nodeClass instance created in the beforeEach block instead of creating a new Condition_Agentflow instance in each test. This should be applied to all tests within the 'Complex regex patterns - all metacharacters' describe block.

            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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().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',
// ^[a-z]+@[a-z]+\.(com|org)$ - the \. needs escaping in JS
value2: '^[a-z]+@[a-z]+\\.(com|org)$'
}]
const result = await new Condition_Agentflow().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 new Condition_Agentflow().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 new Condition_Agentflow().run(createConditionNodeData('test-uuid', conditions), '', { agentflowRuntime: { state: {} } })
expect(result.output.conditions[0].isFulfilled).toBe(true)
})
})
})

21 changes: 21 additions & 0 deletions packages/components/nodes/agentflow/Condition/Condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,27 @@ 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) => {
/**
* Unescapes a regex pattern that was escaped by Flowise input handling.
* Flowise escapes regex metacharacters in inputs by prefixing with backslash.
* 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 all regex metacharacters
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The set of metacharacters being unescaped here is too broad and can cause incorrect regex evaluation. According to the provided tests and description, Flowise only escapes [, ], and \. This implementation, however, unescapes many other metacharacters like ., *, +, ?, etc. This would cause a pattern like \. (intended to match a literal dot) to become ., which matches any character.

The unescaping should be restricted to only [ and ], as \ is already handled by the surrounding replace calls.

Suggested change
.replace(/\\([[\].*+?^${}()|])/g, '$1') // Unescape all regex metacharacters
.replace(/\([[\]])/g, '$1') // Unescape only brackets, as other metachars aren't escaped by Flowise

.replace(/\0/g, '\\') // Restore preserved backslashes
}

try {
const pattern = unescapeRegexPattern((value2 || '').toString())
return new RegExp(pattern).test((value1 || '').toString())
} catch {
// Invalid regex pattern - fail gracefully
return false
}
},
isEmpty: (value1: CommonType) => [undefined, null, ''].includes(value1 as string),
notEmpty: (value1: CommonType) => ![undefined, null, ''].includes(value1 as string)
}
Expand Down