diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx index aca7e8fc4..bc6432283 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx @@ -24,8 +24,8 @@ interface FolderContextMenuProps { folderId: string folderName: string onCreateWorkflow: (folderId: string) => void - onRename?: (folderId: string, newName: string) => void onDelete?: (folderId: string) => void + onStartEdit?: () => void level: number } @@ -33,23 +33,20 @@ export function FolderContextMenu({ folderId, folderName, onCreateWorkflow, - onRename, onDelete, + onStartEdit, level, }: FolderContextMenuProps) { const [showSubfolderDialog, setShowSubfolderDialog] = useState(false) - const [showRenameDialog, setShowRenameDialog] = useState(false) const [subfolderName, setSubfolderName] = useState('') - const [renameName, setRenameName] = useState(folderName) const [isCreating, setIsCreating] = useState(false) - const [isRenaming, setIsRenaming] = useState(false) const params = useParams() const workspaceId = params.workspaceId as string // Get user permissions for the workspace const userPermissions = useUserPermissionsContext() - const { createFolder, updateFolder, deleteFolder } = useFolderStore() + const { createFolder, deleteFolder } = useFolderStore() const handleCreateWorkflow = () => { onCreateWorkflow(folderId) @@ -60,8 +57,9 @@ export function FolderContextMenu({ } const handleRename = () => { - setRenameName(folderName) - setShowRenameDialog(true) + if (onStartEdit) { + onStartEdit() + } } const handleDelete = async () => { @@ -98,31 +96,9 @@ export function FolderContextMenu({ } } - const handleRenameSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!renameName.trim()) return - - setIsRenaming(true) - try { - if (onRename) { - onRename(folderId, renameName.trim()) - } else { - // Default rename behavior - await updateFolder(folderId, { name: renameName.trim() }) - } - setShowRenameDialog(false) - } catch (error) { - console.error('Failed to rename folder:', error) - } finally { - setIsRenaming(false) - } - } - const handleCancel = () => { setSubfolderName('') setShowSubfolderDialog(false) - setRenameName(folderName) - setShowRenameDialog(false) } return ( @@ -230,37 +206,6 @@ export function FolderContextMenu({ - - {/* Rename dialog */} - - e.stopPropagation()}> - - Rename Folder - -
-
- - setRenameName(e.target.value)} - placeholder='Enter folder name...' - maxLength={50} - autoFocus - required - /> -
-
- - -
-
-
-
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx index c867c1bbc..5d793abbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-tree/components/folder-item.tsx @@ -14,6 +14,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' +import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { createLogger } from '@/lib/logs/console-logger' import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store' @@ -48,14 +49,33 @@ export function FolderItem({ const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isDragging, setIsDragging] = useState(false) + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState(folder.name) + const [isRenaming, setIsRenaming] = useState(false) const dragStartedRef = useRef(false) + const inputRef = useRef(null) const params = useParams() const workspaceId = params.workspaceId as string const isExpanded = expandedFolders.has(folder.id) const updateTimeoutRef = useRef | undefined>(undefined) const pendingStateRef = useRef(null) + // Update editValue when folder name changes + useEffect(() => { + setEditValue(folder.name) + }, [folder.name]) + + // Focus input when entering edit mode + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus() + inputRef.current.select() + } + }, [isEditing]) + const handleToggleExpanded = useCallback(() => { + if (isEditing) return // Don't toggle when editing + const newExpandedState = !isExpanded toggleExpanded(folder.id) pendingStateRef.current = newExpandedState @@ -73,9 +93,11 @@ export function FolderItem({ }) } }, 300) - }, [folder.id, isExpanded, toggleExpanded, updateFolderAPI]) + }, [folder.id, isExpanded, toggleExpanded, updateFolderAPI, isEditing]) const handleDragStart = (e: React.DragEvent) => { + if (isEditing) return + dragStartedRef.current = true setIsDragging(true) @@ -101,7 +123,7 @@ export function FolderItem({ } const handleClick = (e: React.MouseEvent) => { - if (dragStartedRef.current) { + if (dragStartedRef.current || isEditing) { e.preventDefault() return } @@ -116,15 +138,57 @@ export function FolderItem({ } }, []) - const handleRename = async (folderId: string, newName: string) => { + const handleStartEdit = () => { + setIsEditing(true) + setEditValue(folder.name) + } + + const handleSaveEdit = async () => { + if (!editValue.trim() || editValue.trim() === folder.name) { + setIsEditing(false) + setEditValue(folder.name) + return + } + + setIsRenaming(true) try { - await updateFolderAPI(folderId, { name: newName }) + await updateFolderAPI(folder.id, { name: editValue.trim() }) + logger.info(`Successfully renamed folder from "${folder.name}" to "${editValue.trim()}"`) + setIsEditing(false) } catch (error) { - logger.error('Failed to rename folder:', { error }) + logger.error('Failed to rename folder:', { + error, + folderId: folder.id, + oldName: folder.name, + newName: editValue.trim(), + }) + // Reset to original name on error + setEditValue(folder.name) + } finally { + setIsRenaming(false) } } - const handleDelete = async (folderId: string) => { + const handleCancelEdit = () => { + setIsEditing(false) + setEditValue(folder.name) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSaveEdit() + } else if (e.key === 'Escape') { + e.preventDefault() + handleCancelEdit() + } + } + + const handleInputBlur = () => { + handleSaveEdit() + } + + const handleDelete = async () => { setShowDeleteDialog(true) } @@ -154,7 +218,7 @@ export function FolderItem({ onDragLeave={onDragLeave} onDrop={onDrop} onClick={handleClick} - draggable={true} + draggable={!isEditing} onDragStart={handleDragStart} onDragEnd={handleDragEnd} > @@ -222,7 +286,7 @@ export function FolderItem({ maxWidth: isFirstItem ? `${164 - level * 20}px` : `${206 - level * 20}px`, }} onClick={handleClick} - draggable={true} + draggable={!isEditing} onDragStart={handleDragStart} onDragEnd={handleDragEnd} > @@ -234,18 +298,34 @@ export function FolderItem({ )} - {folder.name} - -
e.stopPropagation()}> - setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleInputBlur} + className='h-6 flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' + maxLength={50} + disabled={isRenaming} + onClick={(e) => e.stopPropagation()} // Prevent folder toggle when clicking input /> -
+ ) : ( + {folder.name} + )} + + {!isEditing && ( +
e.stopPropagation()}> + +
+ )} diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 02f3cd2ad..2b9f13679 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -21,7 +21,7 @@ export const GoogleCalendarBlock: BlockConfig = { description: 'Manage Google Calendar events', longDescription: "Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. Email invitations are sent asynchronously and delivery depends on recipients' Google Calendar settings.", - docsLink: 'https://docs.simstudio.ai/tools/google-calendar', + docsLink: 'https://docs.simstudio.ai/tools/google_calendar', category: 'tools', bgColor: '#E0E0E0', icon: GoogleCalendarIcon, diff --git a/apps/sim/executor/consts.ts b/apps/sim/executor/consts.ts new file mode 100644 index 000000000..b5ebea715 --- /dev/null +++ b/apps/sim/executor/consts.ts @@ -0,0 +1,29 @@ +/** + * Enum defining all supported block types in the executor. + * This centralizes block type definitions and eliminates magic strings. + */ +export enum BlockType { + PARALLEL = 'parallel', + LOOP = 'loop', + ROUTER = 'router', + CONDITION = 'condition', + FUNCTION = 'function', + AGENT = 'agent', + API = 'api', + EVALUATOR = 'evaluator', + RESPONSE = 'response', + WORKFLOW = 'workflow', + STARTER = 'starter', +} + +/** + * Array of all block types for iteration and validation + */ +export const ALL_BLOCK_TYPES = Object.values(BlockType) as string[] + +/** + * Type guard to check if a string is a valid block type + */ +export function isValidBlockType(type: string): type is BlockType { + return ALL_BLOCK_TYPES.includes(type) +} diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index b6933907e..c260c0489 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -1,12 +1,13 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { isHosted } from '@/lib/environment' import { getAllBlocks } from '@/blocks' +import { BlockType } from '@/executor/consts' +import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' +import type { ExecutionContext, StreamingExecution } from '@/executor/types' import { executeProviderRequest } from '@/providers' import { getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { executeTool } from '@/tools' -import type { ExecutionContext, StreamingExecution } from '../../types' -import { AgentBlockHandler } from './agent-handler' process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' @@ -88,8 +89,8 @@ describe('AgentBlockHandler', () => { mockBlock = { id: 'test-agent-block', - metadata: { id: 'agent', name: 'Test Agent' }, - type: 'agent', + metadata: { id: BlockType.AGENT, name: 'Test Agent' }, + type: BlockType.AGENT, position: { x: 0, y: 0 }, config: { tool: 'mock-tool', @@ -908,7 +909,7 @@ describe('AgentBlockHandler', () => { logs: [ { blockId: 'some-id', - blockType: 'agent', + blockType: BlockType.AGENT, startedAt: new Date().toISOString(), endedAt: new Date().toISOString(), durationMs: 100, @@ -959,7 +960,7 @@ describe('AgentBlockHandler', () => { const logs = (result as StreamingExecution).execution.logs expect(logs?.length).toBe(1) if (logs && logs.length > 0 && logs[0]) { - expect(logs[0].blockType).toBe('agent') + expect(logs[0].blockType).toBe(BlockType.AGENT) } }) @@ -1069,7 +1070,7 @@ describe('AgentBlockHandler', () => { memories: [ { key: 'conversation-1', - type: 'agent', + type: BlockType.AGENT, data: [ { role: 'user', content: 'Hi there!' }, { role: 'assistant', content: 'Hello! How can I help you?' }, diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 940f44e0b..b739c71f0 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -2,13 +2,19 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console-logger' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { + AgentInputs, + Message, + StreamingConfig, + ToolInput, +} from '@/executor/handlers/agent/types' +import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types' import { executeProviderRequest } from '@/providers' import { getApiKey, getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool, getToolAsync } from '@/tools/utils' -import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types' -import type { AgentInputs, Message, StreamingConfig, ToolInput } from './types' const logger = createLogger('AgentBlockHandler') @@ -22,7 +28,7 @@ const CUSTOM_TOOL_PREFIX = 'custom_' */ export class AgentBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'agent' + return block.metadata?.id === BlockType.AGENT } async execute( diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index 54927db80..74b84357d 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -1,12 +1,13 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { ApiBlockHandler } from '@/executor/handlers/api/api-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import type { ToolConfig } from '@/tools/types' import { getTool } from '@/tools/utils' -import type { ExecutionContext } from '../../types' -import { ApiBlockHandler } from './api-handler' const mockGetTool = vi.mocked(getTool) const mockExecuteTool = executeTool as Mock @@ -21,7 +22,7 @@ describe('ApiBlockHandler', () => { handler = new ApiBlockHandler() mockBlock = { id: 'api-block-1', - metadata: { id: 'api', name: 'Test API Block' }, + metadata: { id: BlockType.API, name: 'Test API Block' }, position: { x: 10, y: 10 }, config: { tool: 'http_request', params: {} }, inputs: {}, diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index 31c7df53d..acc8b7611 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -1,8 +1,9 @@ import { createLogger } from '@/lib/logs/console-logger' +import { BlockType } from '@/executor/consts' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('ApiBlockHandler') @@ -11,7 +12,7 @@ const logger = createLogger('ApiBlockHandler') */ export class ApiBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'api' + return block.metadata?.id === BlockType.API } async execute( diff --git a/apps/sim/executor/handlers/condition/condition-handler.test.ts b/apps/sim/executor/handlers/condition/condition-handler.test.ts index 9d7e91cbc..226900cec 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.test.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.test.ts @@ -1,11 +1,12 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mocked, type MockedClass, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler' +import { PathTracker } from '@/executor/path/path' +import { InputResolver } from '@/executor/resolver/resolver' +import type { BlockState, ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { PathTracker } from '../../path' -import { InputResolver } from '../../resolver' -import type { BlockState, ExecutionContext } from '../../types' -import { ConditionBlockHandler } from './condition-handler' const MockPathTracker = PathTracker as MockedClass const MockInputResolver = InputResolver as MockedClass @@ -34,9 +35,9 @@ describe('ConditionBlockHandler', () => { } mockBlock = { id: 'cond-block-1', - metadata: { id: 'condition', name: 'Test Condition' }, + metadata: { id: BlockType.CONDITION, name: 'Test Condition' }, position: { x: 50, y: 50 }, - config: { tool: 'condition', params: {} }, + config: { tool: BlockType.CONDITION, params: {} }, inputs: { conditions: 'json' }, // Corrected based on previous step outputs: {}, enabled: true, diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index 3f479fdf2..be4a4c841 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -1,9 +1,10 @@ import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { PathTracker } from '@/executor/path/path' +import type { InputResolver } from '@/executor/resolver/resolver' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { PathTracker } from '../../path' -import type { InputResolver } from '../../resolver' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('ConditionBlockHandler') @@ -21,7 +22,7 @@ export class ConditionBlockHandler implements BlockHandler { ) {} canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'condition' + return block.metadata?.id === BlockType.CONDITION } async execute( diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index 096f3ca6a..a35ff4d3a 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -1,10 +1,11 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' +import type { ExecutionContext } from '@/executor/types' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' -import type { ExecutionContext } from '../../types' -import { EvaluatorBlockHandler } from './evaluator-handler' const mockGetProviderFromModel = getProviderFromModel as Mock const mockFetch = global.fetch as unknown as Mock @@ -19,9 +20,9 @@ describe('EvaluatorBlockHandler', () => { mockBlock = { id: 'eval-block-1', - metadata: { id: 'evaluator', name: 'Test Evaluator' }, + metadata: { id: BlockType.EVALUATOR, name: 'Test Evaluator' }, position: { x: 20, y: 20 }, - config: { tool: 'evaluator', params: {} }, + config: { tool: BlockType.EVALUATOR, params: {} }, inputs: { content: 'string', metrics: 'json', diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 8d41c5bd7..9616d0d5b 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -1,9 +1,10 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('EvaluatorBlockHandler') @@ -12,7 +13,7 @@ const logger = createLogger('EvaluatorBlockHandler') */ export class EvaluatorBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'evaluator' + return block.metadata?.id === BlockType.EVALUATOR } async execute( diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index f40f5be94..5d8493c1a 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -1,8 +1,9 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' -import type { ExecutionContext } from '../../types' -import { FunctionBlockHandler } from './function-handler' vi.mock('@/lib/logs/console-logger', () => ({ createLogger: vi.fn(() => ({ @@ -29,9 +30,9 @@ describe('FunctionBlockHandler', () => { mockBlock = { id: 'func-block-1', - metadata: { id: 'function', name: 'Test Function' }, + metadata: { id: BlockType.FUNCTION, name: 'Test Function' }, position: { x: 30, y: 30 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: { code: 'string', timeout: 'number' }, // Using ParamType strings outputs: {}, enabled: true, diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index d895093b4..dd84d1a65 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -1,7 +1,8 @@ import { createLogger } from '@/lib/logs/console-logger' +import { BlockType } from '@/executor/consts' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('FunctionBlockHandler') @@ -10,7 +11,7 @@ const logger = createLogger('FunctionBlockHandler') */ export class FunctionBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'function' + return block.metadata?.id === BlockType.FUNCTION } async execute( diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index 2cd7b33c7..22339e5b3 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -1,12 +1,13 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import type { ToolConfig } from '@/tools/types' import { getTool } from '@/tools/utils' -import type { ExecutionContext } from '../../types' -import { GenericBlockHandler } from './generic-handler' const mockGetTool = vi.mocked(getTool) const mockExecuteTool = executeTool as Mock @@ -74,7 +75,7 @@ describe('GenericBlockHandler', () => { }) it.concurrent('should always handle any block type', () => { - const agentBlock: SerializedBlock = { ...mockBlock, metadata: { id: 'agent' } } + const agentBlock: SerializedBlock = { ...mockBlock, metadata: { id: BlockType.AGENT } } expect(handler.canHandle(agentBlock)).toBe(true) expect(handler.canHandle(mockBlock)).toBe(true) const noMetaIdBlock: SerializedBlock = { ...mockBlock, metadata: undefined } diff --git a/apps/sim/executor/handlers/generic/generic-handler.ts b/apps/sim/executor/handlers/generic/generic-handler.ts index 513f5f990..7a963950c 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.ts @@ -1,8 +1,8 @@ import { createLogger } from '@/lib/logs/console-logger' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('GenericBlockHandler') diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index 754832ce8..20c65456e 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -1,14 +1,14 @@ -import { AgentBlockHandler } from './agent/agent-handler' -import { ApiBlockHandler } from './api/api-handler' -import { ConditionBlockHandler } from './condition/condition-handler' -import { EvaluatorBlockHandler } from './evaluator/evaluator-handler' -import { FunctionBlockHandler } from './function/function-handler' -import { GenericBlockHandler } from './generic/generic-handler' -import { LoopBlockHandler } from './loop/loop-handler' -import { ParallelBlockHandler } from './parallel/parallel-handler' -import { ResponseBlockHandler } from './response/response-handler' -import { RouterBlockHandler } from './router/router-handler' -import { WorkflowBlockHandler } from './workflow/workflow-handler' +import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' +import { ApiBlockHandler } from '@/executor/handlers/api/api-handler' +import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler' +import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' +import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' +import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' +import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler' +import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler' +import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' +import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' +import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' export { AgentBlockHandler, diff --git a/apps/sim/executor/handlers/loop/loop-handler.test.ts b/apps/sim/executor/handlers/loop/loop-handler.test.ts index 3c7e7a13e..1f3ebec7a 100644 --- a/apps/sim/executor/handlers/loop/loop-handler.test.ts +++ b/apps/sim/executor/handlers/loop/loop-handler.test.ts @@ -1,22 +1,28 @@ +import { vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { ExecutionContext } from '../../types' -import { LoopBlockHandler } from './loop-handler' describe('LoopBlockHandler', () => { let handler: LoopBlockHandler let mockContext: ExecutionContext let mockBlock: SerializedBlock + const mockPathTracker = { + isInActivePath: vi.fn(), + } + beforeEach(() => { handler = new LoopBlockHandler() mockBlock = { id: 'loop-1', position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'loop', name: 'Test Loop' }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, enabled: true, } @@ -66,7 +72,7 @@ describe('LoopBlockHandler', () => { it('should not handle non-loop blocks', () => { if (mockBlock.metadata) { - mockBlock.metadata.id = 'function' + mockBlock.metadata.id = BlockType.FUNCTION } expect(handler.canHandle(mockBlock)).toBe(false) }) @@ -212,4 +218,46 @@ describe('LoopBlockHandler', () => { ) }) }) + + describe('PathTracker integration', () => { + it('should activate children when in active path', async () => { + const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any) + + // Mock PathTracker to return true (block is in active path) + mockPathTracker.isInActivePath.mockReturnValue(true) + + await handlerWithPathTracker.execute(mockBlock, {}, mockContext) + + // Should activate children when in active path + expect(mockContext.activeExecutionPath.has('inner-block')).toBe(true) + expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('loop-1', mockContext) + }) + + it('should not activate children when not in active path', async () => { + const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any) + + // Mock PathTracker to return false (block is not in active path) + mockPathTracker.isInActivePath.mockReturnValue(false) + + await handlerWithPathTracker.execute(mockBlock, {}, mockContext) + + // Should not activate children when not in active path + expect(mockContext.activeExecutionPath.has('inner-block')).toBe(false) + expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('loop-1', mockContext) + }) + + it('should handle PathTracker errors gracefully', async () => { + const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any) + + // Mock PathTracker to throw error + mockPathTracker.isInActivePath.mockImplementation(() => { + throw new Error('PathTracker error') + }) + + await handlerWithPathTracker.execute(mockBlock, {}, mockContext) + + // Should default to activating children when PathTracker fails + expect(mockContext.activeExecutionPath.has('inner-block')).toBe(true) + }) + }) }) diff --git a/apps/sim/executor/handlers/loop/loop-handler.ts b/apps/sim/executor/handlers/loop/loop-handler.ts index 5795355ab..a6713a200 100644 --- a/apps/sim/executor/handlers/loop/loop-handler.ts +++ b/apps/sim/executor/handlers/loop/loop-handler.ts @@ -1,8 +1,11 @@ import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { PathTracker } from '@/executor/path/path' +import type { InputResolver } from '@/executor/resolver/resolver' +import { Routing } from '@/executor/routing/routing' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { InputResolver } from '../../resolver' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('LoopBlockHandler') @@ -13,10 +16,13 @@ const DEFAULT_MAX_ITERATIONS = 5 * Loop blocks don't execute logic themselves but control the flow of blocks within them. */ export class LoopBlockHandler implements BlockHandler { - constructor(private resolver?: InputResolver) {} + constructor( + private resolver?: InputResolver, + private pathTracker?: PathTracker + ) {} canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'loop' + return block.metadata?.id === BlockType.LOOP } async execute( @@ -126,15 +132,31 @@ export class LoopBlockHandler implements BlockHandler { `Loop ${block.id} - Incremented counter for next iteration: ${currentIteration + 1}` ) - // Loop is still active, activate the loop-start-source connection - const loopStartConnections = - context.workflow?.connections.filter( - (conn) => conn.source === block.id && conn.sourceHandle === 'loop-start-source' - ) || [] + // Use routing strategy to determine if this block requires active path checking + const blockType = block.metadata?.id + if (Routing.requiresActivePathCheck(blockType || '')) { + let isInActivePath = true + if (this.pathTracker) { + try { + isInActivePath = this.pathTracker.isInActivePath(block.id, context) + } catch (error) { + logger.warn(`PathTracker check failed for ${blockType} block ${block.id}:`, error) + // Default to true to maintain existing behavior if PathTracker fails + isInActivePath = true + } + } - for (const conn of loopStartConnections) { - context.activeExecutionPath.add(conn.target) - logger.info(`Activated loop start path to ${conn.target} for iteration ${currentIteration}`) + // Only activate child nodes if this block is in the active execution path + if (isInActivePath) { + this.activateChildNodes(block, context, currentIteration) + } else { + logger.info( + `${blockType} block ${block.id} is not in active execution path, skipping child activation` + ) + } + } else { + // Regular blocks always activate their children + this.activateChildNodes(block, context, currentIteration) } return { @@ -147,6 +169,26 @@ export class LoopBlockHandler implements BlockHandler { } as Record } + /** + * Activate child nodes for loop execution + */ + private activateChildNodes( + block: SerializedBlock, + context: ExecutionContext, + currentIteration: number + ): void { + // Loop is still active, activate the loop-start-source connection + const loopStartConnections = + context.workflow?.connections.filter( + (conn) => conn.source === block.id && conn.sourceHandle === 'loop-start-source' + ) || [] + + for (const conn of loopStartConnections) { + context.activeExecutionPath.add(conn.target) + logger.info(`Activated loop start path to ${conn.target} for iteration ${currentIteration}`) + } + } + /** * Evaluates forEach items expression or value */ diff --git a/apps/sim/executor/handlers/parallel/parallel-handler.test.ts b/apps/sim/executor/handlers/parallel/parallel-handler.test.ts index 3eaa01ea3..62f601d49 100644 --- a/apps/sim/executor/handlers/parallel/parallel-handler.test.ts +++ b/apps/sim/executor/handlers/parallel/parallel-handler.test.ts @@ -1,21 +1,26 @@ import { describe, expect, it, vi } from 'vitest' +import { createParallelExecutionState } from '@/executor/__test-utils__/executor-mocks' +import { BlockType } from '@/executor/consts' +import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedParallel } from '@/serializer/types' -import { createParallelExecutionState } from '../../__test-utils__/executor-mocks' -import type { ExecutionContext } from '../../types' -import { ParallelBlockHandler } from './parallel-handler' describe('ParallelBlockHandler', () => { const mockResolver = { resolveBlockReferences: vi.fn((expr: string) => expr), } + const mockPathTracker = { + isInActivePath: vi.fn(), + } + const createMockBlock = (id: string): SerializedBlock => ({ id, position: { x: 0, y: 0 }, config: { tool: '', params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'parallel', name: 'Test Parallel' }, + metadata: { id: BlockType.PARALLEL, name: 'Test Parallel' }, enabled: true, }) @@ -46,7 +51,7 @@ describe('ParallelBlockHandler', () => { expect(handler.canHandle(block)).toBe(true) - const nonParallelBlock = { ...block, metadata: { id: 'agent' } } + const nonParallelBlock = { ...block, metadata: { id: BlockType.AGENT } } expect(handler.canHandle(nonParallelBlock)).toBe(false) }) @@ -394,24 +399,24 @@ describe('ParallelBlockHandler', () => { { id: 'agent-1', position: { x: 0, y: 0 }, - config: { tool: 'agent', params: {} }, + config: { tool: BlockType.AGENT, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'agent', name: 'Agent 1' }, + metadata: { id: BlockType.AGENT, name: 'Agent 1' }, enabled: true, }, { id: 'function-1', position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ;', }, }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Function 1' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, enabled: true, }, ], @@ -481,4 +486,91 @@ describe('ParallelBlockHandler', () => { }).not.toThrow() }) }) + + describe('PathTracker integration', () => { + it('should activate children when in active path', async () => { + const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any) + const block = createMockBlock('parallel-1') + const parallel = { + id: 'parallel-1', + nodes: ['agent-1'], + distribution: ['item1', 'item2'], + } + + const context = createMockContext(parallel) + context.workflow!.connections = [ + { + source: 'parallel-1', + target: 'agent-1', + sourceHandle: 'parallel-start-source', + }, + ] + + // Mock PathTracker to return true (block is in active path) + mockPathTracker.isInActivePath.mockReturnValue(true) + + await handler.execute(block, {}, context) + + // Should activate children when in active path + expect(context.activeExecutionPath.has('agent-1')).toBe(true) + expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('parallel-1', context) + }) + + it('should not activate children when not in active path', async () => { + const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any) + const block = createMockBlock('parallel-1') + const parallel = { + id: 'parallel-1', + nodes: ['agent-1'], + distribution: ['item1', 'item2'], + } + + const context = createMockContext(parallel) + context.workflow!.connections = [ + { + source: 'parallel-1', + target: 'agent-1', + sourceHandle: 'parallel-start-source', + }, + ] + + // Mock PathTracker to return false (block is not in active path) + mockPathTracker.isInActivePath.mockReturnValue(false) + + await handler.execute(block, {}, context) + + // Should not activate children when not in active path + expect(context.activeExecutionPath.has('agent-1')).toBe(false) + expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('parallel-1', context) + }) + + it('should handle PathTracker errors gracefully', async () => { + const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any) + const block = createMockBlock('parallel-1') + const parallel = { + id: 'parallel-1', + nodes: ['agent-1'], + distribution: ['item1', 'item2'], + } + + const context = createMockContext(parallel) + context.workflow!.connections = [ + { + source: 'parallel-1', + target: 'agent-1', + sourceHandle: 'parallel-start-source', + }, + ] + + // Mock PathTracker to throw error + mockPathTracker.isInActivePath.mockImplementation(() => { + throw new Error('PathTracker error') + }) + + await handler.execute(block, {}, context) + + // Should default to activating children when PathTracker fails + expect(context.activeExecutionPath.has('agent-1')).toBe(true) + }) + }) }) diff --git a/apps/sim/executor/handlers/parallel/parallel-handler.ts b/apps/sim/executor/handlers/parallel/parallel-handler.ts index 9aaf17b8a..0a04b770a 100644 --- a/apps/sim/executor/handlers/parallel/parallel-handler.ts +++ b/apps/sim/executor/handlers/parallel/parallel-handler.ts @@ -1,8 +1,11 @@ import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { PathTracker } from '@/executor/path/path' +import type { InputResolver } from '@/executor/resolver/resolver' +import { Routing } from '@/executor/routing/routing' +import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { InputResolver } from '../../resolver' -import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types' const logger = createLogger('ParallelBlockHandler') @@ -12,10 +15,13 @@ const logger = createLogger('ParallelBlockHandler') * create virtual instances for true parallel execution. */ export class ParallelBlockHandler implements BlockHandler { - constructor(private resolver?: InputResolver) {} + constructor( + private resolver?: InputResolver, + private pathTracker?: PathTracker + ) {} canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'parallel' + return block.metadata?.id === BlockType.PARALLEL } async execute( @@ -185,15 +191,31 @@ export class ParallelBlockHandler implements BlockHandler { } // Note: For simple count-based parallels without distribution, we don't store items - // Activate all child nodes (the executor will handle creating virtual instances) - const parallelStartConnections = - context.workflow?.connections.filter( - (conn) => conn.source === block.id && conn.sourceHandle === 'parallel-start-source' - ) || [] + // Use routing strategy to determine if this block requires active path checking + const blockType = block.metadata?.id + if (Routing.requiresActivePathCheck(blockType || '')) { + let isInActivePath = true + if (this.pathTracker) { + try { + isInActivePath = this.pathTracker.isInActivePath(block.id, context) + } catch (error) { + logger.warn(`PathTracker check failed for ${blockType} block ${block.id}:`, error) + // Default to true to maintain existing behavior if PathTracker fails + isInActivePath = true + } + } - for (const conn of parallelStartConnections) { - context.activeExecutionPath.add(conn.target) - logger.info(`Activated parallel path to ${conn.target}`) + // Only activate child nodes if this block is in the active execution path + if (isInActivePath) { + this.activateChildNodes(block, context) + } else { + logger.info( + `${blockType} block ${block.id} is not in active execution path, skipping child activation` + ) + } + } else { + // Regular blocks always activate their children + this.activateChildNodes(block, context) } return { @@ -291,6 +313,22 @@ export class ParallelBlockHandler implements BlockHandler { } as Record } + /** + * Activate child nodes for parallel execution + */ + private activateChildNodes(block: SerializedBlock, context: ExecutionContext): void { + // Activate all child nodes (the executor will handle creating virtual instances) + const parallelStartConnections = + context.workflow?.connections.filter( + (conn) => conn.source === block.id && conn.sourceHandle === 'parallel-start-source' + ) || [] + + for (const conn of parallelStartConnections) { + context.activeExecutionPath.add(conn.target) + logger.info(`Activated parallel path to ${conn.target}`) + } + } + /** * Checks if all iterations of a parallel block have completed */ diff --git a/apps/sim/executor/handlers/response/response-handler.ts b/apps/sim/executor/handlers/response/response-handler.ts index 9450afc0e..4b036c2e6 100644 --- a/apps/sim/executor/handlers/response/response-handler.ts +++ b/apps/sim/executor/handlers/response/response-handler.ts @@ -1,7 +1,8 @@ import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { BlockHandler } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { BlockHandler } from '../../types' const logger = createLogger('ResponseBlockHandler') @@ -15,7 +16,7 @@ interface JSONProperty { export class ResponseBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'response' + return block.metadata?.id === BlockType.RESPONSE } async execute(block: SerializedBlock, inputs: Record): Promise { diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 435e28c44..b6ef6ccb9 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -11,11 +11,12 @@ import { vi, } from 'vitest' import { generateRouterPrompt } from '@/blocks/blocks/router' +import { BlockType } from '@/executor/consts' +import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' +import { PathTracker } from '@/executor/path/path' +import type { ExecutionContext } from '@/executor/types' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { PathTracker } from '../../path' -import type { ExecutionContext } from '../../types' -import { RouterBlockHandler } from './router-handler' const mockGenerateRouterPrompt = generateRouterPrompt as Mock const mockGetProviderFromModel = getProviderFromModel as Mock @@ -52,9 +53,9 @@ describe('RouterBlockHandler', () => { } mockBlock = { id: 'router-block-1', - metadata: { id: 'router', name: 'Test Router' }, + metadata: { id: BlockType.ROUTER, name: 'Test Router' }, position: { x: 50, y: 50 }, - config: { tool: 'router', params: {} }, + config: { tool: BlockType.ROUTER, params: {} }, inputs: { prompt: 'string', model: 'string' }, // Using ParamType strings outputs: {}, enabled: true, diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 66fb3cd42..a92b971dd 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -2,10 +2,11 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console-logger' import { generateRouterPrompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' +import { BlockType } from '@/executor/consts' +import type { PathTracker } from '@/executor/path/path' +import type { BlockHandler, ExecutionContext } from '@/executor/types' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' -import type { PathTracker } from '../../path' -import type { BlockHandler, ExecutionContext } from '../../types' const logger = createLogger('RouterBlockHandler') @@ -19,7 +20,7 @@ export class RouterBlockHandler implements BlockHandler { constructor(private pathTracker: PathTracker) {} canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'router' + return block.metadata?.id === BlockType.ROUTER } async execute( @@ -144,7 +145,7 @@ export class RouterBlockHandler implements BlockHandler { // Extract system prompt for agent blocks let systemPrompt = '' - if (targetBlock.metadata?.id === 'agent') { + if (targetBlock.metadata?.id === BlockType.AGENT) { // Try to get system prompt from different possible locations systemPrompt = targetBlock.config?.params?.systemPrompt || targetBlock.inputs?.systemPrompt || '' diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index dd2a1ff98..ac28ea4c5 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' -import type { ExecutionContext } from '../../types' -import { WorkflowBlockHandler } from './workflow-handler' // Mock fetch globally global.fetch = vi.fn() @@ -18,9 +19,9 @@ describe('WorkflowBlockHandler', () => { mockBlock = { id: 'workflow-block-1', - metadata: { id: 'workflow', name: 'Test Workflow Block' }, + metadata: { id: BlockType.WORKFLOW, name: 'Test Workflow Block' }, position: { x: 0, y: 0 }, - config: { tool: 'workflow', params: {} }, + config: { tool: BlockType.WORKFLOW, params: {} }, inputs: { workflowId: 'string' }, outputs: {}, enabled: true, @@ -64,9 +65,9 @@ describe('WorkflowBlockHandler', () => { blocks: [ { id: 'starter', - metadata: { id: 'starter', name: 'Starter' }, + metadata: { id: BlockType.STARTER, name: 'Starter' }, position: { x: 0, y: 0 }, - config: { tool: 'starter', params: {} }, + config: { tool: BlockType.STARTER, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -87,7 +88,7 @@ describe('WorkflowBlockHandler', () => { }) it('should not handle non-workflow blocks', () => { - const nonWorkflowBlock = { ...mockBlock, metadata: { id: 'function' } } + const nonWorkflowBlock = { ...mockBlock, metadata: { id: BlockType.FUNCTION } } expect(handler.canHandle(nonWorkflowBlock)).toBe(false) }) }) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 6c4e59381..bf9d83407 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -2,11 +2,12 @@ import { generateInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console-logger' import { getBaseUrl } from '@/lib/urls/utils' import type { BlockOutput } from '@/blocks/types' +import { Executor } from '@/executor' +import { BlockType } from '@/executor/consts' +import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types' import { Serializer } from '@/serializer' import type { SerializedBlock } from '@/serializer/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { Executor } from '../../index' -import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types' const logger = createLogger('WorkflowBlockHandler') @@ -22,7 +23,7 @@ export class WorkflowBlockHandler implements BlockHandler { private static executionStack = new Set() canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === 'workflow' + return block.metadata?.id === BlockType.WORKFLOW } async execute( diff --git a/apps/sim/executor/index.test.ts b/apps/sim/executor/index.test.ts index afb1e4e3e..1dc0f0543 100644 --- a/apps/sim/executor/index.test.ts +++ b/apps/sim/executor/index.test.ts @@ -8,6 +8,7 @@ * resolving inputs and dependencies, and managing errors. */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { Executor } from '@/executor' import { createMinimalWorkflow, createMockContext, @@ -15,8 +16,8 @@ import { createWorkflowWithErrorPath, createWorkflowWithLoop, setupAllMocks, -} from './__test-utils__/executor-mocks' -import { Executor } from './index' +} from '@/executor/__test-utils__/executor-mocks' +import { BlockType } from '@/executor/consts' vi.mock('@/stores/execution/store', () => ({ useExecutionStore: { @@ -155,14 +156,14 @@ describe('Executor', () => { test('should throw error for workflow without starter block', () => { const workflow = createMinimalWorkflow() - workflow.blocks = workflow.blocks.filter((block) => block.metadata?.id !== 'starter') + workflow.blocks = workflow.blocks.filter((block) => block.metadata?.id !== BlockType.STARTER) expect(() => new Executor(workflow)).toThrow('Workflow must have an enabled starter block') }) test('should throw error for workflow with disabled starter block', () => { const workflow = createMinimalWorkflow() - workflow.blocks.find((block) => block.metadata?.id === 'starter')!.enabled = false + workflow.blocks.find((block) => block.metadata?.id === BlockType.STARTER)!.enabled = false expect(() => new Executor(workflow)).toThrow('Workflow must have an enabled starter block') }) @@ -459,7 +460,7 @@ describe('Executor', () => { inputs: {}, outputs: {}, enabled: true, - metadata: { id: 'condition', name: 'Condition Block' }, + metadata: { id: BlockType.CONDITION, name: 'Condition Block' }, }) // Mock context @@ -641,7 +642,7 @@ describe('Executor', () => { { id: 'start', position: { x: 0, y: 0 }, - metadata: { id: 'starter', name: 'Start' }, + metadata: { id: BlockType.STARTER, name: 'Start' }, config: { tool: 'test-tool', params: {} }, inputs: {}, outputs: {}, @@ -650,7 +651,7 @@ describe('Executor', () => { { id: 'router', position: { x: 100, y: 0 }, - metadata: { id: 'router', name: 'Router' }, + metadata: { id: BlockType.ROUTER, name: 'Router' }, config: { tool: 'test-tool', params: { prompt: 'test', model: 'gpt-4' } }, inputs: {}, outputs: {}, @@ -659,7 +660,7 @@ describe('Executor', () => { { id: 'api1', position: { x: 200, y: -50 }, - metadata: { id: 'api', name: 'API 1' }, + metadata: { id: BlockType.API, name: 'API 1' }, config: { tool: 'test-tool', params: { url: 'http://api1.com', method: 'GET' } }, inputs: {}, outputs: {}, @@ -668,7 +669,7 @@ describe('Executor', () => { { id: 'api2', position: { x: 200, y: 50 }, - metadata: { id: 'api', name: 'API 2' }, + metadata: { id: BlockType.API, name: 'API 2' }, config: { tool: 'test-tool', params: { url: 'http://api2.com', method: 'GET' } }, inputs: {}, outputs: {}, @@ -677,7 +678,7 @@ describe('Executor', () => { { id: 'agent', position: { x: 300, y: 0 }, - metadata: { id: 'agent', name: 'Agent' }, + metadata: { id: BlockType.AGENT, name: 'Agent' }, config: { tool: 'test-tool', params: { model: 'gpt-4', userPrompt: 'test' } }, inputs: {}, outputs: {}, @@ -771,7 +772,7 @@ describe('Executor', () => { workflow.blocks.push({ id: 'router1', position: { x: 200, y: 0 }, - metadata: { id: 'router', name: 'Router' }, + metadata: { id: BlockType.ROUTER, name: 'Router' }, config: { tool: 'test-tool', params: {} }, inputs: {}, outputs: {}, diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 66fee46f4..22508b769 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -1,10 +1,7 @@ import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' -import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { useExecutionStore } from '@/stores/execution/store' -import { useConsoleStore } from '@/stores/panel/console/store' -import { useGeneralStore } from '@/stores/settings/general/store' +import { BlockType } from '@/executor/consts' import { AgentBlockHandler, ApiBlockHandler, @@ -17,11 +14,11 @@ import { ResponseBlockHandler, RouterBlockHandler, WorkflowBlockHandler, -} from './handlers/index' -import { LoopManager } from './loops' -import { ParallelManager } from './parallels' -import { PathTracker } from './path' -import { InputResolver } from './resolver' +} from '@/executor/handlers' +import { LoopManager } from '@/executor/loops/loops' +import { ParallelManager } from '@/executor/parallels/parallels' +import { PathTracker } from '@/executor/path/path' +import { InputResolver } from '@/executor/resolver/resolver' import type { BlockHandler, BlockLog, @@ -29,8 +26,12 @@ import type { ExecutionResult, NormalizedBlockOutput, StreamingExecution, -} from './types' -import { streamingResponseFormatProcessor } from './utils' +} from '@/executor/types' +import { streamingResponseFormatProcessor } from '@/executor/utils' +import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +import { useExecutionStore } from '@/stores/execution/store' +import { useConsoleStore } from '@/stores/panel/console/store' +import { useGeneralStore } from '@/stores/settings/general/store' const logger = createLogger('Executor') @@ -152,8 +153,8 @@ export class Executor { new EvaluatorBlockHandler(), new FunctionBlockHandler(), new ApiBlockHandler(), - new LoopBlockHandler(this.resolver), - new ParallelBlockHandler(this.resolver), + new LoopBlockHandler(this.resolver, this.pathTracker), + new ParallelBlockHandler(this.resolver, this.pathTracker), new ResponseBlockHandler(), new WorkflowBlockHandler(), new GenericBlockHandler(), @@ -548,7 +549,7 @@ export class Executor { */ private validateWorkflow(): void { const starterBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === 'starter' + (block) => block.metadata?.id === BlockType.STARTER ) if (!starterBlock || !starterBlock.enabled) { throw new Error('Workflow must have an enabled starter block') @@ -652,7 +653,7 @@ export class Executor { } const starterBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === 'starter' + (block) => block.metadata?.id === BlockType.STARTER ) if (starterBlock) { // Initialize the starter block with the workflow input @@ -998,7 +999,7 @@ export class Executor { // Check if this is a loop block const isLoopBlock = incomingConnections.some((conn) => { const sourceBlock = this.actualWorkflow.blocks.find((b) => b.id === conn.source) - return sourceBlock?.metadata?.id === 'loop' + return sourceBlock?.metadata?.id === BlockType.LOOP }) if (isLoopBlock) { @@ -1081,7 +1082,7 @@ export class Executor { // For condition blocks, check if this is the selected path if (conn.sourceHandle?.startsWith('condition-')) { const sourceBlock = this.actualWorkflow.blocks.find((b) => b.id === conn.source) - if (sourceBlock?.metadata?.id === 'condition') { + if (sourceBlock?.metadata?.id === BlockType.CONDITION) { const conditionId = conn.sourceHandle.replace('condition-', '') const selectedCondition = context.decisions.condition.get(conn.source) @@ -1096,7 +1097,7 @@ export class Executor { } // For router blocks, check if this is the selected target - if (sourceBlock?.metadata?.id === 'router') { + if (sourceBlock?.metadata?.id === BlockType.ROUTER) { const selectedTarget = context.decisions.router.get(conn.source) // If source is executed and this is not the selected target, consider it met @@ -1219,7 +1220,7 @@ export class Executor { // Special case for starter block - it's already been initialized in createExecutionContext // This ensures we don't re-execute the starter block and just return its existing state - if (block.metadata?.id === 'starter') { + if (block.metadata?.id === BlockType.STARTER) { const starterState = context.blockStates.get(actualBlockId) if (starterState) { return starterState.output as NormalizedBlockOutput @@ -1243,7 +1244,9 @@ export class Executor { // Check if this block needs the starter block's output // This is especially relevant for API, function, and conditions that might reference - const starterBlock = this.actualWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = this.actualWorkflow.blocks.find( + (b) => b.metadata?.id === BlockType.STARTER + ) if (starterBlock) { const starterState = context.blockStates.get(starterBlock.id) if (!starterState) { @@ -1345,7 +1348,7 @@ export class Executor { // Skip console logging for infrastructure blocks like loops and parallels // For streaming blocks, we'll add the console entry after stream processing - if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') { + if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) { addConsole({ input: blockLog.input, output: blockLog.output, @@ -1415,7 +1418,7 @@ export class Executor { context.blockLogs.push(blockLog) // Skip console logging for infrastructure blocks like loops and parallels - if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') { + if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) { addConsole({ input: blockLog.input, output: blockLog.output, @@ -1483,7 +1486,7 @@ export class Executor { context.blockLogs.push(blockLog) // Skip console logging for infrastructure blocks like loops and parallels - if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') { + if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) { addConsole({ input: blockLog.input, output: {}, @@ -1575,10 +1578,10 @@ export class Executor { // Skip for starter blocks which don't have error handles const block = this.actualWorkflow.blocks.find((b) => b.id === blockId) if ( - block?.metadata?.id === 'starter' || - block?.metadata?.id === 'condition' || - block?.metadata?.id === 'loop' || - block?.metadata?.id === 'parallel' + block?.metadata?.id === BlockType.STARTER || + block?.metadata?.id === BlockType.CONDITION || + block?.metadata?.id === BlockType.LOOP || + block?.metadata?.id === BlockType.PARALLEL ) { return false } diff --git a/apps/sim/executor/loops.test.ts b/apps/sim/executor/loops/loops.test.ts similarity index 95% rename from apps/sim/executor/loops.test.ts rename to apps/sim/executor/loops/loops.test.ts index 3de1e4923..511b707b9 100644 --- a/apps/sim/executor/loops.test.ts +++ b/apps/sim/executor/loops/loops.test.ts @@ -1,8 +1,9 @@ import { beforeEach, describe, expect, test, vi } from 'vitest' +import { createMockContext } from '@/executor/__test-utils__/executor-mocks' +import { BlockType } from '@/executor/consts' +import { LoopManager } from '@/executor/loops/loops' +import type { ExecutionContext } from '@/executor/types' import type { SerializedLoop, SerializedWorkflow } from '@/serializer/types' -import { createMockContext } from './__test-utils__/executor-mocks' -import { LoopManager } from './loops' -import type { ExecutionContext } from './types' vi.mock('@/lib/logs/console-logger', () => ({ createLogger: () => ({ @@ -40,8 +41,8 @@ describe('LoopManager', () => { { id: 'starter', position: { x: 0, y: 0 }, - metadata: { id: 'starter', name: 'Start' }, - config: { tool: 'starter', params: {} }, + metadata: { id: BlockType.STARTER, name: 'Start' }, + config: { tool: BlockType.STARTER, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -49,8 +50,8 @@ describe('LoopManager', () => { { id: 'loop-1', position: { x: 100, y: 0 }, - metadata: { id: 'loop', name: 'Test Loop' }, - config: { tool: 'loop', params: {} }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -58,8 +59,8 @@ describe('LoopManager', () => { { id: 'block-1', position: { x: 200, y: 0 }, - metadata: { id: 'function', name: 'Block 1' }, - config: { tool: 'function', params: {} }, + metadata: { id: BlockType.FUNCTION, name: 'Block 1' }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -67,8 +68,8 @@ describe('LoopManager', () => { { id: 'block-2', position: { x: 300, y: 0 }, - metadata: { id: 'function', name: 'Block 2' }, - config: { tool: 'function', params: {} }, + metadata: { id: BlockType.FUNCTION, name: 'Block 2' }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -76,8 +77,8 @@ describe('LoopManager', () => { { id: 'after-loop', position: { x: 400, y: 0 }, - metadata: { id: 'function', name: 'After Loop' }, - config: { tool: 'function', params: {} }, + metadata: { id: BlockType.FUNCTION, name: 'After Loop' }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -409,7 +410,7 @@ describe('LoopManager', () => { test('should handle router blocks with selected paths', async () => { // Create a workflow with a router block inside the loop const workflow = createWorkflowWithLoop(createBasicLoop()) - workflow.blocks[2].metadata!.id = 'router' // Make block-1 a router + workflow.blocks[2].metadata!.id = BlockType.ROUTER // Make block-1 a router workflow.connections = [ { source: 'starter', target: 'loop-1' }, { source: 'loop-1', target: 'block-1', sourceHandle: 'loop-start-source' }, @@ -435,7 +436,7 @@ describe('LoopManager', () => { test('should handle condition blocks with selected paths', async () => { // Create a workflow with a condition block inside the loop const workflow = createWorkflowWithLoop(createBasicLoop()) - workflow.blocks[2].metadata!.id = 'condition' // Make block-1 a condition + workflow.blocks[2].metadata!.id = BlockType.CONDITION // Make block-1 a condition workflow.connections = [ { source: 'starter', target: 'loop-1' }, { source: 'loop-1', target: 'block-1', sourceHandle: 'loop-start-source' }, @@ -496,8 +497,8 @@ describe('LoopManager', () => { workflow.blocks.push({ id: 'error-handler', position: { x: 350, y: 100 }, - metadata: { id: 'function', name: 'Error Handler' }, - config: { tool: 'function', params: {} }, + metadata: { id: BlockType.FUNCTION, name: 'Error Handler' }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, diff --git a/apps/sim/executor/loops.ts b/apps/sim/executor/loops/loops.ts similarity index 98% rename from apps/sim/executor/loops.ts rename to apps/sim/executor/loops/loops.ts index b12b13981..6e4f1c863 100644 --- a/apps/sim/executor/loops.ts +++ b/apps/sim/executor/loops/loops.ts @@ -1,6 +1,7 @@ import { createLogger } from '@/lib/logs/console-logger' +import { BlockType } from '@/executor/consts' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedConnection, SerializedLoop } from '@/serializer/types' -import type { ExecutionContext } from './types' const logger = createLogger('LoopManager') @@ -366,13 +367,13 @@ export class LoopManager { const outgoing = blockOutgoingConnections.get(currentBlockId) || [] // Handle routing blocks specially - if (block.metadata?.id === 'router') { + if (block.metadata?.id === BlockType.ROUTER) { // For router blocks, only follow the selected path const selectedTarget = context.decisions.router.get(currentBlockId) if (selectedTarget && nodeIds.includes(selectedTarget)) { toVisit.push(selectedTarget) } - } else if (block.metadata?.id === 'condition') { + } else if (block.metadata?.id === BlockType.CONDITION) { // For condition blocks, only follow the selected condition path const selectedConditionId = context.decisions.condition.get(currentBlockId) if (selectedConditionId) { diff --git a/apps/sim/executor/parallels.test.ts b/apps/sim/executor/parallels/parallels.test.ts similarity index 96% rename from apps/sim/executor/parallels.test.ts rename to apps/sim/executor/parallels/parallels.test.ts index 51d4c84e5..c79585704 100644 --- a/apps/sim/executor/parallels.test.ts +++ b/apps/sim/executor/parallels/parallels.test.ts @@ -1,8 +1,9 @@ import { describe, expect, test, vi } from 'vitest' +import { createParallelExecutionState } from '@/executor/__test-utils__/executor-mocks' +import { BlockType } from '@/executor/consts' +import { ParallelManager } from '@/executor/parallels/parallels' +import type { ExecutionContext } from '@/executor/types' import type { SerializedWorkflow } from '@/serializer/types' -import { createParallelExecutionState } from './__test-utils__/executor-mocks' -import { ParallelManager } from './parallels' -import type { ExecutionContext } from './types' vi.mock('@/lib/logs/console-logger', () => ({ createLogger: () => ({ @@ -154,7 +155,7 @@ describe('ParallelManager', () => { const block = { id: 'func-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -185,7 +186,7 @@ describe('ParallelManager', () => { const block = { id: 'func-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, diff --git a/apps/sim/executor/parallels.ts b/apps/sim/executor/parallels/parallels.ts similarity index 98% rename from apps/sim/executor/parallels.ts rename to apps/sim/executor/parallels/parallels.ts index a9d3598ff..dcffda3e3 100644 --- a/apps/sim/executor/parallels.ts +++ b/apps/sim/executor/parallels/parallels.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console-logger' +import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types' import type { SerializedBlock, SerializedParallel, SerializedWorkflow } from '@/serializer/types' -import type { ExecutionContext, NormalizedBlockOutput } from './types' const logger = createLogger('ParallelManager') diff --git a/apps/sim/executor/path.test.ts b/apps/sim/executor/path/path.test.ts similarity index 72% rename from apps/sim/executor/path.test.ts rename to apps/sim/executor/path/path.test.ts index 1c33fafb8..57f62ba81 100644 --- a/apps/sim/executor/path.test.ts +++ b/apps/sim/executor/path/path.test.ts @@ -1,7 +1,9 @@ import { beforeEach, describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/consts' +import { PathTracker } from '@/executor/path/path' +import { Routing } from '@/executor/routing/routing' +import type { BlockState, ExecutionContext } from '@/executor/types' import type { SerializedWorkflow } from '@/serializer/types' -import { PathTracker } from './path' -import type { BlockState, ExecutionContext } from './types' describe('PathTracker', () => { let pathTracker: PathTracker @@ -32,27 +34,27 @@ describe('PathTracker', () => { }, { id: 'router1', - metadata: { id: 'router' }, + metadata: { id: BlockType.ROUTER }, position: { x: 0, y: 0 }, - config: { tool: 'router', params: {} }, + config: { tool: BlockType.ROUTER, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'condition1', - metadata: { id: 'condition' }, + metadata: { id: BlockType.CONDITION }, position: { x: 0, y: 0 }, - config: { tool: 'condition', params: {} }, + config: { tool: BlockType.CONDITION, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'loop1', - metadata: { id: 'loop' }, + metadata: { id: BlockType.LOOP }, position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -75,6 +77,7 @@ describe('PathTracker', () => { loopType: 'for', }, }, + parallels: {}, } mockContext = { @@ -417,36 +420,36 @@ describe('PathTracker', () => { blocks: [ { id: 'router1', - metadata: { id: 'router', name: 'Router' }, + metadata: { id: BlockType.ROUTER, name: 'Router' }, position: { x: 0, y: 0 }, - config: { tool: 'router', params: {} }, + config: { tool: BlockType.ROUTER, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'api1', - metadata: { id: 'api', name: 'API 1' }, + metadata: { id: BlockType.API, name: 'API 1' }, position: { x: 0, y: 0 }, - config: { tool: 'api', params: {} }, + config: { tool: BlockType.API, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'api2', - metadata: { id: 'api', name: 'API 2' }, + metadata: { id: BlockType.API, name: 'API 2' }, position: { x: 0, y: 0 }, - config: { tool: 'api', params: {} }, + config: { tool: BlockType.API, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'agent1', - metadata: { id: 'agent', name: 'Agent' }, + metadata: { id: BlockType.AGENT, name: 'Agent' }, position: { x: 0, y: 0 }, - config: { tool: 'agent', params: {} }, + config: { tool: BlockType.AGENT, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -485,7 +488,7 @@ describe('PathTracker', () => { output: { selectedPath: { blockId: 'api1', - blockType: 'api', + blockType: BlockType.API, blockTitle: 'API 1', }, }, @@ -508,9 +511,9 @@ describe('PathTracker', () => { // Add another level to test deep activation mockWorkflow.blocks.push({ id: 'finalStep', - metadata: { id: 'api', name: 'Final Step' }, + metadata: { id: BlockType.API, name: 'Final Step' }, position: { x: 0, y: 0 }, - config: { tool: 'api', params: {} }, + config: { tool: BlockType.API, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -524,7 +527,7 @@ describe('PathTracker', () => { output: { selectedPath: { blockId: 'api1', - blockType: 'api', + blockType: BlockType.API, blockTitle: 'API 1', }, }, @@ -552,7 +555,7 @@ describe('PathTracker', () => { output: { selectedPath: { blockId: 'api1', - blockType: 'api', + blockType: BlockType.API, blockTitle: 'API 1', }, }, @@ -586,7 +589,7 @@ describe('PathTracker', () => { output: { selectedPath: { blockId: 'api1', - blockType: 'api', + blockType: BlockType.API, blockTitle: 'API 1', }, }, @@ -602,4 +605,158 @@ describe('PathTracker', () => { expect(mockContext.activeExecutionPath.has('agent1')).toBe(false) }) }) + + describe('RoutingStrategy integration', () => { + beforeEach(() => { + // Add more block types to test the new routing strategy + mockWorkflow.blocks.push( + { + id: 'parallel1', + metadata: { id: BlockType.PARALLEL }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function1', + metadata: { id: BlockType.FUNCTION }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'agent1', + metadata: { id: BlockType.AGENT }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + ) + + mockWorkflow.connections.push( + { source: 'parallel1', target: 'function1', sourceHandle: 'parallel-start-source' }, + { source: 'parallel1', target: 'agent1', sourceHandle: 'parallel-end-source' } + ) + + mockWorkflow.parallels = { + parallel1: { + id: 'parallel1', + nodes: ['function1'], + distribution: ['item1', 'item2'], + }, + } + + pathTracker = new PathTracker(mockWorkflow) + }) + + it('should correctly categorize different block types', () => { + // Test that our refactored code properly uses RoutingStrategy + expect(Routing.getCategory(BlockType.ROUTER)).toBe('routing') + expect(Routing.getCategory(BlockType.CONDITION)).toBe('routing') + expect(Routing.getCategory(BlockType.PARALLEL)).toBe('flow-control') + expect(Routing.getCategory(BlockType.LOOP)).toBe('flow-control') + expect(Routing.getCategory(BlockType.FUNCTION)).toBe('regular') + expect(Routing.getCategory(BlockType.AGENT)).toBe('regular') + }) + + it('should handle flow control blocks correctly in path checking', () => { + // Test that parallel blocks are handled correctly + mockContext.executedBlocks.add('parallel1') + mockContext.activeExecutionPath.add('parallel1') + + // Function1 should be reachable from parallel1 via parallel-start-source + expect(pathTracker.isInActivePath('function1', mockContext)).toBe(true) + + // Agent1 should be reachable from parallel1 via parallel-end-source + expect(pathTracker.isInActivePath('agent1', mockContext)).toBe(true) + }) + + it('should handle router selecting routing blocks correctly', () => { + // Test the refactored logic where router selects another routing block + const blockState: BlockState = { + output: { selectedPath: { blockId: 'condition1' } }, + executed: true, + executionTime: 100, + } + mockContext.blockStates.set('router1', blockState) + + pathTracker.updateExecutionPaths(['router1'], mockContext) + + // Condition1 should be activated but not its downstream paths + // (since routing blocks make their own decisions) + expect(mockContext.activeExecutionPath.has('condition1')).toBe(true) + expect(mockContext.decisions.router.get('router1')).toBe('condition1') + }) + + it('should handle router selecting flow control blocks correctly', () => { + // Test the refactored logic where router selects a flow control block + const blockState: BlockState = { + output: { selectedPath: { blockId: 'parallel1' } }, + executed: true, + executionTime: 100, + } + mockContext.blockStates.set('router1', blockState) + + pathTracker.updateExecutionPaths(['router1'], mockContext) + + // Parallel1 should be activated but not its downstream paths + // (since flow control blocks don't activate downstream automatically) + expect(mockContext.activeExecutionPath.has('parallel1')).toBe(true) + expect(mockContext.decisions.router.get('router1')).toBe('parallel1') + // Children should NOT be activated automatically + expect(mockContext.activeExecutionPath.has('function1')).toBe(false) + expect(mockContext.activeExecutionPath.has('agent1')).toBe(false) + }) + + it('should handle router selecting regular blocks correctly', () => { + // Test that regular blocks still activate downstream paths + const blockState: BlockState = { + output: { selectedPath: { blockId: 'function1' } }, + executed: true, + executionTime: 100, + } + mockContext.blockStates.set('router1', blockState) + + pathTracker.updateExecutionPaths(['router1'], mockContext) + + // Function1 should be activated and can activate downstream paths + expect(mockContext.activeExecutionPath.has('function1')).toBe(true) + expect(mockContext.decisions.router.get('router1')).toBe('function1') + }) + + it('should use category-based logic for updatePathForBlock', () => { + // Test that the refactored switch statement works correctly + + // Test routing block (condition) + const conditionState: BlockState = { + output: { selectedConditionId: 'if' }, + executed: true, + executionTime: 100, + } + mockContext.blockStates.set('condition1', conditionState) + pathTracker.updateExecutionPaths(['condition1'], mockContext) + expect(mockContext.decisions.condition.get('condition1')).toBe('if') + + // Test flow control block (loop) + pathTracker.updateExecutionPaths(['loop1'], mockContext) + expect(mockContext.activeExecutionPath.has('block1')).toBe(true) // loop-start-source + + // Test regular block + const functionState: BlockState = { + output: { result: 'success' }, + executed: true, + executionTime: 100, + } + mockContext.blockStates.set('function1', functionState) + mockContext.executedBlocks.add('function1') + pathTracker.updateExecutionPaths(['function1'], mockContext) + // Should activate downstream connections (handled by regular block logic) + }) + }) }) diff --git a/apps/sim/executor/path.ts b/apps/sim/executor/path/path.ts similarity index 74% rename from apps/sim/executor/path.ts rename to apps/sim/executor/path/path.ts index 4fa1f6c48..915e026c9 100644 --- a/apps/sim/executor/path.ts +++ b/apps/sim/executor/path/path.ts @@ -1,6 +1,8 @@ import { createLogger } from '@/lib/logs/console-logger' +import { BlockType } from '@/executor/consts' +import { Routing } from '@/executor/routing/routing' +import type { BlockState, ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedConnection, SerializedWorkflow } from '@/serializer/types' -import type { BlockState, ExecutionContext } from './types' const logger = createLogger('PathTracker') @@ -79,14 +81,15 @@ export class PathTracker { const sourceBlock = this.getBlock(connection.source) if (!sourceBlock) return false - const blockType = sourceBlock.metadata?.id + const blockType = sourceBlock.metadata?.id || '' + const category = Routing.getCategory(blockType) - // Use strategy pattern for different block types - switch (blockType) { - case 'router': - return this.isRouterConnectionActive(connection, context) - case 'condition': - return this.isConditionConnectionActive(connection, context) + // Use routing strategy to determine connection checking method + switch (category) { + case 'routing': + return blockType === BlockType.ROUTER + ? this.isRouterConnectionActive(connection, context) + : this.isConditionConnectionActive(connection, context) default: return this.isRegularConnectionActive(connection, context) } @@ -137,17 +140,24 @@ export class PathTracker { * Update paths for a specific block based on its type */ private updatePathForBlock(block: SerializedBlock, context: ExecutionContext): void { - const blockType = block.metadata?.id + const blockType = block.metadata?.id || '' + const category = Routing.getCategory(blockType) - switch (blockType) { - case 'router': - this.updateRouterPaths(block, context) + switch (category) { + case 'routing': + if (blockType === BlockType.ROUTER) { + this.updateRouterPaths(block, context) + } else { + this.updateConditionPaths(block, context) + } break - case 'condition': - this.updateConditionPaths(block, context) - break - case 'loop': - this.updateLoopPaths(block, context) + case 'flow-control': + if (blockType === BlockType.LOOP) { + this.updateLoopPaths(block, context) + } else { + // For parallel blocks, they're handled by their own handler + this.updateRegularBlockPaths(block, context) + } break default: this.updateRegularBlockPaths(block, context) @@ -166,23 +176,43 @@ export class PathTracker { context.decisions.router.set(block.id, selectedPath) context.activeExecutionPath.add(selectedPath) - this.activateDownstreamPaths(selectedPath, context) + // Check if the selected target should activate downstream paths + const selectedBlock = this.getBlock(selectedPath) + const selectedBlockType = selectedBlock?.metadata?.id || '' + const selectedCategory = Routing.getCategory(selectedBlockType) + + // Only activate downstream paths for regular blocks + // Routing blocks make their own routing decisions when they execute + // Flow control blocks manage their own path activation + if (selectedCategory === 'regular') { + this.activateDownstreamPathsSelectively(selectedPath, context) + } logger.info(`Router ${block.id} selected path: ${selectedPath}`) } } /** - * Recursively activate downstream paths from a block + * Selectively activate downstream paths, respecting block routing behavior + * This prevents flow control blocks from being activated when they should be controlled by routing */ - private activateDownstreamPaths(blockId: string, context: ExecutionContext): void { + private activateDownstreamPathsSelectively(blockId: string, context: ExecutionContext): void { const outgoingConnections = this.getOutgoingConnections(blockId) for (const conn of outgoingConnections) { if (!context.activeExecutionPath.has(conn.target)) { - context.activeExecutionPath.add(conn.target) + const targetBlock = this.getBlock(conn.target) + const targetBlockType = targetBlock?.metadata?.id - this.activateDownstreamPaths(conn.target, context) + // Use routing strategy to determine if this connection should be activated + if (!Routing.shouldSkipConnection(conn.sourceHandle, targetBlockType || '')) { + context.activeExecutionPath.add(conn.target) + + // Recursively activate downstream paths if the target block should activate downstream + if (Routing.shouldActivateDownstream(targetBlockType || '')) { + this.activateDownstreamPathsSelectively(conn.target, context) + } + } } } } @@ -238,6 +268,14 @@ export class PathTracker { for (const conn of outgoingConnections) { if (this.shouldActivateConnection(conn, hasError, isPartOfLoop, blockLoops, context)) { + const targetBlock = this.getBlock(conn.target) + const targetBlockType = targetBlock?.metadata?.id + + // Use routing strategy to determine if this connection should be activated + if (Routing.shouldSkipConnection(conn.sourceHandle, targetBlockType || '')) { + continue + } + context.activeExecutionPath.add(conn.target) } } diff --git a/apps/sim/executor/resolver.test.ts b/apps/sim/executor/resolver/resolver.test.ts similarity index 90% rename from apps/sim/executor/resolver.test.ts rename to apps/sim/executor/resolver/resolver.test.ts index 520dbc13d..8e0fd2756 100644 --- a/apps/sim/executor/resolver.test.ts +++ b/apps/sim/executor/resolver/resolver.test.ts @@ -1,7 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { InputResolver } from '@/executor/resolver/resolver' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { InputResolver } from './resolver' -import type { ExecutionContext } from './types' // Mock logger vi.mock('@/lib/logs/console-logger', () => ({ @@ -27,36 +28,36 @@ describe('InputResolver', () => { blocks: [ { id: 'starter-block', - metadata: { id: 'starter', name: 'Start' }, + metadata: { id: BlockType.STARTER, name: 'Start' }, position: { x: 100, y: 100 }, - config: { tool: 'starter', params: {} }, + config: { tool: BlockType.STARTER, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'function-block', - metadata: { id: 'function', name: 'Function' }, + metadata: { id: BlockType.FUNCTION, name: 'Function' }, position: { x: 300, y: 100 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'condition-block', - metadata: { id: 'condition', name: 'Condition' }, + metadata: { id: BlockType.CONDITION, name: 'Condition' }, position: { x: 500, y: 100 }, - config: { tool: 'condition', params: {} }, + config: { tool: BlockType.CONDITION, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'api-block', - metadata: { id: 'api', name: 'API' }, + metadata: { id: BlockType.API, name: 'API' }, position: { x: 700, y: 100 }, - config: { tool: 'api', params: {} }, + config: { tool: BlockType.API, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -429,7 +430,7 @@ describe('InputResolver', () => { it('should resolve environment variables in API key contexts', () => { const block: SerializedBlock = { id: 'test-block', - metadata: { id: 'api', name: 'Test API Block' }, // API block type + metadata: { id: BlockType.API, name: 'Test API Block' }, // API block type position: { x: 0, y: 0 }, config: { tool: 'api', @@ -626,10 +627,10 @@ describe('InputResolver', () => { it('should handle code input for function blocks', () => { const block: SerializedBlock = { id: 'code-block', - metadata: { id: 'function', name: 'Code Block' }, + metadata: { id: BlockType.FUNCTION, name: 'Code Block' }, position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'const name = "";\nconst num = ;\nreturn { name, num };', }, @@ -652,7 +653,7 @@ describe('InputResolver', () => { it('should handle body input for API blocks', () => { const block: SerializedBlock = { id: 'api-block', - metadata: { id: 'api', name: 'API Block' }, + metadata: { id: BlockType.API, name: 'API Block' }, position: { x: 0, y: 0 }, config: { tool: 'api', @@ -679,7 +680,7 @@ describe('InputResolver', () => { it('should handle conditions parameter for condition blocks', () => { const block: SerializedBlock = { id: 'condition-block', - metadata: { id: 'condition', name: 'Condition Block' }, + metadata: { id: BlockType.CONDITION, name: 'Condition Block' }, position: { x: 0, y: 0 }, config: { tool: 'condition', @@ -734,10 +735,10 @@ describe('InputResolver', () => { const loopBlock: SerializedBlock = { id: 'loop-1', position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'loop', name: 'Test Loop' }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, enabled: true, } @@ -745,14 +746,14 @@ describe('InputResolver', () => { id: 'function-1', position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { item: '', // Direct reference, not wrapped in quotes }, }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Process Item' }, + metadata: { id: BlockType.FUNCTION, name: 'Process Item' }, enabled: true, } @@ -796,10 +797,10 @@ describe('InputResolver', () => { const loopBlock: SerializedBlock = { id: 'loop-1', position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'loop', name: 'Test Loop' }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, enabled: true, } @@ -807,14 +808,14 @@ describe('InputResolver', () => { id: 'function-1', position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { index: '', // Direct reference, not wrapped in quotes }, }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Process Index' }, + metadata: { id: BlockType.FUNCTION, name: 'Process Index' }, enabled: true, } @@ -857,10 +858,10 @@ describe('InputResolver', () => { const loopBlock: SerializedBlock = { id: 'loop-1', position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'loop', name: 'Test Loop' }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, enabled: true, } @@ -868,14 +869,14 @@ describe('InputResolver', () => { id: 'function-1', position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { allItems: '', // Direct reference to all items }, }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Process All Items' }, + metadata: { id: BlockType.FUNCTION, name: 'Process All Items' }, enabled: true, } @@ -924,10 +925,10 @@ describe('InputResolver', () => { const loopBlock: SerializedBlock = { id: 'loop-1', position: { x: 0, y: 0 }, - config: { tool: 'loop', params: {} }, + config: { tool: BlockType.LOOP, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'loop', name: 'Test Loop' }, + metadata: { id: BlockType.LOOP, name: 'Test Loop' }, enabled: true, } @@ -935,14 +936,14 @@ describe('InputResolver', () => { id: 'function-1', position: { x: 0, y: 0 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { allItems: '', // Direct reference to all items }, }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Process All Items' }, + metadata: { id: BlockType.FUNCTION, name: 'Process All Items' }, enabled: true, } @@ -997,19 +998,19 @@ describe('InputResolver', () => { { id: 'parallel-1', position: { x: 0, y: 0 }, - config: { tool: 'parallel', params: {} }, + config: { tool: BlockType.PARALLEL, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'parallel', name: 'Parallel 1' }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' }, enabled: true, }, { id: 'function-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: { code: '' } }, + config: { tool: BlockType.FUNCTION, params: { code: '' } }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Function 1' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, enabled: true, }, ], @@ -1053,28 +1054,28 @@ describe('InputResolver', () => { { id: 'parallel-1', position: { x: 0, y: 0 }, - config: { tool: 'parallel', params: {} }, + config: { tool: BlockType.PARALLEL, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'parallel', name: 'Parallel 1' }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' }, enabled: true, }, { id: 'parallel-2', position: { x: 0, y: 0 }, - config: { tool: 'parallel', params: {} }, + config: { tool: BlockType.PARALLEL, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'parallel', name: 'Parallel 2' }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' }, enabled: true, }, { id: 'function-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: { code: '' } }, + config: { tool: BlockType.FUNCTION, params: { code: '' } }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Function 1' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, enabled: true, }, ], @@ -1156,19 +1157,19 @@ describe('InputResolver', () => { { id: 'parallel-1', position: { x: 0, y: 0 }, - config: { tool: 'parallel', params: {} }, + config: { tool: BlockType.PARALLEL, params: {} }, inputs: {}, outputs: {}, - metadata: { id: 'parallel', name: 'Parallel 1' }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' }, enabled: true, }, { id: 'function-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: { code: '' } }, + config: { tool: BlockType.FUNCTION, params: { code: '' } }, inputs: {}, outputs: {}, - metadata: { id: 'function', name: 'Function 1' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, enabled: true, }, ], @@ -1244,36 +1245,36 @@ describe('InputResolver', () => { blocks: [ { id: 'starter-1', - metadata: { id: 'starter', name: 'Start' }, + metadata: { id: BlockType.STARTER, name: 'Start' }, position: { x: 0, y: 0 }, - config: { tool: 'starter', params: {} }, + config: { tool: BlockType.STARTER, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'agent-1', - metadata: { id: 'agent', name: 'Agent Block' }, + metadata: { id: BlockType.AGENT, name: 'Agent Block' }, position: { x: 100, y: 100 }, - config: { tool: 'agent', params: {} }, + config: { tool: BlockType.AGENT, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'function-1', - metadata: { id: 'function', name: 'Function Block' }, + metadata: { id: BlockType.FUNCTION, name: 'Function Block' }, position: { x: 200, y: 200 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'isolated-block', - metadata: { id: 'agent', name: 'Isolated Block' }, + metadata: { id: BlockType.AGENT, name: 'Isolated Block' }, position: { x: 300, y: 300 }, - config: { tool: 'agent', params: {} }, + config: { tool: BlockType.AGENT, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1301,7 +1302,7 @@ describe('InputResolver', () => { }) // Always allow starter block access const starterBlock = workflowWithConnections.blocks.find( - (b) => b.metadata?.id === 'starter' + (b) => b.metadata?.id === BlockType.STARTER ) if (starterBlock) { accessibleBlocks.add(starterBlock.id) @@ -1323,7 +1324,7 @@ describe('InputResolver', () => { }) // Always allow starter block access const starterBlock = workflowWithConnections.blocks.find( - (b) => b.metadata?.id === 'starter' + (b) => b.metadata?.id === BlockType.STARTER ) if (starterBlock) { accessibleBlocks.add(starterBlock.id) @@ -1370,7 +1371,7 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { ...functionBlock, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ', // function-1 can reference agent-1 (connected) }, @@ -1385,9 +1386,9 @@ describe('InputResolver', () => { // Create a new block that is added to the workflow but not connected to isolated-block workflowWithConnections.blocks.push({ id: 'test-block', - metadata: { id: 'function', name: 'Test Block' }, + metadata: { id: BlockType.FUNCTION, name: 'Test Block' }, position: { x: 500, y: 500 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1404,7 +1405,9 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = workflowWithConnections.blocks.find( + (b) => b.metadata?.id === BlockType.STARTER + ) if (starterBlock) { testBlockAccessible.add(starterBlock.id) } @@ -1412,10 +1415,10 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { id: 'test-block', - metadata: { id: 'function', name: 'Test Block' }, + metadata: { id: BlockType.FUNCTION, name: 'Test Block' }, position: { x: 500, y: 500 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ', // test-block cannot reference isolated-block (not connected) }, @@ -1435,7 +1438,7 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { ...functionBlock, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ', // Any block can reference start }, @@ -1450,9 +1453,9 @@ describe('InputResolver', () => { // Create a test block in the workflow first workflowWithConnections.blocks.push({ id: 'test-block-2', - metadata: { id: 'function', name: 'Test Block 2' }, + metadata: { id: BlockType.FUNCTION, name: 'Test Block 2' }, position: { x: 600, y: 600 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1469,7 +1472,9 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = workflowWithConnections.blocks.find( + (b) => b.metadata?.id === BlockType.STARTER + ) if (starterBlock) { testBlock2Accessible.add(starterBlock.id) } @@ -1477,10 +1482,10 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { id: 'test-block-2', - metadata: { id: 'function', name: 'Test Block 2' }, + metadata: { id: BlockType.FUNCTION, name: 'Test Block 2' }, position: { x: 600, y: 600 }, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ', }, @@ -1500,7 +1505,7 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { ...functionBlock, config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { nameRef: '', // Reference by actual name normalizedRef: '', // Reference by normalized name @@ -1523,9 +1528,9 @@ describe('InputResolver', () => { ...workflowWithConnections.blocks, { id: 'response-1', - metadata: { id: 'response', name: 'Response Block' }, + metadata: { id: BlockType.RESPONSE, name: 'Response Block' }, position: { x: 400, y: 400 }, - config: { tool: 'response', params: {} }, + config: { tool: BlockType.RESPONSE, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1555,7 +1560,9 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = extendedWorkflow.blocks.find( + (b) => b.metadata?.id === BlockType.STARTER + ) if (starterBlock) { accessibleBlocks.add(starterBlock.id) } @@ -1572,7 +1579,9 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = extendedWorkflow.blocks.find( + (b) => b.metadata?.id === BlockType.STARTER + ) if (starterBlock) { accessibleBlocks.add(starterBlock.id) } @@ -1590,7 +1599,7 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { ...responseBlock, config: { - tool: 'response', + tool: BlockType.RESPONSE, params: { canReferenceFunction: '', // Can reference directly connected function-1 cannotReferenceAgent: '', // Cannot reference agent-1 (not directly connected) @@ -1614,7 +1623,7 @@ describe('InputResolver', () => { expect(() => { const block1 = { ...testBlock, - config: { tool: 'response', params: { test: '' } }, + config: { tool: BlockType.RESPONSE, params: { test: '' } }, } extendedResolver.resolveInputs(block1, extendedContext) }).not.toThrow() @@ -1624,9 +1633,9 @@ describe('InputResolver', () => { // Add the response block to the workflow so it can be validated properly extendedWorkflow.blocks.push({ id: 'test-response-block', - metadata: { id: 'response', name: 'Test Response Block' }, + metadata: { id: BlockType.RESPONSE, name: 'Test Response Block' }, position: { x: 500, y: 500 }, - config: { tool: 'response', params: {} }, + config: { tool: BlockType.RESPONSE, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1635,9 +1644,9 @@ describe('InputResolver', () => { const block2 = { id: 'test-response-block', - metadata: { id: 'response', name: 'Test Response Block' }, + metadata: { id: BlockType.RESPONSE, name: 'Test Response Block' }, position: { x: 500, y: 500 }, - config: { tool: 'response', params: { test: '' } }, + config: { tool: BlockType.RESPONSE, params: { test: '' } }, inputs: {}, outputs: {}, enabled: true, @@ -1652,16 +1661,16 @@ describe('InputResolver', () => { blocks: [ { id: 'starter-1', - metadata: { id: 'starter', name: 'Start' }, + metadata: { id: BlockType.STARTER, name: 'Start' }, position: { x: 0, y: 0 }, - config: { tool: 'starter', params: {} }, + config: { tool: BlockType.STARTER, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'loop-1', - metadata: { id: 'loop', name: 'Loop' }, + metadata: { id: BlockType.LOOP, name: 'Loop' }, position: { x: 100, y: 100 }, config: { tool: '', params: {} }, inputs: {}, @@ -1670,18 +1679,18 @@ describe('InputResolver', () => { }, { id: 'function-1', - metadata: { id: 'function', name: 'Function 1' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, position: { x: 200, y: 200 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, }, { id: 'function-2', - metadata: { id: 'function', name: 'Function 2' }, + metadata: { id: BlockType.FUNCTION, name: 'Function 2' }, position: { x: 300, y: 300 }, - config: { tool: 'function', params: {} }, + config: { tool: BlockType.FUNCTION, params: {} }, inputs: {}, outputs: {}, enabled: true, @@ -1711,7 +1720,7 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === BlockType.STARTER) if (starterBlock) { accessibleBlocks.add(starterBlock.id) } @@ -1735,7 +1744,7 @@ describe('InputResolver', () => { } }) // Always allow starter block access - const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === BlockType.STARTER) if (starterBlock) { accessibleBlocks.add(starterBlock.id) } @@ -1746,7 +1755,7 @@ describe('InputResolver', () => { const testBlock: SerializedBlock = { ...loopWorkflow.blocks[2], config: { - tool: 'function', + tool: BlockType.FUNCTION, params: { code: 'return ', // function-1 can reference function-2 (same loop) }, diff --git a/apps/sim/executor/resolver.ts b/apps/sim/executor/resolver/resolver.ts similarity index 99% rename from apps/sim/executor/resolver.ts rename to apps/sim/executor/resolver/resolver.ts index fd268ea99..e41afe3b4 100644 --- a/apps/sim/executor/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -1,9 +1,9 @@ import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import { VariableManager } from '@/lib/variables/variable-manager' +import type { LoopManager } from '@/executor/loops/loops' +import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import type { LoopManager } from './loops' -import type { ExecutionContext } from './types' const logger = createLogger('InputResolver') diff --git a/apps/sim/executor/routing/routing.test.ts b/apps/sim/executor/routing/routing.test.ts new file mode 100644 index 000000000..5ff43b75f --- /dev/null +++ b/apps/sim/executor/routing/routing.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/consts' +import { BlockCategory, Routing } from '@/executor/routing/routing' + +describe('Routing', () => { + describe('getCategory', () => { + it.concurrent('should categorize flow control blocks correctly', () => { + expect(Routing.getCategory(BlockType.PARALLEL)).toBe(BlockCategory.FLOW_CONTROL) + expect(Routing.getCategory(BlockType.LOOP)).toBe(BlockCategory.FLOW_CONTROL) + }) + + it.concurrent('should categorize routing blocks correctly', () => { + expect(Routing.getCategory(BlockType.ROUTER)).toBe(BlockCategory.ROUTING_BLOCK) + expect(Routing.getCategory(BlockType.CONDITION)).toBe(BlockCategory.ROUTING_BLOCK) + }) + + it.concurrent('should categorize regular blocks correctly', () => { + expect(Routing.getCategory(BlockType.FUNCTION)).toBe(BlockCategory.REGULAR_BLOCK) + expect(Routing.getCategory(BlockType.AGENT)).toBe(BlockCategory.REGULAR_BLOCK) + expect(Routing.getCategory(BlockType.API)).toBe(BlockCategory.REGULAR_BLOCK) + expect(Routing.getCategory(BlockType.STARTER)).toBe(BlockCategory.REGULAR_BLOCK) + }) + + it.concurrent('should default to regular block for unknown types', () => { + expect(Routing.getCategory('unknown')).toBe(BlockCategory.REGULAR_BLOCK) + expect(Routing.getCategory('')).toBe(BlockCategory.REGULAR_BLOCK) + }) + }) + + describe('shouldActivateDownstream', () => { + it.concurrent('should return true for routing blocks', () => { + expect(Routing.shouldActivateDownstream(BlockType.ROUTER)).toBe(true) + expect(Routing.shouldActivateDownstream(BlockType.CONDITION)).toBe(true) + }) + + it.concurrent('should return false for flow control blocks', () => { + expect(Routing.shouldActivateDownstream(BlockType.PARALLEL)).toBe(false) + expect(Routing.shouldActivateDownstream(BlockType.LOOP)).toBe(false) + }) + + it.concurrent('should return true for regular blocks', () => { + expect(Routing.shouldActivateDownstream(BlockType.FUNCTION)).toBe(true) + expect(Routing.shouldActivateDownstream(BlockType.AGENT)).toBe(true) + }) + + it.concurrent('should handle empty/undefined block types', () => { + expect(Routing.shouldActivateDownstream('')).toBe(true) + expect(Routing.shouldActivateDownstream(undefined as any)).toBe(true) + }) + }) + + describe('requiresActivePathCheck', () => { + it.concurrent('should return true for flow control blocks', () => { + expect(Routing.requiresActivePathCheck(BlockType.PARALLEL)).toBe(true) + expect(Routing.requiresActivePathCheck(BlockType.LOOP)).toBe(true) + }) + + it.concurrent('should return false for routing blocks', () => { + expect(Routing.requiresActivePathCheck(BlockType.ROUTER)).toBe(false) + expect(Routing.requiresActivePathCheck(BlockType.CONDITION)).toBe(false) + }) + + it.concurrent('should return false for regular blocks', () => { + expect(Routing.requiresActivePathCheck(BlockType.FUNCTION)).toBe(false) + expect(Routing.requiresActivePathCheck(BlockType.AGENT)).toBe(false) + }) + + it.concurrent('should handle empty/undefined block types', () => { + expect(Routing.requiresActivePathCheck('')).toBe(false) + expect(Routing.requiresActivePathCheck(undefined as any)).toBe(false) + }) + }) + + describe('shouldSkipInSelectiveActivation', () => { + it.concurrent('should return true for flow control blocks', () => { + expect(Routing.shouldSkipInSelectiveActivation(BlockType.PARALLEL)).toBe(true) + expect(Routing.shouldSkipInSelectiveActivation(BlockType.LOOP)).toBe(true) + }) + + it.concurrent('should return false for routing blocks', () => { + expect(Routing.shouldSkipInSelectiveActivation(BlockType.ROUTER)).toBe(false) + expect(Routing.shouldSkipInSelectiveActivation(BlockType.CONDITION)).toBe(false) + }) + + it.concurrent('should return false for regular blocks', () => { + expect(Routing.shouldSkipInSelectiveActivation(BlockType.FUNCTION)).toBe(false) + expect(Routing.shouldSkipInSelectiveActivation(BlockType.AGENT)).toBe(false) + }) + }) + + describe('shouldSkipConnection', () => { + it.concurrent('should skip flow control blocks', () => { + expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(true) + expect(Routing.shouldSkipConnection('source', BlockType.LOOP)).toBe(true) + }) + + it.concurrent('should skip flow control specific connections', () => { + expect(Routing.shouldSkipConnection('parallel-start-source', BlockType.FUNCTION)).toBe(true) + expect(Routing.shouldSkipConnection('parallel-end-source', BlockType.AGENT)).toBe(true) + expect(Routing.shouldSkipConnection('loop-start-source', BlockType.API)).toBe(true) + expect(Routing.shouldSkipConnection('loop-end-source', BlockType.EVALUATOR)).toBe(true) + }) + + it.concurrent('should not skip regular connections to regular blocks', () => { + expect(Routing.shouldSkipConnection('source', BlockType.FUNCTION)).toBe(false) + expect(Routing.shouldSkipConnection('source', BlockType.AGENT)).toBe(false) + expect(Routing.shouldSkipConnection(undefined, BlockType.API)).toBe(false) + }) + + it.concurrent('should not skip routing connections', () => { + expect(Routing.shouldSkipConnection('condition-test-if', BlockType.FUNCTION)).toBe(false) + expect(Routing.shouldSkipConnection('condition-test-else', BlockType.AGENT)).toBe(false) + }) + + it.concurrent('should handle empty/undefined types', () => { + expect(Routing.shouldSkipConnection('', '')).toBe(false) + expect(Routing.shouldSkipConnection(undefined, '')).toBe(false) + }) + }) + + describe('getBehavior', () => { + it.concurrent('should return correct behavior for each category', () => { + const flowControlBehavior = Routing.getBehavior(BlockType.PARALLEL) + expect(flowControlBehavior).toEqual({ + shouldActivateDownstream: false, + requiresActivePathCheck: true, + skipInSelectiveActivation: true, + }) + + const routingBehavior = Routing.getBehavior(BlockType.ROUTER) + expect(routingBehavior).toEqual({ + shouldActivateDownstream: true, + requiresActivePathCheck: false, + skipInSelectiveActivation: false, + }) + + const regularBehavior = Routing.getBehavior(BlockType.FUNCTION) + expect(regularBehavior).toEqual({ + shouldActivateDownstream: true, + requiresActivePathCheck: false, + skipInSelectiveActivation: false, + }) + }) + }) +}) diff --git a/apps/sim/executor/routing/routing.ts b/apps/sim/executor/routing/routing.ts new file mode 100644 index 000000000..09db1df8c --- /dev/null +++ b/apps/sim/executor/routing/routing.ts @@ -0,0 +1,97 @@ +import { BlockType } from '@/executor/consts' + +export enum BlockCategory { + ROUTING_BLOCK = 'routing', // router, condition - make routing decisions + FLOW_CONTROL = 'flow-control', // parallel, loop - control execution flow + REGULAR_BLOCK = 'regular', // function, agent, etc. - regular execution +} + +export interface RoutingBehavior { + shouldActivateDownstream: boolean + requiresActivePathCheck: boolean + skipInSelectiveActivation: boolean +} + +/** + * Centralized routing strategy that defines how different block types + * should behave in the execution path system. + */ +export class Routing { + private static readonly BEHAVIOR_MAP: Record = { + [BlockCategory.ROUTING_BLOCK]: { + shouldActivateDownstream: true, + requiresActivePathCheck: false, + skipInSelectiveActivation: false, + }, + [BlockCategory.FLOW_CONTROL]: { + shouldActivateDownstream: false, + requiresActivePathCheck: true, + skipInSelectiveActivation: true, + }, + [BlockCategory.REGULAR_BLOCK]: { + shouldActivateDownstream: true, + requiresActivePathCheck: false, + skipInSelectiveActivation: false, + }, + } + + private static readonly BLOCK_TYPE_TO_CATEGORY: Record = { + // Flow control blocks + [BlockType.PARALLEL]: BlockCategory.FLOW_CONTROL, + [BlockType.LOOP]: BlockCategory.FLOW_CONTROL, + + // Routing blocks + [BlockType.ROUTER]: BlockCategory.ROUTING_BLOCK, + [BlockType.CONDITION]: BlockCategory.ROUTING_BLOCK, + + // Regular blocks (default category) + [BlockType.FUNCTION]: BlockCategory.REGULAR_BLOCK, + [BlockType.AGENT]: BlockCategory.REGULAR_BLOCK, + [BlockType.API]: BlockCategory.REGULAR_BLOCK, + [BlockType.EVALUATOR]: BlockCategory.REGULAR_BLOCK, + [BlockType.RESPONSE]: BlockCategory.REGULAR_BLOCK, + [BlockType.WORKFLOW]: BlockCategory.REGULAR_BLOCK, + [BlockType.STARTER]: BlockCategory.REGULAR_BLOCK, + } + + static getCategory(blockType: string): BlockCategory { + return Routing.BLOCK_TYPE_TO_CATEGORY[blockType] || BlockCategory.REGULAR_BLOCK + } + + static getBehavior(blockType: string): RoutingBehavior { + const category = Routing.getCategory(blockType) + return Routing.BEHAVIOR_MAP[category] + } + + static shouldActivateDownstream(blockType: string): boolean { + return Routing.getBehavior(blockType).shouldActivateDownstream + } + + static requiresActivePathCheck(blockType: string): boolean { + return Routing.getBehavior(blockType).requiresActivePathCheck + } + + static shouldSkipInSelectiveActivation(blockType: string): boolean { + return Routing.getBehavior(blockType).skipInSelectiveActivation + } + + /** + * Checks if a connection should be skipped during selective activation + */ + static shouldSkipConnection(sourceHandle: string | undefined, targetBlockType: string): boolean { + // Skip flow control blocks + if (Routing.shouldSkipInSelectiveActivation(targetBlockType)) { + return true + } + + // Skip flow control specific connections + const flowControlHandles = [ + 'parallel-start-source', + 'parallel-end-source', + 'loop-start-source', + 'loop-end-source', + ] + + return flowControlHandles.includes(sourceHandle || '') + } +} diff --git a/apps/sim/executor/tests/executor-layer-validation.test.ts b/apps/sim/executor/tests/executor-layer-validation.test.ts new file mode 100644 index 000000000..7f6eb5a55 --- /dev/null +++ b/apps/sim/executor/tests/executor-layer-validation.test.ts @@ -0,0 +1,227 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { Executor } from '@/executor' +import { BlockType } from '@/executor/consts' +import type { SerializedWorkflow } from '@/serializer/types' + +describe('Full Executor Test', () => { + let workflow: SerializedWorkflow + let executor: Executor + + beforeEach(() => { + workflow = { + version: '2.0', + blocks: [ + { + id: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19', + position: { x: 0, y: 0 }, + metadata: { id: BlockType.STARTER, name: 'Start' }, + config: { tool: BlockType.STARTER, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'f29a40b7-125a-45a7-a670-af14a1498f94', + position: { x: 100, y: 0 }, + metadata: { id: BlockType.ROUTER, name: 'Router 1' }, + config: { + tool: BlockType.ROUTER, + params: { + prompt: 'if x then function 1\nif y then parallel\n\ninput: x', + model: 'gpt-4o', + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + position: { x: 200, y: -50 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, + config: { tool: BlockType.FUNCTION, params: { code: "return 'one'" } }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'a62902db-fd8d-4851-aa88-acd5e7667497', + position: { x: 200, y: 50 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '0494cf56-2520-4e29-98ad-313ea55cf142', + position: { x: 300, y: -50 }, + metadata: { id: 'condition', name: 'Condition 1' }, + config: { tool: 'condition', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '033ea142-3002-4a68-9e12-092b10b8c9c8', + position: { x: 400, y: -100 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 2' }, + config: { tool: BlockType.FUNCTION, params: { code: "return 'two'" } }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '037140a8-fda3-44e2-896c-6adea53ea30f', + position: { x: 400, y: 0 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'a91e3a02-b884-4823-8197-30ae498ac94c', + position: { x: 300, y: 100 }, + metadata: { id: 'agent', name: 'Agent 1' }, + config: { tool: 'agent', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '97974a42-cdf4-4810-9caa-b5e339f42ab0', + position: { x: 500, y: 0 }, + metadata: { id: 'agent', name: 'Agent 2' }, + config: { tool: 'agent', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + { + source: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19', + target: 'f29a40b7-125a-45a7-a670-af14a1498f94', + }, + { + source: 'f29a40b7-125a-45a7-a670-af14a1498f94', + target: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + }, + { + source: 'f29a40b7-125a-45a7-a670-af14a1498f94', + target: 'a62902db-fd8d-4851-aa88-acd5e7667497', + }, + { + source: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + target: '0494cf56-2520-4e29-98ad-313ea55cf142', + }, + { + source: '0494cf56-2520-4e29-98ad-313ea55cf142', + target: '033ea142-3002-4a68-9e12-092b10b8c9c8', + sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-if', + }, + { + source: '0494cf56-2520-4e29-98ad-313ea55cf142', + target: '037140a8-fda3-44e2-896c-6adea53ea30f', + sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-else', + }, + { + source: 'a62902db-fd8d-4851-aa88-acd5e7667497', + target: 'a91e3a02-b884-4823-8197-30ae498ac94c', + sourceHandle: 'parallel-start-source', + }, + { + source: '037140a8-fda3-44e2-896c-6adea53ea30f', + target: '97974a42-cdf4-4810-9caa-b5e339f42ab0', + sourceHandle: 'parallel-start-source', + }, + ], + loops: {}, + parallels: { + 'a62902db-fd8d-4851-aa88-acd5e7667497': { + id: 'a62902db-fd8d-4851-aa88-acd5e7667497', + nodes: ['a91e3a02-b884-4823-8197-30ae498ac94c'], + distribution: ['item1', 'item2'], + }, + '037140a8-fda3-44e2-896c-6adea53ea30f': { + id: '037140a8-fda3-44e2-896c-6adea53ea30f', + nodes: ['97974a42-cdf4-4810-9caa-b5e339f42ab0'], + distribution: ['item1', 'item2'], + }, + }, + } + + executor = new Executor(workflow) + }) + + it('should test the full executor flow and see what happens', async () => { + // Mock the necessary functions to avoid actual API calls + const mockInput = {} + + try { + // Execute the workflow + const result = await executor.execute('test-workflow-id') + + // Check if it's an ExecutionResult (not StreamingExecution) + if ('success' in result) { + // Check if there are any logs that might indicate what happened + if (result.logs) { + } + + // The test itself doesn't need to assert anything specific + // We just want to see what the executor does + expect(result.success).toBeDefined() + } else { + expect(result).toBeDefined() + } + } catch (error) { + console.error('Execution error:', error) + // Log the error but don't fail the test - we want to see what happens + } + }) + + it('should test the executor getNextExecutionLayer method directly', async () => { + // Create a mock context in the exact state after the condition executes + const context = (executor as any).createExecutionContext('test-workflow', new Date()) + + // Set up the state as it would be after the condition executes + context.executedBlocks.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start + context.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1 + context.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') // Function 1 + context.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142') // Condition 1 + context.executedBlocks.add('033ea142-3002-4a68-9e12-092b10b8c9c8') // Function 2 + + // Set router decision + context.decisions.router.set( + 'f29a40b7-125a-45a7-a670-af14a1498f94', + 'd09b0a90-2c59-4a2c-af15-c30321e36d9b' + ) + + // Set condition decision to if path (Function 2) + context.decisions.condition.set( + '0494cf56-2520-4e29-98ad-313ea55cf142', + '0494cf56-2520-4e29-98ad-313ea55cf142-if' + ) + + // Set up active execution path as it should be after condition + context.activeExecutionPath.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') + context.activeExecutionPath.add('f29a40b7-125a-45a7-a670-af14a1498f94') + context.activeExecutionPath.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') + context.activeExecutionPath.add('0494cf56-2520-4e29-98ad-313ea55cf142') + context.activeExecutionPath.add('033ea142-3002-4a68-9e12-092b10b8c9c8') + + // Get the next execution layer + const nextLayer = (executor as any).getNextExecutionLayer(context) + + // Check if Parallel 2 is in the next execution layer + const hasParallel2 = nextLayer.includes('037140a8-fda3-44e2-896c-6adea53ea30f') + + // Check if Agent 2 is in the next execution layer + const hasAgent2 = nextLayer.includes('97974a42-cdf4-4810-9caa-b5e339f42ab0') + + // The key test: Parallel 2 should NOT be in the next execution layer + expect(nextLayer).not.toContain('037140a8-fda3-44e2-896c-6adea53ea30f') + expect(nextLayer).not.toContain('97974a42-cdf4-4810-9caa-b5e339f42ab0') + }) +}) diff --git a/apps/sim/executor/tests/nested-router-condition.test.ts b/apps/sim/executor/tests/nested-router-condition.test.ts new file mode 100644 index 000000000..3e4865cab --- /dev/null +++ b/apps/sim/executor/tests/nested-router-condition.test.ts @@ -0,0 +1,307 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/consts' +import { PathTracker } from '@/executor/path/path' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedWorkflow } from '@/serializer/types' + +describe('Nested Routing Fix - Router → Condition → Target', () => { + let workflow: SerializedWorkflow + let pathTracker: PathTracker + let mockContext: ExecutionContext + + beforeEach(() => { + // Create a workflow similar to the screenshot: Router → Condition → Function/Parallel + workflow = { + version: '2.0', + blocks: [ + { + id: 'starter', + position: { x: 0, y: 0 }, + metadata: { id: BlockType.STARTER, name: 'Start' }, + config: { tool: BlockType.STARTER, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'router-1', + position: { x: 100, y: 0 }, + metadata: { id: BlockType.ROUTER, name: 'Router 1' }, + config: { tool: BlockType.ROUTER, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-2', + position: { x: 200, y: -100 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 2' }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'condition-1', + position: { x: 200, y: 100 }, + metadata: { id: BlockType.CONDITION, name: 'Condition 1' }, + config: { tool: BlockType.CONDITION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-4', + position: { x: 350, y: 50 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 4' }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'parallel-block', + position: { x: 350, y: 150 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel Block' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'agent-inside-parallel', + position: { x: 450, y: 150 }, + metadata: { id: BlockType.AGENT, name: 'Agent Inside Parallel' }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + { source: 'starter', target: 'router-1' }, + { source: 'router-1', target: 'function-2' }, + { source: 'router-1', target: 'condition-1' }, + { + source: 'condition-1', + target: 'function-4', + sourceHandle: 'condition-b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if', + }, + { + source: 'condition-1', + target: 'parallel-block', + sourceHandle: 'condition-b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else', + }, + { + source: 'parallel-block', + target: 'agent-inside-parallel', + sourceHandle: 'parallel-start-source', + }, + ], + loops: {}, + parallels: { + 'parallel-block': { + id: 'parallel-block', + nodes: ['agent-inside-parallel'], + distribution: ['item1', 'item2'], + }, + }, + } + + pathTracker = new PathTracker(workflow) + + mockContext = { + workflowId: 'test-workflow', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + workflow, + } + + // Initialize starter as executed and in active path + mockContext.executedBlocks.add('starter') + mockContext.activeExecutionPath.add('starter') + mockContext.activeExecutionPath.add('router-1') + }) + + it('should handle nested routing: router selects condition, condition selects function', () => { + // Step 1: Router selects the condition path (not function-2) + mockContext.blockStates.set('router-1', { + output: { + selectedPath: { + blockId: 'condition-1', + blockType: BlockType.CONDITION, + blockTitle: 'Condition 1', + }, + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('router-1') + + // Update paths after router execution + pathTracker.updateExecutionPaths(['router-1'], mockContext) + + // Verify router decision + expect(mockContext.decisions.router.get('router-1')).toBe('condition-1') + + // After router execution, condition should be active but not function-2 + expect(mockContext.activeExecutionPath.has('condition-1')).toBe(true) + expect(mockContext.activeExecutionPath.has('function-2')).toBe(false) + + // CRITICAL: Parallel block should NOT be activated yet + expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false) + expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false) + + // Step 2: Condition executes and selects function-4 (not parallel) + mockContext.blockStates.set('condition-1', { + output: { + result: 'two', + stdout: '', + conditionResult: true, + selectedPath: { + blockId: 'function-4', + blockType: BlockType.FUNCTION, + blockTitle: 'Function 4', + }, + selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('condition-1') + + // Update paths after condition execution + pathTracker.updateExecutionPaths(['condition-1'], mockContext) + + // Verify condition decision + expect(mockContext.decisions.condition.get('condition-1')).toBe( + 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if' + ) + + // After condition execution, function-4 should be active + expect(mockContext.activeExecutionPath.has('function-4')).toBe(true) + + // CRITICAL: Parallel block should still NOT be activated + expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false) + expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false) + }) + + it('should handle nested routing: router selects condition, condition selects parallel', () => { + // Step 1: Router selects the condition path + mockContext.blockStates.set('router-1', { + output: { + selectedPath: { + blockId: 'condition-1', + blockType: BlockType.CONDITION, + blockTitle: 'Condition 1', + }, + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('router-1') + + pathTracker.updateExecutionPaths(['router-1'], mockContext) + + // Step 2: Condition executes and selects parallel-block (not function-4) + mockContext.blockStates.set('condition-1', { + output: { + result: 'else', + stdout: '', + conditionResult: false, + selectedPath: { + blockId: 'parallel-block', + blockType: BlockType.PARALLEL, + blockTitle: 'Parallel Block', + }, + selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('condition-1') + + pathTracker.updateExecutionPaths(['condition-1'], mockContext) + + // Verify condition decision + expect(mockContext.decisions.condition.get('condition-1')).toBe( + 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else' + ) + + // After condition execution, parallel-block should be active + expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(true) + + // Function-4 should NOT be activated + expect(mockContext.activeExecutionPath.has('function-4')).toBe(false) + + // The agent inside parallel should NOT be automatically activated + // It should only be activated when the parallel block executes + expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false) + }) + + it('should prevent parallel blocks from executing when not selected by nested routing', () => { + // This test simulates the exact scenario from the bug report + + // Step 1: Router selects condition path + mockContext.blockStates.set('router-1', { + output: { + selectedPath: { + blockId: 'condition-1', + blockType: BlockType.CONDITION, + blockTitle: 'Condition 1', + }, + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('router-1') + pathTracker.updateExecutionPaths(['router-1'], mockContext) + + // Step 2: Condition selects function-4 (NOT parallel) + mockContext.blockStates.set('condition-1', { + output: { + result: 'two', + stdout: '', + conditionResult: true, + selectedPath: { + blockId: 'function-4', + blockType: BlockType.FUNCTION, + blockTitle: 'Function 4', + }, + selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('condition-1') + pathTracker.updateExecutionPaths(['condition-1'], mockContext) + + // Step 3: Simulate what the executor's getNextExecutionLayer would do + const blocksToExecute = workflow.blocks.filter( + (block) => + mockContext.activeExecutionPath.has(block.id) && !mockContext.executedBlocks.has(block.id) + ) + + const blockIds = blocksToExecute.map((b) => b.id) + + // Should only include function-4, NOT parallel-block + expect(blockIds).toContain('function-4') + expect(blockIds).not.toContain('parallel-block') + expect(blockIds).not.toContain('agent-inside-parallel') + + // Verify that parallel block is not in active path + expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false) + + // Verify that isInActivePath also returns false for parallel block + const isParallelActive = pathTracker.isInActivePath('parallel-block', mockContext) + expect(isParallelActive).toBe(false) + }) +}) diff --git a/apps/sim/executor/tests/parallel-handler-routing.test.ts b/apps/sim/executor/tests/parallel-handler-routing.test.ts new file mode 100644 index 000000000..00bd07403 --- /dev/null +++ b/apps/sim/executor/tests/parallel-handler-routing.test.ts @@ -0,0 +1,206 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/consts' +import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler' +import { PathTracker } from '@/executor/path/path' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedWorkflow } from '@/serializer/types' + +describe('Parallel Handler Integration with PathTracker', () => { + let workflow: SerializedWorkflow + let pathTracker: PathTracker + let parallelHandler: ParallelBlockHandler + let mockContext: ExecutionContext + + beforeEach(() => { + // Create a simplified workflow with condition → parallel scenario + workflow = { + version: '2.0', + blocks: [ + { + id: 'condition-1', + position: { x: 0, y: 0 }, + metadata: { id: BlockType.CONDITION, name: 'Condition 1' }, + config: { tool: BlockType.CONDITION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-2', + position: { x: 100, y: -50 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 2' }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'parallel-2', + position: { x: 100, y: 50 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'agent-2', + position: { x: 200, y: 50 }, + metadata: { id: BlockType.AGENT, name: 'Agent 2' }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + // Condition → Function 2 (if path) + { + source: 'condition-1', + target: 'function-2', + sourceHandle: 'condition-test-if', + }, + // Condition → Parallel 2 (else path) + { + source: 'condition-1', + target: 'parallel-2', + sourceHandle: 'condition-test-else', + }, + // Parallel 2 → Agent 2 + { + source: 'parallel-2', + target: 'agent-2', + sourceHandle: 'parallel-start-source', + }, + ], + loops: {}, + parallels: { + 'parallel-2': { + id: 'parallel-2', + nodes: ['agent-2'], + distribution: ['item1', 'item2'], + }, + }, + } + + pathTracker = new PathTracker(workflow) + parallelHandler = new ParallelBlockHandler(undefined, pathTracker) + + mockContext = { + workflowId: 'test-workflow', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + workflow, + } + }) + + it('should not allow parallel block to execute when not in active path', async () => { + // Set up scenario where condition selected function-2 (if path), not parallel-2 (else path) + mockContext.decisions.condition.set('condition-1', 'test-if') + mockContext.executedBlocks.add('condition-1') + mockContext.activeExecutionPath.add('condition-1') + mockContext.activeExecutionPath.add('function-2') // Only function-2 should be active + + // Parallel-2 should NOT be in active path + expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(false) + + // Test PathTracker's isInActivePath method + const isParallel2Active = pathTracker.isInActivePath('parallel-2', mockContext) + expect(isParallel2Active).toBe(false) + + // Get the parallel block + const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')! + + // Try to execute the parallel block + const result = await parallelHandler.execute(parallelBlock, {}, mockContext) + + // The parallel block should execute (return started: true) but should NOT activate its children + expect(result).toMatchObject({ + parallelId: 'parallel-2', + started: true, + }) + + // CRITICAL: Agent 2 should NOT be activated because parallel-2 is not in active path + expect(mockContext.activeExecutionPath.has('agent-2')).toBe(false) + }) + + it('should allow parallel block to execute and activate children when in active path', async () => { + // Set up scenario where condition selected parallel-2 (else path) + mockContext.decisions.condition.set('condition-1', 'test-else') + mockContext.executedBlocks.add('condition-1') + mockContext.activeExecutionPath.add('condition-1') + mockContext.activeExecutionPath.add('parallel-2') // Parallel-2 should be active + + // Parallel-2 should be in active path + expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(true) + + // Test PathTracker's isInActivePath method + const isParallel2Active = pathTracker.isInActivePath('parallel-2', mockContext) + expect(isParallel2Active).toBe(true) + + // Get the parallel block + const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')! + + // Try to execute the parallel block + const result = await parallelHandler.execute(parallelBlock, {}, mockContext) + + // The parallel block should execute and activate its children + expect(result).toMatchObject({ + parallelId: 'parallel-2', + started: true, + }) + + // Agent 2 should be activated because parallel-2 is in active path + expect(mockContext.activeExecutionPath.has('agent-2')).toBe(true) + }) + + it('should test the routing failure scenario with parallel block', async () => { + // Step 1: Condition 1 selects Function 2 (if path) + mockContext.blockStates.set('condition-1', { + output: { + result: 'one', + stdout: '', + conditionResult: true, + selectedPath: { + blockId: 'function-2', + blockType: 'function', + blockTitle: 'Function 2', + }, + selectedConditionId: 'test-if', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('condition-1') + mockContext.activeExecutionPath.add('condition-1') + + // Update paths after condition execution + pathTracker.updateExecutionPaths(['condition-1'], mockContext) + + // Verify condition selected if path + expect(mockContext.decisions.condition.get('condition-1')).toBe('test-if') + expect(mockContext.activeExecutionPath.has('function-2')).toBe(true) + expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(false) + + // Step 2: Try to execute parallel-2 (should not activate children) + const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')! + const result = await parallelHandler.execute(parallelBlock, {}, mockContext) + + // Parallel should execute but not activate children + expect(result).toMatchObject({ + parallelId: 'parallel-2', + started: true, + }) + + // CRITICAL: Agent 2 should NOT be activated + expect(mockContext.activeExecutionPath.has('agent-2')).toBe(false) + }) +}) diff --git a/apps/sim/executor/tests/router-parallel-execution.test.ts b/apps/sim/executor/tests/router-parallel-execution.test.ts new file mode 100644 index 000000000..eed2e82cf --- /dev/null +++ b/apps/sim/executor/tests/router-parallel-execution.test.ts @@ -0,0 +1,318 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/consts' +import { PathTracker } from '@/executor/path/path' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedWorkflow } from '@/serializer/types' + +describe('Router and Condition Block Path Selection in Complex Workflows', () => { + let workflow: SerializedWorkflow + let pathTracker: PathTracker + let mockContext: ExecutionContext + + beforeEach(() => { + workflow = { + version: '2.0', + blocks: [ + { + id: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19', + position: { x: 0, y: 0 }, + metadata: { id: BlockType.STARTER, name: 'Start' }, + config: { tool: BlockType.STARTER, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'f29a40b7-125a-45a7-a670-af14a1498f94', + position: { x: 100, y: 0 }, + metadata: { id: BlockType.ROUTER, name: 'Router 1' }, + config: { tool: BlockType.ROUTER, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + position: { x: 200, y: -50 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 1' }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'a62902db-fd8d-4851-aa88-acd5e7667497', + position: { x: 200, y: 50 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '0494cf56-2520-4e29-98ad-313ea55cf142', + position: { x: 300, y: -50 }, + metadata: { id: BlockType.CONDITION, name: 'Condition 1' }, + config: { tool: BlockType.CONDITION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '033ea142-3002-4a68-9e12-092b10b8c9c8', + position: { x: 400, y: -100 }, + metadata: { id: BlockType.FUNCTION, name: 'Function 2' }, + config: { tool: BlockType.FUNCTION, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '037140a8-fda3-44e2-896c-6adea53ea30f', + position: { x: 400, y: 0 }, + metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' }, + config: { tool: BlockType.PARALLEL, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'a91e3a02-b884-4823-8197-30ae498ac94c', + position: { x: 300, y: 100 }, + metadata: { id: BlockType.AGENT, name: 'Agent 1' }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: '97974a42-cdf4-4810-9caa-b5e339f42ab0', + position: { x: 500, y: 0 }, + metadata: { id: BlockType.AGENT, name: 'Agent 2' }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + // Start → Router 1 + { + source: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19', + target: 'f29a40b7-125a-45a7-a670-af14a1498f94', + }, + // Router 1 → Function 1 + { + source: 'f29a40b7-125a-45a7-a670-af14a1498f94', + target: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + }, + // Router 1 → Parallel 1 + { + source: 'f29a40b7-125a-45a7-a670-af14a1498f94', + target: 'a62902db-fd8d-4851-aa88-acd5e7667497', + }, + // Function 1 → Condition 1 + { + source: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + target: '0494cf56-2520-4e29-98ad-313ea55cf142', + }, + // Condition 1 → Function 2 (if path) + { + source: '0494cf56-2520-4e29-98ad-313ea55cf142', + target: '033ea142-3002-4a68-9e12-092b10b8c9c8', + sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-if', + }, + // Condition 1 → Parallel 2 (else path) + { + source: '0494cf56-2520-4e29-98ad-313ea55cf142', + target: '037140a8-fda3-44e2-896c-6adea53ea30f', + sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-else', + }, + // Parallel 1 → Agent 1 + { + source: 'a62902db-fd8d-4851-aa88-acd5e7667497', + target: 'a91e3a02-b884-4823-8197-30ae498ac94c', + sourceHandle: 'parallel-start-source', + }, + // Parallel 2 → Agent 2 + { + source: '037140a8-fda3-44e2-896c-6adea53ea30f', + target: '97974a42-cdf4-4810-9caa-b5e339f42ab0', + sourceHandle: 'parallel-start-source', + }, + ], + loops: {}, + parallels: { + 'a62902db-fd8d-4851-aa88-acd5e7667497': { + id: 'a62902db-fd8d-4851-aa88-acd5e7667497', + nodes: ['a91e3a02-b884-4823-8197-30ae498ac94c'], + distribution: ['item1', 'item2'], + }, + '037140a8-fda3-44e2-896c-6adea53ea30f': { + id: '037140a8-fda3-44e2-896c-6adea53ea30f', + nodes: ['97974a42-cdf4-4810-9caa-b5e339f42ab0'], + distribution: ['item1', 'item2'], + }, + }, + } + + pathTracker = new PathTracker(workflow) + + mockContext = { + workflowId: 'test-workflow', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + workflow, + } + + // Initialize execution state + mockContext.executedBlocks.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start + mockContext.activeExecutionPath.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start + mockContext.activeExecutionPath.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1 + }) + + it('should reproduce the exact router and condition block path selection scenario', () => { + // Step 1: Router 1 executes and selects Function 1 (not Parallel 1) + mockContext.blockStates.set('f29a40b7-125a-45a7-a670-af14a1498f94', { + output: { + selectedPath: { + blockId: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b', + blockType: BlockType.FUNCTION, + blockTitle: 'Function 1', + }, + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94') + + pathTracker.updateExecutionPaths(['f29a40b7-125a-45a7-a670-af14a1498f94'], mockContext) + + // Verify router selected Function 1 + expect(mockContext.decisions.router.get('f29a40b7-125a-45a7-a670-af14a1498f94')).toBe( + 'd09b0a90-2c59-4a2c-af15-c30321e36d9b' + ) + expect(mockContext.activeExecutionPath.has('d09b0a90-2c59-4a2c-af15-c30321e36d9b')).toBe(true) // Function 1 + + // Parallel 1 should NOT be in active path (not selected by router) + expect(mockContext.activeExecutionPath.has('a62902db-fd8d-4851-aa88-acd5e7667497')).toBe(false) // Parallel 1 + expect(mockContext.activeExecutionPath.has('a91e3a02-b884-4823-8197-30ae498ac94c')).toBe(false) // Agent 1 + + // Step 2: Function 1 executes and returns "one" + mockContext.blockStates.set('d09b0a90-2c59-4a2c-af15-c30321e36d9b', { + output: { + result: 'one', + stdout: '', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') + + pathTracker.updateExecutionPaths(['d09b0a90-2c59-4a2c-af15-c30321e36d9b'], mockContext) + + // Function 1 should activate Condition 1 + expect(mockContext.activeExecutionPath.has('0494cf56-2520-4e29-98ad-313ea55cf142')).toBe(true) // Condition 1 + + // Parallel 2 should NOT be in active path yet + expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2 + expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2 + + // Step 3: Condition 1 executes and selects Function 2 (if path, not else/parallel path) + mockContext.blockStates.set('0494cf56-2520-4e29-98ad-313ea55cf142', { + output: { + result: 'one', + stdout: '', + conditionResult: true, + selectedPath: { + blockId: '033ea142-3002-4a68-9e12-092b10b8c9c8', + blockType: BlockType.FUNCTION, + blockTitle: 'Function 2', + }, + selectedConditionId: '0494cf56-2520-4e29-98ad-313ea55cf142-if', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142') + + pathTracker.updateExecutionPaths(['0494cf56-2520-4e29-98ad-313ea55cf142'], mockContext) + + // Verify condition selected the if path (Function 2) + expect(mockContext.decisions.condition.get('0494cf56-2520-4e29-98ad-313ea55cf142')).toBe( + '0494cf56-2520-4e29-98ad-313ea55cf142-if' + ) + expect(mockContext.activeExecutionPath.has('033ea142-3002-4a68-9e12-092b10b8c9c8')).toBe(true) // Function 2 + + // CRITICAL: Parallel 2 should NOT be in active path (condition selected if, not else) + expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2 + expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2 + + // Step 4: Function 2 executes (this should be the end of the workflow) + mockContext.blockStates.set('033ea142-3002-4a68-9e12-092b10b8c9c8', { + output: { + result: 'two', + stdout: '', + }, + executed: true, + executionTime: 0, + }) + mockContext.executedBlocks.add('033ea142-3002-4a68-9e12-092b10b8c9c8') + + pathTracker.updateExecutionPaths(['033ea142-3002-4a68-9e12-092b10b8c9c8'], mockContext) + + // Final verification: Parallel 2 and Agent 2 should NEVER be in active path + expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2 + expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2 + + // Simulate what executor's getNextExecutionLayer would return + const blocksToExecute = workflow.blocks.filter( + (block) => + mockContext.activeExecutionPath.has(block.id) && !mockContext.executedBlocks.has(block.id) + ) + const blockIds = blocksToExecute.map((b) => b.id) + + // Should be empty (no more blocks to execute) + expect(blockIds).toHaveLength(0) + + // Should NOT include Parallel 2 or Agent 2 + expect(blockIds).not.toContain('037140a8-fda3-44e2-896c-6adea53ea30f') // Parallel 2 + expect(blockIds).not.toContain('97974a42-cdf4-4810-9caa-b5e339f42ab0') // Agent 2 + }) + + it('should test the isInActivePath method for Parallel 2', () => { + // Set up the same execution state as above + mockContext.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1 + mockContext.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') // Function 1 + mockContext.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142') // Condition 1 + + // Set router decision + mockContext.decisions.router.set( + 'f29a40b7-125a-45a7-a670-af14a1498f94', + 'd09b0a90-2c59-4a2c-af15-c30321e36d9b' + ) + + // Set condition decision to if path (not else path) + mockContext.decisions.condition.set( + '0494cf56-2520-4e29-98ad-313ea55cf142', + '0494cf56-2520-4e29-98ad-313ea55cf142-if' + ) + + // Test isInActivePath for Parallel 2 + const isParallel2Active = pathTracker.isInActivePath( + '037140a8-fda3-44e2-896c-6adea53ea30f', + mockContext + ) + + // Should be false because condition selected if path, not else path + expect(isParallel2Active).toBe(false) + }) +}) diff --git a/apps/sim/executor/utils.test.ts b/apps/sim/executor/utils.test.ts index 4453bc580..3df72a403 100644 --- a/apps/sim/executor/utils.test.ts +++ b/apps/sim/executor/utils.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { StreamingResponseFormatProcessor, streamingResponseFormatProcessor } from './utils' +import { + StreamingResponseFormatProcessor, + streamingResponseFormatProcessor, +} from '@/executor/utils' vi.mock('@/lib/logs/console-logger', () => ({ createLogger: vi.fn().mockReturnValue({ diff --git a/apps/sim/executor/utils.ts b/apps/sim/executor/utils.ts index 007404319..afaab5c2d 100644 --- a/apps/sim/executor/utils.ts +++ b/apps/sim/executor/utils.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console-logger' -import type { ResponseFormatStreamProcessor } from './types' +import type { ResponseFormatStreamProcessor } from '@/executor/types' const logger = createLogger('ExecutorUtils')