mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
test[serializer]: added tests for serializer
This commit is contained in:
662
sim/app/serializer/__test-utils__/test-workflows.ts
Normal file
662
sim/app/serializer/__test-utils__/test-workflows.ts
Normal file
@@ -0,0 +1,662 @@
|
||||
/**
|
||||
* Test Workflows
|
||||
*
|
||||
* This file contains test fixtures for serializer tests, providing
|
||||
* sample workflow states with different configurations.
|
||||
*/
|
||||
import { Edge } from 'reactflow'
|
||||
import { BlockState, Loop } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Workflow State Interface
|
||||
*/
|
||||
export interface WorkflowStateFixture {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
loops: Record<string, Loop>
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a minimal workflow with just a starter and one block
|
||||
*/
|
||||
export function createMinimalWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: {
|
||||
id: 'description',
|
||||
type: 'long-input',
|
||||
value: 'This is the starter block',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Agent Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'anthropic',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'claude-3-7-sonnet-20250219',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Hello, world!',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value: '[]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'starter',
|
||||
target: 'agent1',
|
||||
},
|
||||
]
|
||||
|
||||
const loops: Record<string, Loop> = {}
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with condition blocks
|
||||
*/
|
||||
export function createConditionalWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: {
|
||||
id: 'description',
|
||||
type: 'long-input',
|
||||
value: 'This is the starter block',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
condition1: {
|
||||
id: 'condition1',
|
||||
type: 'condition',
|
||||
name: 'Condition Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
condition: {
|
||||
id: 'condition',
|
||||
type: 'long-input',
|
||||
value: 'input.value > 10',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'True Path Agent',
|
||||
position: { x: 600, y: -100 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'anthropic',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'claude-3-7-sonnet-20250219',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Value is greater than 10',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value: '[]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent2: {
|
||||
id: 'agent2',
|
||||
type: 'agent',
|
||||
name: 'False Path Agent',
|
||||
position: { x: 600, y: 100 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'anthropic',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'claude-3-7-sonnet-20250219',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Value is less than or equal to 10',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value: '[]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'starter',
|
||||
target: 'condition1',
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'condition1',
|
||||
target: 'agent1',
|
||||
sourceHandle: 'condition-true',
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'condition1',
|
||||
target: 'agent2',
|
||||
sourceHandle: 'condition-false',
|
||||
},
|
||||
]
|
||||
|
||||
const loops: Record<string, Loop> = {}
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with a loop
|
||||
*/
|
||||
export function createLoopWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: {
|
||||
id: 'description',
|
||||
type: 'long-input',
|
||||
value: 'This is the starter block',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
function1: {
|
||||
id: 'function1',
|
||||
type: 'function',
|
||||
name: 'Function Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
code: {
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
value: 'let counter = input.counter || 0;\ncounter++;\nreturn { counter };',
|
||||
},
|
||||
language: {
|
||||
id: 'language',
|
||||
type: 'dropdown',
|
||||
value: 'javascript',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
condition1: {
|
||||
id: 'condition1',
|
||||
type: 'condition',
|
||||
name: 'Loop Condition',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {
|
||||
condition: {
|
||||
id: 'condition',
|
||||
type: 'long-input',
|
||||
value: 'input.counter < 5',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Loop Complete Agent',
|
||||
position: { x: 900, y: 100 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'anthropic',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'claude-3-7-sonnet-20250219',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Loop completed after {{input.counter}} iterations',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value: '[]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'starter',
|
||||
target: 'function1',
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'function1',
|
||||
target: 'condition1',
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'condition1',
|
||||
target: 'function1',
|
||||
sourceHandle: 'condition-true',
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'condition1',
|
||||
target: 'agent1',
|
||||
sourceHandle: 'condition-false',
|
||||
},
|
||||
]
|
||||
|
||||
const loops: Record<string, Loop> = {
|
||||
loop1: {
|
||||
id: 'loop1',
|
||||
nodes: ['function1', 'condition1'],
|
||||
maxIterations: 10,
|
||||
minIterations: 1,
|
||||
},
|
||||
}
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with multiple block types
|
||||
*/
|
||||
export function createComplexWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: {
|
||||
id: 'description',
|
||||
type: 'long-input',
|
||||
value: 'This is the starter block',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
api1: {
|
||||
id: 'api1',
|
||||
type: 'api',
|
||||
name: 'API Request',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
url: {
|
||||
id: 'url',
|
||||
type: 'short-input',
|
||||
value: 'https://api.example.com/data',
|
||||
},
|
||||
method: {
|
||||
id: 'method',
|
||||
type: 'dropdown',
|
||||
value: 'GET',
|
||||
},
|
||||
headers: {
|
||||
id: 'headers',
|
||||
type: 'table',
|
||||
value: [
|
||||
['Content-Type', 'application/json'],
|
||||
['Authorization', 'Bearer {{API_KEY}}'],
|
||||
],
|
||||
},
|
||||
body: {
|
||||
id: 'body',
|
||||
type: 'long-input',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
function1: {
|
||||
id: 'function1',
|
||||
type: 'function',
|
||||
name: 'Process Data',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {
|
||||
code: {
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
value: 'const data = input.data;\nreturn { processed: data.map(item => item.name) };',
|
||||
},
|
||||
language: {
|
||||
id: 'language',
|
||||
type: 'dropdown',
|
||||
value: 'javascript',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Summarize Data',
|
||||
position: { x: 900, y: 0 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'openai',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'gpt-4o',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Summarize the following data:\n\n{{input.processed}}',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value:
|
||||
'[{"type":"function","name":"calculator","description":"Perform calculations","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"Math expression to evaluate"}},"required":["expression"]}}]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a data analyst assistant.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value:
|
||||
'{"type":"object","properties":{"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"sentiment":{"type":"string","enum":["positive","neutral","negative"]}},"required":["summary","keyPoints","sentiment"]}',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'starter',
|
||||
target: 'api1',
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'api1',
|
||||
target: 'function1',
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'function1',
|
||||
target: 'agent1',
|
||||
},
|
||||
]
|
||||
|
||||
const loops: Record<string, Loop> = {}
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow with agent blocks that have custom tools
|
||||
*/
|
||||
export function createAgentWithToolsWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, BlockState> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: {
|
||||
id: 'description',
|
||||
type: 'long-input',
|
||||
value: 'This is the starter block',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Custom Tools Agent',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
provider: {
|
||||
id: 'provider',
|
||||
type: 'dropdown',
|
||||
value: 'openai',
|
||||
},
|
||||
model: {
|
||||
id: 'model',
|
||||
type: 'dropdown',
|
||||
value: 'gpt-4o',
|
||||
},
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Use the tools to help answer: {{input.question}}',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value:
|
||||
'[{"type":"custom-tool","name":"weather","description":"Get current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculator","description":"Calculate expression","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant with access to tools.',
|
||||
},
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'starter',
|
||||
target: 'agent1',
|
||||
},
|
||||
]
|
||||
|
||||
const loops: Record<string, Loop> = {}
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow state with an invalid block type for error testing
|
||||
*/
|
||||
export function createInvalidWorkflowState(): WorkflowStateFixture {
|
||||
const { blocks, edges, loops } = createMinimalWorkflowState()
|
||||
|
||||
// Add an invalid block type
|
||||
blocks.invalid = {
|
||||
id: 'invalid',
|
||||
type: 'invalid-type',
|
||||
name: 'Invalid Block',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
edges.push({
|
||||
id: 'edge-invalid',
|
||||
source: 'agent1',
|
||||
target: 'invalid',
|
||||
})
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized workflow with invalid metadata for error testing
|
||||
*/
|
||||
export function createInvalidSerializedWorkflow() {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'invalid',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'invalid',
|
||||
params: {},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: {
|
||||
id: 'non-existent-type',
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized workflow with missing metadata for error testing
|
||||
*/
|
||||
export function createMissingMetadataWorkflow() {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'invalid',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'invalid',
|
||||
params: {},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: undefined,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
450
sim/app/serializer/index.test.ts
Normal file
450
sim/app/serializer/index.test.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*
|
||||
* Serializer Class Unit Tests
|
||||
*
|
||||
* This file contains unit tests for the Serializer class, which is responsible for
|
||||
* converting between workflow state (blocks, edges, loops) and serialized format
|
||||
* used by the executor.
|
||||
*/
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
import {
|
||||
createAgentWithToolsWorkflowState,
|
||||
createComplexWorkflowState,
|
||||
createConditionalWorkflowState,
|
||||
createInvalidSerializedWorkflow,
|
||||
createInvalidWorkflowState,
|
||||
createLoopWorkflowState,
|
||||
createMinimalWorkflowState,
|
||||
createMissingMetadataWorkflow,
|
||||
} from './__test-utils__/test-workflows'
|
||||
import { Serializer } from './index'
|
||||
import { SerializedWorkflow } from './types'
|
||||
|
||||
// Mock getBlock function
|
||||
vi.mock('@/blocks', () => ({
|
||||
getBlock: (type: string) => {
|
||||
// Mock block configurations for different block types
|
||||
const mockConfigs: Record<string, any> = {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
description: 'Start of the workflow',
|
||||
category: 'flow',
|
||||
bgColor: '#4CAF50',
|
||||
tools: {
|
||||
access: ['starter'],
|
||||
config: {
|
||||
tool: () => 'starter',
|
||||
},
|
||||
},
|
||||
subBlocks: [{ id: 'description', type: 'long-input', label: 'Description' }],
|
||||
inputs: {},
|
||||
},
|
||||
agent: {
|
||||
name: 'Agent',
|
||||
description: 'AI Agent',
|
||||
category: 'ai',
|
||||
bgColor: '#2196F3',
|
||||
tools: {
|
||||
access: ['anthropic_chat', 'openai_chat', 'google_chat'],
|
||||
config: {
|
||||
// Use the real getProviderFromModel that we imported
|
||||
tool: (params: Record<string, any>) => getProviderFromModel(params.model || 'gpt-4o'),
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'provider', type: 'dropdown', label: 'Provider' },
|
||||
{ id: 'model', type: 'dropdown', label: 'Model' },
|
||||
{ id: 'prompt', type: 'long-input', label: 'Prompt' },
|
||||
{ id: 'tools', type: 'tool-input', label: 'Tools' },
|
||||
{ id: 'system', type: 'long-input', label: 'System Message' },
|
||||
{ id: 'responseFormat', type: 'code', label: 'Response Format' },
|
||||
],
|
||||
inputs: {
|
||||
input: { type: 'string' },
|
||||
tools: { type: 'array' },
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
name: 'Condition',
|
||||
description: 'Branch based on condition',
|
||||
category: 'flow',
|
||||
bgColor: '#FF9800',
|
||||
tools: {
|
||||
access: ['condition'],
|
||||
config: {
|
||||
tool: () => 'condition',
|
||||
},
|
||||
},
|
||||
subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }],
|
||||
inputs: {
|
||||
input: { type: 'any' },
|
||||
},
|
||||
},
|
||||
function: {
|
||||
name: 'Function',
|
||||
description: 'Execute custom code',
|
||||
category: 'code',
|
||||
bgColor: '#9C27B0',
|
||||
tools: {
|
||||
access: ['function'],
|
||||
config: {
|
||||
tool: () => 'function',
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'code', type: 'code', label: 'Code' },
|
||||
{ id: 'language', type: 'dropdown', label: 'Language' },
|
||||
],
|
||||
inputs: {
|
||||
input: { type: 'any' },
|
||||
},
|
||||
},
|
||||
api: {
|
||||
name: 'API',
|
||||
description: 'Make API request',
|
||||
category: 'data',
|
||||
bgColor: '#E91E63',
|
||||
tools: {
|
||||
access: ['api'],
|
||||
config: {
|
||||
tool: () => 'api',
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'url', type: 'short-input', label: 'URL' },
|
||||
{ id: 'method', type: 'dropdown', label: 'Method' },
|
||||
{ id: 'headers', type: 'table', label: 'Headers' },
|
||||
{ id: 'body', type: 'long-input', label: 'Body' },
|
||||
],
|
||||
inputs: {},
|
||||
},
|
||||
}
|
||||
|
||||
return mockConfigs[type] || null
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: () => ({
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Serializer', () => {
|
||||
/**
|
||||
* Serialization tests
|
||||
*/
|
||||
describe('serializeWorkflow', () => {
|
||||
test('should serialize a minimal workflow correctly', () => {
|
||||
const { blocks, edges, loops } = createMinimalWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Check if blocks are correctly serialized
|
||||
expect(serialized.blocks).toHaveLength(2)
|
||||
|
||||
// Check starter block
|
||||
const starterBlock = serialized.blocks.find((b) => b.id === 'starter')
|
||||
expect(starterBlock).toBeDefined()
|
||||
expect(starterBlock?.metadata?.id).toBe('starter')
|
||||
expect(starterBlock?.config.tool).toBe('starter')
|
||||
expect(starterBlock?.config.params.description).toBe('This is the starter block')
|
||||
|
||||
// Check agent block
|
||||
const agentBlock = serialized.blocks.find((b) => b.id === 'agent1')
|
||||
expect(agentBlock).toBeDefined()
|
||||
expect(agentBlock?.metadata?.id).toBe('agent')
|
||||
expect(agentBlock?.config.params.prompt).toBe('Hello, world!')
|
||||
expect(agentBlock?.config.params.model).toBe('claude-3-7-sonnet-20250219')
|
||||
|
||||
// Check if edges are correctly serialized
|
||||
expect(serialized.connections).toHaveLength(1)
|
||||
expect(serialized.connections[0].source).toBe('starter')
|
||||
expect(serialized.connections[0].target).toBe('agent1')
|
||||
})
|
||||
|
||||
test('should serialize a conditional workflow correctly', () => {
|
||||
const { blocks, edges, loops } = createConditionalWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Check blocks
|
||||
expect(serialized.blocks).toHaveLength(4)
|
||||
|
||||
// Check condition block
|
||||
const conditionBlock = serialized.blocks.find((b) => b.id === 'condition1')
|
||||
expect(conditionBlock).toBeDefined()
|
||||
expect(conditionBlock?.metadata?.id).toBe('condition')
|
||||
expect(conditionBlock?.config.tool).toBe('condition')
|
||||
expect(conditionBlock?.config.params.condition).toBe('input.value > 10')
|
||||
|
||||
// Check connections with handles
|
||||
expect(serialized.connections).toHaveLength(3)
|
||||
|
||||
const truePathConnection = serialized.connections.find(
|
||||
(c) => c.source === 'condition1' && c.sourceHandle === 'condition-true'
|
||||
)
|
||||
expect(truePathConnection).toBeDefined()
|
||||
expect(truePathConnection?.target).toBe('agent1')
|
||||
|
||||
const falsePathConnection = serialized.connections.find(
|
||||
(c) => c.source === 'condition1' && c.sourceHandle === 'condition-false'
|
||||
)
|
||||
expect(falsePathConnection).toBeDefined()
|
||||
expect(falsePathConnection?.target).toBe('agent2')
|
||||
})
|
||||
|
||||
test('should serialize a workflow with loops correctly', () => {
|
||||
const { blocks, edges, loops } = createLoopWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Check loops
|
||||
expect(Object.keys(serialized.loops)).toHaveLength(1)
|
||||
expect(serialized.loops.loop1).toBeDefined()
|
||||
expect(serialized.loops.loop1.nodes).toContain('function1')
|
||||
expect(serialized.loops.loop1.nodes).toContain('condition1')
|
||||
expect(serialized.loops.loop1.maxIterations).toBe(10)
|
||||
expect(serialized.loops.loop1.minIterations).toBe(1)
|
||||
|
||||
// Check connections for loop
|
||||
const loopBackConnection = serialized.connections.find(
|
||||
(c) => c.source === 'condition1' && c.target === 'function1'
|
||||
)
|
||||
expect(loopBackConnection).toBeDefined()
|
||||
expect(loopBackConnection?.sourceHandle).toBe('condition-true')
|
||||
})
|
||||
|
||||
test('should serialize a complex workflow with multiple block types', () => {
|
||||
const { blocks, edges, loops } = createComplexWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Check all blocks
|
||||
expect(serialized.blocks).toHaveLength(4)
|
||||
|
||||
// Check API block
|
||||
const apiBlock = serialized.blocks.find((b) => b.id === 'api1')
|
||||
expect(apiBlock).toBeDefined()
|
||||
expect(apiBlock?.metadata?.id).toBe('api')
|
||||
expect(apiBlock?.config.tool).toBe('api')
|
||||
expect(apiBlock?.config.params.url).toBe('https://api.example.com/data')
|
||||
expect(apiBlock?.config.params.method).toBe('GET')
|
||||
expect(apiBlock?.config.params.headers).toEqual([
|
||||
['Content-Type', 'application/json'],
|
||||
['Authorization', 'Bearer {{API_KEY}}'],
|
||||
])
|
||||
|
||||
// Check function block
|
||||
const functionBlock = serialized.blocks.find((b) => b.id === 'function1')
|
||||
expect(functionBlock).toBeDefined()
|
||||
expect(functionBlock?.metadata?.id).toBe('function')
|
||||
expect(functionBlock?.config.tool).toBe('function')
|
||||
expect(functionBlock?.config.params.language).toBe('javascript')
|
||||
|
||||
// Check agent block with response format
|
||||
const agentBlock = serialized.blocks.find((b) => b.id === 'agent1')
|
||||
expect(agentBlock).toBeDefined()
|
||||
expect(agentBlock?.metadata?.id).toBe('agent')
|
||||
expect(agentBlock?.config.tool).toBe('openai')
|
||||
expect(agentBlock?.config.params.model).toBe('gpt-4o')
|
||||
expect(agentBlock?.outputs.responseFormat).toBeDefined()
|
||||
})
|
||||
|
||||
test('should serialize agent block with custom tools correctly', () => {
|
||||
const { blocks, edges, loops } = createAgentWithToolsWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Check agent block
|
||||
const agentBlock = serialized.blocks.find((b) => b.id === 'agent1')
|
||||
expect(agentBlock).toBeDefined()
|
||||
// The model used is 'gpt-4o', so tool should be 'openai'
|
||||
expect(agentBlock?.config.tool).toBe('openai')
|
||||
expect(agentBlock?.config.params.model).toBe('gpt-4o')
|
||||
|
||||
// Tools should be preserved as-is in params
|
||||
const toolsParam = agentBlock?.config.params.tools
|
||||
expect(toolsParam).toBeDefined()
|
||||
|
||||
// Parse tools to verify content
|
||||
const tools = JSON.parse(toolsParam)
|
||||
expect(tools).toHaveLength(2)
|
||||
|
||||
// Check custom tool
|
||||
const customTool = tools.find((t: any) => t.type === 'custom-tool')
|
||||
expect(customTool).toBeDefined()
|
||||
expect(customTool.name).toBe('weather')
|
||||
|
||||
// Check function tool
|
||||
const functionTool = tools.find((t: any) => t.type === 'function')
|
||||
expect(functionTool).toBeDefined()
|
||||
expect(functionTool.name).toBe('calculator')
|
||||
})
|
||||
|
||||
test('should handle invalid block types gracefully', () => {
|
||||
const { blocks, edges, loops } = createInvalidWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
// Should throw an error when serializing an invalid block type
|
||||
expect(() => serializer.serializeWorkflow(blocks, edges, loops)).toThrow(
|
||||
'Invalid block type: invalid-type'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Deserialization tests
|
||||
*/
|
||||
describe('deserializeWorkflow', () => {
|
||||
test('should deserialize a serialized workflow correctly', () => {
|
||||
const { blocks, edges, loops } = createMinimalWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
// First serialize
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Then deserialize
|
||||
const deserialized = serializer.deserializeWorkflow(serialized)
|
||||
|
||||
// Check blocks
|
||||
expect(Object.keys(deserialized.blocks)).toHaveLength(2)
|
||||
|
||||
// Check starter block
|
||||
const starterBlock = deserialized.blocks.starter
|
||||
expect(starterBlock).toBeDefined()
|
||||
expect(starterBlock.type).toBe('starter')
|
||||
expect(starterBlock.name).toBe('Starter Block')
|
||||
expect(starterBlock.subBlocks.description.value).toBe('This is the starter block')
|
||||
|
||||
// Check agent block
|
||||
const agentBlock = deserialized.blocks.agent1
|
||||
expect(agentBlock).toBeDefined()
|
||||
expect(agentBlock.type).toBe('agent')
|
||||
expect(agentBlock.name).toBe('Agent Block')
|
||||
expect(agentBlock.subBlocks.prompt.value).toBe('Hello, world!')
|
||||
expect(agentBlock.subBlocks.model.value).toBe('claude-3-7-sonnet-20250219')
|
||||
|
||||
// Check edges
|
||||
expect(deserialized.edges).toHaveLength(1)
|
||||
expect(deserialized.edges[0].source).toBe('starter')
|
||||
expect(deserialized.edges[0].target).toBe('agent1')
|
||||
})
|
||||
|
||||
test('should deserialize a complex workflow with all block types', () => {
|
||||
const { blocks, edges, loops } = createComplexWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
// First serialize
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Then deserialize
|
||||
const deserialized = serializer.deserializeWorkflow(serialized)
|
||||
|
||||
// Check all blocks are deserialized
|
||||
expect(Object.keys(deserialized.blocks)).toHaveLength(4)
|
||||
|
||||
// Check API block
|
||||
const apiBlock = deserialized.blocks.api1
|
||||
expect(apiBlock).toBeDefined()
|
||||
expect(apiBlock.type).toBe('api')
|
||||
expect(apiBlock.subBlocks.url.value).toBe('https://api.example.com/data')
|
||||
expect(apiBlock.subBlocks.method.value).toBe('GET')
|
||||
expect(apiBlock.subBlocks.headers.value).toEqual([
|
||||
['Content-Type', 'application/json'],
|
||||
['Authorization', 'Bearer {{API_KEY}}'],
|
||||
])
|
||||
|
||||
// Check function block
|
||||
const functionBlock = deserialized.blocks.function1
|
||||
expect(functionBlock).toBeDefined()
|
||||
expect(functionBlock.type).toBe('function')
|
||||
expect(functionBlock.subBlocks.language.value).toBe('javascript')
|
||||
|
||||
// Check agent block
|
||||
const agentBlock = deserialized.blocks.agent1
|
||||
expect(agentBlock).toBeDefined()
|
||||
expect(agentBlock.type).toBe('agent')
|
||||
expect(agentBlock.subBlocks.model.value).toBe('gpt-4o')
|
||||
expect(agentBlock.subBlocks.provider.value).toBe('openai')
|
||||
})
|
||||
|
||||
test('should handle serialized workflow with invalid block metadata', () => {
|
||||
const invalidWorkflow = createInvalidSerializedWorkflow() as SerializedWorkflow
|
||||
const serializer = new Serializer()
|
||||
|
||||
// Should throw an error when deserializing an invalid block type
|
||||
expect(() => serializer.deserializeWorkflow(invalidWorkflow)).toThrow(
|
||||
'Invalid block type: non-existent-type'
|
||||
)
|
||||
})
|
||||
|
||||
test('should handle serialized workflow with missing metadata', () => {
|
||||
const invalidWorkflow = createMissingMetadataWorkflow() as SerializedWorkflow
|
||||
const serializer = new Serializer()
|
||||
|
||||
// Should throw an error when deserializing with missing metadata
|
||||
expect(() => serializer.deserializeWorkflow(invalidWorkflow)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* End-to-end serialization/deserialization tests
|
||||
*/
|
||||
describe('round-trip serialization', () => {
|
||||
test('should preserve all data through serialization and deserialization', () => {
|
||||
const { blocks, edges, loops } = createComplexWorkflowState()
|
||||
const serializer = new Serializer()
|
||||
|
||||
// Serialize
|
||||
const serialized = serializer.serializeWorkflow(blocks, edges, loops)
|
||||
|
||||
// Deserialize
|
||||
const deserialized = serializer.deserializeWorkflow(serialized)
|
||||
|
||||
// Re-serialize to check for consistency
|
||||
const reserialized = serializer.serializeWorkflow(
|
||||
deserialized.blocks,
|
||||
deserialized.edges,
|
||||
loops
|
||||
)
|
||||
|
||||
// Compare the two serialized versions
|
||||
expect(reserialized.blocks.length).toBe(serialized.blocks.length)
|
||||
expect(reserialized.connections.length).toBe(serialized.connections.length)
|
||||
|
||||
// Check blocks by ID
|
||||
serialized.blocks.forEach((originalBlock) => {
|
||||
const reserializedBlock = reserialized.blocks.find((b) => b.id === originalBlock.id)
|
||||
|
||||
expect(reserializedBlock).toBeDefined()
|
||||
expect(reserializedBlock?.config.tool).toBe(originalBlock.config.tool)
|
||||
expect(reserializedBlock?.metadata?.id).toBe(originalBlock.metadata?.id)
|
||||
|
||||
// Check params - we only check a subset because some default values might be added
|
||||
Object.entries(originalBlock.config.params).forEach(([key, value]) => {
|
||||
if (value !== null) {
|
||||
expect(reserializedBlock?.config.params[key]).toEqual(value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Check connections
|
||||
expect(reserialized.connections).toEqual(serialized.connections)
|
||||
|
||||
// Check loops
|
||||
expect(reserialized.loops).toEqual(serialized.loops)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user