Files
sim/apps/sim/executor/handlers/workflow/workflow-handler.test.ts
Waleed ebc2ffa1c5 fix(agent): always fetch latest custom tool from DB when customToolId is present (#3208)
* fix(agent): always fetch latest custom tool from DB when customToolId is present

* test(agent): use generic test data for customToolId resolution tests

* fix(agent): mock buildAuthHeaders in tests for CI compatibility

* remove inline mocks in favor of sim/testing ones
2026-02-12 15:31:11 -08:00

248 lines
7.0 KiB
TypeScript

import { setupGlobalFetchMock } from '@sim/testing'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { BlockType } from '@/executor/constants'
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
vi.mock('@/lib/auth/internal', () => ({
generateInternalToken: vi.fn().mockResolvedValue('test-token'),
}))
// Mock fetch globally
setupGlobalFetchMock()
describe('WorkflowBlockHandler', () => {
let handler: WorkflowBlockHandler
let mockBlock: SerializedBlock
let mockContext: ExecutionContext
let mockFetch: Mock
beforeEach(() => {
// Mock window.location.origin for getBaseUrl()
;(global as any).window = {
location: {
origin: 'http://localhost:3000',
},
}
handler = new WorkflowBlockHandler()
mockFetch = global.fetch as Mock
mockBlock = {
id: 'workflow-block-1',
metadata: { id: BlockType.WORKFLOW, name: 'Test Workflow Block' },
position: { x: 0, y: 0 },
config: { tool: BlockType.WORKFLOW, params: {} },
inputs: { workflowId: 'string' },
outputs: {},
enabled: true,
}
mockContext = {
workflowId: 'parent-workflow-id',
blockStates: new Map(),
blockLogs: [],
metadata: { duration: 0 },
environmentVariables: {},
decisions: { router: new Map(), condition: new Map() },
loopExecutions: new Map(),
executedBlocks: new Set(),
activeExecutionPath: new Set(),
completedLoops: new Set(),
workflow: {
version: '1.0',
blocks: [],
connections: [],
loops: {},
},
}
// Reset all mocks
vi.clearAllMocks()
// Setup default fetch mock
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
data: {
name: 'Child Workflow',
state: {
blocks: [
{
id: 'starter',
metadata: { id: BlockType.STARTER, name: 'Starter' },
position: { x: 0, y: 0 },
config: { tool: BlockType.STARTER, params: {} },
inputs: {},
outputs: {},
enabled: true,
},
],
edges: [],
loops: {},
parallels: {},
},
},
}),
})
})
describe('canHandle', () => {
it('should handle workflow blocks', () => {
expect(handler.canHandle(mockBlock)).toBe(true)
})
it('should not handle non-workflow blocks', () => {
const nonWorkflowBlock = { ...mockBlock, metadata: { id: BlockType.FUNCTION } }
expect(handler.canHandle(nonWorkflowBlock)).toBe(false)
})
})
describe('execute', () => {
it('should throw error when no workflowId is provided', async () => {
const inputs = {}
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'No workflow selected for execution'
)
})
it('should enforce maximum depth limit', async () => {
const inputs = { workflowId: 'child-workflow-id' }
// Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10)
const deepContext = {
...mockContext,
workflowId:
'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11',
}
await expect(handler.execute(deepContext, mockBlock, inputs)).rejects.toThrow(
'"child-workflow-id" failed: Maximum workflow nesting depth of 10 exceeded'
)
})
it('should handle child workflow not found', async () => {
const inputs = { workflowId: 'non-existent-workflow' }
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
})
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'"non-existent-workflow" failed: Child workflow non-existent-workflow not found'
)
})
it('should handle fetch errors gracefully', async () => {
const inputs = { workflowId: 'child-workflow-id' }
mockFetch.mockRejectedValueOnce(new Error('Network error'))
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'"child-workflow-id" failed: Network error'
)
})
})
describe('loadChildWorkflow', () => {
it('should return null for 404 responses', async () => {
const workflowId = 'non-existent-workflow'
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
})
const result = await (handler as any).loadChildWorkflow(workflowId)
expect(result).toBeNull()
})
it('should handle invalid workflow state', async () => {
const workflowId = 'invalid-workflow'
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: {
name: 'Invalid Workflow',
state: null, // Invalid state
},
}),
})
await expect((handler as any).loadChildWorkflow(workflowId)).rejects.toThrow(
'Child workflow invalid-workflow has invalid state'
)
})
})
describe('mapChildOutputToParent', () => {
it('should map successful child output correctly', () => {
const childResult = {
success: true,
output: { data: 'test result' },
}
const result = (handler as any).mapChildOutputToParent(
childResult,
'child-id',
'Child Workflow',
100
)
expect(result).toEqual({
success: true,
childWorkflowId: 'child-id',
childWorkflowName: 'Child Workflow',
result: { data: 'test result' },
childTraceSpans: [],
})
})
it('should throw error for failed child output so BlockExecutor can check error port', () => {
const childResult = {
success: false,
error: 'Child workflow failed',
}
expect(() =>
(handler as any).mapChildOutputToParent(childResult, 'child-id', 'Child Workflow', 100)
).toThrow('"Child Workflow" failed: Child workflow failed')
try {
;(handler as any).mapChildOutputToParent(childResult, 'child-id', 'Child Workflow', 100)
} catch (error: any) {
expect(error.childTraceSpans).toEqual([])
}
})
it('should handle nested response structures', () => {
const childResult = {
output: { nested: 'data' },
}
const result = (handler as any).mapChildOutputToParent(
childResult,
'child-id',
'Child Workflow',
100
)
expect(result).toEqual({
success: true,
childWorkflowId: 'child-id',
childWorkflowName: 'Child Workflow',
result: { nested: 'data' },
childTraceSpans: [],
})
})
})
})