Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/sim/lib/core/utils/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ export function formatDuration(
const precision = options?.precision ?? 0

if (ms < 1) {
// Zero or near-zero: show "0ms" instead of "0.00ms"
if (ms === 0 || ms < 0.005) {
return '0ms'
}
// Sub-millisecond: show with 2 decimal places
return `${ms.toFixed(2)}ms`
}
Expand Down
102 changes: 102 additions & 0 deletions apps/sim/serializer/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,108 @@ describe('Serializer', () => {
}).not.toThrow()
})

it.concurrent(
'should validate required fields for blocks without tools (empty tools.access)',
() => {
const serializer = new Serializer()

const waitBlockMissingRequired: any = {
id: 'wait-block',
type: 'wait',
name: 'Wait Block',
position: { x: 0, y: 0 },
subBlocks: {
timeValue: { value: '' },
timeUnit: { value: 'seconds' },
},
outputs: {},
enabled: true,
}

expect(() => {
serializer.serializeWorkflow(
{ 'wait-block': waitBlockMissingRequired },
[],
{},
undefined,
true
)
}).toThrow('Wait Block is missing required fields: Wait Amount')
}
)

it.concurrent(
'should pass validation for blocks without tools when required fields are present',
() => {
const serializer = new Serializer()

const waitBlockWithFields: any = {
id: 'wait-block',
type: 'wait',
name: 'Wait Block',
position: { x: 0, y: 0 },
subBlocks: {
timeValue: { value: '10' },
timeUnit: { value: 'seconds' },
},
outputs: {},
enabled: true,
}

expect(() => {
serializer.serializeWorkflow(
{ 'wait-block': waitBlockWithFields },
[],
{},
undefined,
true
)
}).not.toThrow()
}
)

it.concurrent('should report all missing required fields for blocks without tools', () => {
const serializer = new Serializer()

const waitBlockAllMissing: any = {
id: 'wait-block',
type: 'wait',
name: 'Wait Block',
position: { x: 0, y: 0 },
subBlocks: {
timeValue: { value: null },
timeUnit: { value: '' },
},
outputs: {},
enabled: true,
}

expect(() => {
serializer.serializeWorkflow({ 'wait-block': waitBlockAllMissing }, [], {}, undefined, true)
}).toThrow('Wait Block is missing required fields: Wait Amount, Unit')
})

it.concurrent('should skip validation for disabled blocks without tools', () => {
const serializer = new Serializer()

const disabledWaitBlock: any = {
id: 'wait-block',
type: 'wait',
name: 'Wait Block',
position: { x: 0, y: 0 },
subBlocks: {
timeValue: { value: null },
timeUnit: { value: null },
},
outputs: {},
enabled: false,
}

expect(() => {
serializer.serializeWorkflow({ 'wait-block': disabledWaitBlock }, [], {}, undefined, true)
}).not.toThrow()
})

it.concurrent('should handle empty string values as missing', () => {
const serializer = new Serializer()

Expand Down
153 changes: 94 additions & 59 deletions apps/sim/serializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,21 +416,6 @@ export class Serializer {
return
}

// Get the tool configuration to check parameter visibility
const toolAccess = blockConfig.tools?.access
if (!toolAccess || toolAccess.length === 0) {
return // No tools to validate against
}

// Determine the current tool ID using the same logic as the serializer
const currentToolId = this.selectToolId(blockConfig, params)

// Get the specific tool to validate against
const currentTool = getTool(currentToolId)
if (!currentTool) {
return // Tool not found, skip validation
}

const missingFields: string[] = []
const displayAdvancedOptions = block.advancedMode ?? false
const isTriggerContext = block.triggerMode ?? false
Expand All @@ -439,55 +424,105 @@ export class Serializer {
const canonicalModeOverrides = block.data?.canonicalModes
const allValues = buildSubBlockValues(block.subBlocks)

// Iterate through the tool's parameters, not the block's subBlocks
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
if (paramConfig.required && paramConfig.visibility === 'user-only') {
const matchingConfigs = blockConfig.subBlocks?.filter((sb: any) => sb.id === paramId) || []

let shouldValidateParam = true

if (matchingConfigs.length > 0) {
shouldValidateParam = matchingConfigs.some((subBlockConfig: any) => {
const includedByMode = shouldSerializeSubBlock(
subBlockConfig,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
// Get the tool configuration to check parameter visibility
const toolAccess = blockConfig.tools?.access
const currentToolId = toolAccess?.length > 0 ? this.selectToolId(blockConfig, params) : null
const currentTool = currentToolId ? getTool(currentToolId) : null

// Validate tool parameters (for blocks with tools)
if (currentTool) {
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
if (paramConfig.required && paramConfig.visibility === 'user-only') {
const matchingConfigs =
blockConfig.subBlocks?.filter((sb: any) => sb.id === paramId) || []

let shouldValidateParam = true

if (matchingConfigs.length > 0) {
shouldValidateParam = matchingConfigs.some((subBlockConfig: any) => {
const includedByMode = shouldSerializeSubBlock(
subBlockConfig,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
)

const isRequired = (() => {
if (!subBlockConfig.required) return false
if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required
return evaluateSubBlockCondition(subBlockConfig.required, params)
})()

return includedByMode && isRequired
})
}

if (!shouldValidateParam) {
return
}

const fieldValue = params[paramId]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
const activeConfig = matchingConfigs.find((config: any) =>
shouldSerializeSubBlock(
config,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
)
)
const displayName = activeConfig?.title || paramId
missingFields.push(displayName)
}
}
})
}

const isRequired = (() => {
if (!subBlockConfig.required) return false
if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required
return evaluateSubBlockCondition(subBlockConfig.required, params)
})()
// Validate required subBlocks not covered by tool params (e.g., blocks with empty tools.access)
const validatedByTool = new Set(currentTool ? Object.keys(currentTool.params || {}) : [])

return includedByMode && isRequired
})
}
blockConfig.subBlocks?.forEach((subBlockConfig: SubBlockConfig) => {
// Skip if already validated via tool params
if (validatedByTool.has(subBlockConfig.id)) {
return
}

if (!shouldValidateParam) {
return
}
// Check if subBlock is visible
const isVisible = shouldSerializeSubBlock(
subBlockConfig,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
)

if (!isVisible) {
return
}

const fieldValue = params[paramId]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
const activeConfig = matchingConfigs.find((config: any) =>
shouldSerializeSubBlock(
config,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
)
)
const displayName = activeConfig?.title || paramId
missingFields.push(displayName)
}
// Check if subBlock is required
const isRequired = (() => {
if (!subBlockConfig.required) return false
if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required
return evaluateSubBlockCondition(subBlockConfig.required, params)
})()

if (!isRequired) {
return
}

// Check if value is missing
const fieldValue = params[subBlockConfig.id]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
missingFields.push(subBlockConfig.title || subBlockConfig.id)
}
})

Expand Down
32 changes: 32 additions & 0 deletions packages/testing/src/mocks/blocks.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,38 @@ export const mockBlockConfigs: Record<string, any> = {
],
inputs: {},
},
wait: {
name: 'Wait',
description: 'Pause workflow execution for a specified time delay',
category: 'blocks',
bgColor: '#F59E0B',
tools: {
access: [],
},
subBlocks: [
{
id: 'timeValue',
title: 'Wait Amount',
type: 'short-input',
placeholder: '10',
required: true,
},
{
id: 'timeUnit',
title: 'Unit',
type: 'dropdown',
required: true,
},
],
inputs: {
timeValue: { type: 'string' },
timeUnit: { type: 'string' },
},
outputs: {
waitDuration: { type: 'number' },
status: { type: 'string' },
},
},
}

/**
Expand Down