fix(serializer): validate required fields for blocks without tools (#3137)

This commit is contained in:
Waleed
2026-02-04 16:47:18 -08:00
committed by GitHub
parent fce566cc2f
commit 36ec68d93e
4 changed files with 231 additions and 58 deletions

View File

@@ -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`
}

View File

@@ -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()

View File

@@ -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
@@ -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) || []
// 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
let shouldValidateParam = true
// 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) || []
if (matchingConfigs.length > 0) {
shouldValidateParam = matchingConfigs.some((subBlockConfig: any) => {
const includedByMode = shouldSerializeSubBlock(
subBlockConfig,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
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 isRequired = (() => {
if (!subBlockConfig.required) return false
if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required
return evaluateSubBlockCondition(subBlockConfig.required, params)
})()
return includedByMode && isRequired
})
const displayName = activeConfig?.title || paramId
missingFields.push(displayName)
}
}
})
}
if (!shouldValidateParam) {
return
}
// Validate required subBlocks not covered by tool params (e.g., blocks with empty tools.access)
const validatedByTool = new Set(currentTool ? Object.keys(currentTool.params || {}) : [])
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)
}
blockConfig.subBlocks?.forEach((subBlockConfig: SubBlockConfig) => {
// Skip if already validated via tool params
if (validatedByTool.has(subBlockConfig.id)) {
return
}
// Check if subBlock is visible
const isVisible = shouldSerializeSubBlock(
subBlockConfig,
allValues,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex,
canonicalModeOverrides
)
if (!isVisible) {
return
}
// 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)
}
})