Files
sim/apps/sim/executor/handlers/condition/condition-handler.test.ts

354 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockType } from '@/executor/constants'
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
import type { BlockState, ExecutionContext } from '@/executor/types'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
})),
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.mock('@/tools', () => ({
executeTool: vi.fn(),
}))
vi.mock('@/executor/utils/block-data', () => ({
collectBlockData: vi.fn(() => ({
blockData: { 'source-block-1': { value: 10, text: 'hello' } },
blockNameMapping: { 'Source Block': 'source-block-1' },
})),
}))
import { collectBlockData } from '@/executor/utils/block-data'
import { executeTool } from '@/tools'
const mockExecuteTool = executeTool as ReturnType<typeof vi.fn>
const mockCollectBlockData = collectBlockData as ReturnType<typeof vi.fn>
/**
* Simulates what the function_execute tool does when evaluating condition code
*/
function simulateConditionExecution(code: string): {
success: boolean
output?: { result: unknown }
error?: string
} {
try {
// The code is in format: "const context = {...};\nreturn Boolean(...)"
// We need to execute it and return the result
const fn = new Function(code)
const result = fn()
return { success: true, output: { result } }
} catch (error: any) {
return {
success: false,
error: error.message,
}
}
}
describe('ConditionBlockHandler', () => {
let handler: ConditionBlockHandler
let mockBlock: SerializedBlock
let mockContext: ExecutionContext
let mockWorkflow: Partial<SerializedWorkflow>
let mockSourceBlock: SerializedBlock
let mockTargetBlock1: SerializedBlock
let mockTargetBlock2: SerializedBlock
beforeEach(() => {
mockSourceBlock = {
id: 'source-block-1',
metadata: { id: 'source', name: 'Source Block' },
position: { x: 10, y: 10 },
config: { tool: 'source_tool', params: {} },
inputs: {},
outputs: {},
enabled: true,
}
mockBlock = {
id: 'cond-block-1',
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
position: { x: 50, y: 50 },
config: { tool: BlockType.CONDITION, params: {} },
inputs: { conditions: 'json' },
outputs: {},
enabled: true,
}
mockTargetBlock1 = {
id: 'target-block-1',
metadata: { id: 'target', name: 'Target Block 1' },
position: { x: 100, y: 100 },
config: { tool: 'target_tool_1', params: {} },
inputs: {},
outputs: {},
enabled: true,
}
mockTargetBlock2 = {
id: 'target-block-2',
metadata: { id: 'target', name: 'Target Block 2' },
position: { x: 100, y: 150 },
config: { tool: 'target_tool_2', params: {} },
inputs: {},
outputs: {},
enabled: true,
}
mockWorkflow = {
blocks: [mockSourceBlock, mockBlock, mockTargetBlock1, mockTargetBlock2],
connections: [
{ source: mockSourceBlock.id, target: mockBlock.id },
{
source: mockBlock.id,
target: mockTargetBlock1.id,
sourceHandle: 'condition-cond1',
},
{
source: mockBlock.id,
target: mockTargetBlock2.id,
sourceHandle: 'condition-else1',
},
],
}
handler = new ConditionBlockHandler()
mockContext = {
workflowId: 'test-workflow-id',
workspaceId: 'test-workspace-id',
blockStates: new Map<string, BlockState>([
[
mockSourceBlock.id,
{
output: { value: 10, text: 'hello' },
executed: true,
executionTime: 100,
},
],
]),
blockLogs: [],
metadata: { duration: 0 },
environmentVariables: { API_KEY: 'test-key' },
workflowVariables: { userName: { name: 'userName', value: 'john', type: 'plain' } },
decisions: { router: new Map(), condition: new Map() },
loopExecutions: new Map(),
executedBlocks: new Set([mockSourceBlock.id]),
activeExecutionPath: new Set(),
workflow: mockWorkflow as SerializedWorkflow,
completedLoops: new Set(),
}
vi.clearAllMocks()
mockExecuteTool.mockImplementation(async (_toolId: string, params: { code: string }) => {
return simulateConditionExecution(params.code)
})
})
it('should handle condition blocks', () => {
expect(handler.canHandle(mockBlock)).toBe(true)
const nonCondBlock: SerializedBlock = { ...mockBlock, metadata: { id: 'other' } }
expect(handler.canHandle(nonCondBlock)).toBe(false)
})
it('should execute condition block correctly and select first path', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.value > 5' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
const expectedOutput = {
value: 10,
text: 'hello',
conditionResult: true,
selectedPath: {
blockId: mockTargetBlock1.id,
blockType: 'target',
blockTitle: 'Target Block 1',
},
selectedOption: 'cond1',
}
const result = await handler.execute(mockContext, mockBlock, inputs)
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
it('should pass correct parameters to function_execute tool', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.value > 5' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
await handler.execute(mockContext, mockBlock, inputs)
expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
code: expect.stringContaining('context.value > 5'),
timeout: 5000,
envVars: mockContext.environmentVariables,
workflowVariables: mockContext.workflowVariables,
blockData: { 'source-block-1': { value: 10, text: 'hello' } },
blockNameMapping: { 'Source Block': 'source-block-1' },
_context: {
workflowId: 'test-workflow-id',
workspaceId: 'test-workspace-id',
},
}),
false,
false,
mockContext
)
})
it('should select the else path if other conditions fail', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.value < 0' }, // Should fail (10 < 0 is false)
{ id: 'else1', title: 'else', value: '' }, // Should be selected
]
const inputs = { conditions: JSON.stringify(conditions) }
const expectedOutput = {
value: 10,
text: 'hello',
conditionResult: true,
selectedPath: {
blockId: mockTargetBlock2.id,
blockType: 'target',
blockTitle: 'Target Block 2',
},
selectedOption: 'else1',
}
const result = await handler.execute(mockContext, mockBlock, inputs)
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
})
it('should handle invalid conditions JSON format', async () => {
const inputs = { conditions: '{ "invalid json ' }
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
/^Invalid conditions format:/
)
})
it('should handle evaluation errors gracefully', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.nonExistentProperty.doSomething()' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
/Evaluation error in condition "if".*doSomething/
)
})
it('should handle missing source block output gracefully', async () => {
const conditions = [{ id: 'cond1', title: 'if', value: 'true' }]
const inputs = { conditions: JSON.stringify(conditions) }
const contextWithoutSource = {
...mockContext,
blockStates: new Map<string, BlockState>(),
}
const result = await handler.execute(contextWithoutSource, mockBlock, inputs)
expect(result).toHaveProperty('conditionResult', true)
expect(result).toHaveProperty('selectedOption', 'cond1')
})
it('should throw error if target block is missing', async () => {
const conditions = [{ id: 'cond1', title: 'if', value: 'true' }]
const inputs = { conditions: JSON.stringify(conditions) }
mockContext.workflow!.blocks = [mockSourceBlock, mockBlock, mockTargetBlock2]
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
`Target block ${mockTargetBlock1.id} not found`
)
})
it('should return no-match result if no condition matches and no else exists', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'false' },
{ id: 'cond2', title: 'else if', value: 'context.value === 99' },
]
const inputs = { conditions: JSON.stringify(conditions) }
mockContext.workflow!.connections = [
{ source: mockSourceBlock.id, target: mockBlock.id },
{
source: mockBlock.id,
target: mockTargetBlock1.id,
sourceHandle: 'condition-cond1',
},
]
const result = await handler.execute(mockContext, mockBlock, inputs)
expect((result as any).conditionResult).toBe(false)
expect((result as any).selectedPath).toBeNull()
expect((result as any).selectedOption).toBeNull()
expect(mockContext.decisions.condition.has(mockBlock.id)).toBe(false)
})
it('falls back to else path when loop context data is unavailable', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.item === "apple"' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
const result = await handler.execute(mockContext, mockBlock, inputs)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
expect((result as any).selectedOption).toBe('else1')
})
it('should use collectBlockData to gather block state', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'true' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
await handler.execute(mockContext, mockBlock, inputs)
expect(mockCollectBlockData).toHaveBeenCalledWith(mockContext)
})
it('should handle function_execute tool failure', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: 'context.value > 5' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
mockExecuteTool.mockResolvedValueOnce({
success: false,
error: 'Execution timeout',
})
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
/Evaluation error in condition "if".*Execution timeout/
)
})
})