improvement(serializer): canonical subblock, serialization cleanups, schedules/webhooks are deployment version friendly (#2848)

* hide form deployment tab from docs

* progress

* fix resolution

* cleanup code

* fix positioning

* cleanup dead sockets adv mode ops

* address greptile comments

* fix tests plus more simplification

* fix cleanup

* bring back advanced mode with specific definition

* revert feature flags

* improvement(subblock): ui

* resolver change to make all var references optional chaining

* fix(webhooks/schedules): deployment version friendly

* fix tests

* fix credential sets with new lifecycle

* prep merge

* add back migration

* fix display check for adv fields

* fix trigger vs block scoping

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
Vikhyath Mondreti
2026-01-16 15:23:43 -08:00
committed by GitHub
parent ce3ddb6ba0
commit 78e4ca9d45
70 changed files with 12806 additions and 1011 deletions

View File

@@ -515,103 +515,131 @@ describe('Serializer', () => {
})
})
/**
* Advanced mode field filtering tests
*/
describe('advanced mode field filtering', () => {
it.concurrent('should include all fields when block is in advanced mode', () => {
describe('canonical mode field selection', () => {
it.concurrent('should use advanced value when canonicalModes specifies advanced', () => {
const serializer = new Serializer()
const advancedModeBlock: any = {
const block: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: true, // Advanced mode enabled
data: {
canonicalModes: { channel: 'advanced' },
},
subBlocks: {
channel: { value: 'general' }, // basic mode field
manualChannel: { value: 'C1234567890' }, // advanced mode field
text: { value: 'Hello world' }, // both mode field
username: { value: 'bot' }, // both mode field
operation: { value: 'send' },
destinationType: { value: 'channel' },
channel: { value: 'general' },
manualChannel: { value: 'C1234567890' },
text: { value: 'Hello world' },
username: { value: 'bot' },
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': advancedModeBlock }, [], {})
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('general')
expect(slackBlock?.config.params.manualChannel).toBe('C1234567890')
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('C1234567890')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
expect(slackBlock?.config.params.text).toBe('Hello world')
expect(slackBlock?.config.params.username).toBe('bot')
})
it.concurrent('should exclude advanced-only fields when block is in basic mode', () => {
it.concurrent('should use basic value when canonicalModes specifies basic', () => {
const serializer = new Serializer()
const basicModeBlock: any = {
const block: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: false, // Basic mode enabled
data: {
canonicalModes: { channel: 'basic' },
},
subBlocks: {
channel: { value: 'general' }, // basic mode field
manualChannel: { value: 'C1234567890' }, // advanced mode field
text: { value: 'Hello world' }, // both mode field
username: { value: 'bot' }, // both mode field
operation: { value: 'send' },
destinationType: { value: 'channel' },
channel: { value: 'general' },
manualChannel: { value: 'C1234567890' },
text: { value: 'Hello world' },
username: { value: 'bot' },
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': basicModeBlock }, [], {})
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('general')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
expect(slackBlock?.config.params.text).toBe('Hello world')
expect(slackBlock?.config.params.username).toBe('bot')
})
it.concurrent(
'should exclude advanced-only fields when advancedMode is undefined (defaults to basic mode)',
() => {
const serializer = new Serializer()
it.concurrent('should fall back to legacy advancedMode when canonicalModes not set', () => {
const serializer = new Serializer()
const defaultModeBlock: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
subBlocks: {
channel: { value: 'general' },
manualChannel: { value: 'C1234567890' },
text: { value: 'Hello world' },
username: { value: 'bot' },
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': defaultModeBlock }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('general')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
expect(slackBlock?.config.params.text).toBe('Hello world')
expect(slackBlock?.config.params.username).toBe('bot')
const block: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: true,
subBlocks: {
operation: { value: 'send' },
destinationType: { value: 'channel' },
channel: { value: 'general' },
manualChannel: { value: 'C1234567890' },
text: { value: 'Hello world' },
username: { value: 'bot' },
},
outputs: {},
enabled: true,
}
)
it.concurrent('should filter memories field correctly in agent blocks', () => {
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('C1234567890')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
})
it.concurrent('should use basic value by default when no mode specified', () => {
const serializer = new Serializer()
const block: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
subBlocks: {
operation: { value: 'send' },
destinationType: { value: 'channel' },
channel: { value: 'general' },
manualChannel: { value: 'C1234567890' },
text: { value: 'Hello world' },
username: { value: 'bot' },
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
expect(slackBlock?.config.params.channel).toBe('general')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
})
it.concurrent('should preserve advanced-only values when present in basic mode', () => {
const serializer = new Serializer()
const agentInBasicMode: any = {
@@ -637,7 +665,9 @@ describe('Serializer', () => {
expect(agentBlock?.config.params.systemPrompt).toBe('You are helpful')
expect(agentBlock?.config.params.userPrompt).toBe('Hello')
expect(agentBlock?.config.params.memories).toBeUndefined()
expect(agentBlock?.config.params.memories).toEqual([
{ role: 'user', content: 'My name is John' },
])
expect(agentBlock?.config.params.model).toBe('claude-3-sonnet')
})

View File

@@ -1,9 +1,17 @@
import { createLogger } from '@sim/logger'
import type { Edge } from 'reactflow'
import { parseResponseFormatSafely } from '@/lib/core/utils/response-format'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import {
buildCanonicalIndex,
buildSubBlockValues,
evaluateSubBlockCondition,
getCanonicalValues,
isNonEmptyValue,
isSubBlockFeatureEnabled,
} from '@/lib/workflows/subblocks/visibility'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { REFERENCE } from '@/executor/constants'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
@@ -27,67 +35,37 @@ export class WorkflowValidationError extends Error {
}
/**
* Helper function to check if a subblock should be included in serialization based on current mode
* Helper function to check if a subblock should be serialized.
*/
function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: boolean): boolean {
const fieldMode = subBlockConfig.mode
function shouldSerializeSubBlock(
subBlockConfig: SubBlockConfig,
values: Record<string, unknown>,
displayAdvancedOptions: boolean,
isTriggerContext: boolean,
isTriggerCategory: boolean,
canonicalIndex: ReturnType<typeof buildCanonicalIndex>
): boolean {
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
if (fieldMode === 'advanced' && !isAdvancedMode) {
return false // Skip advanced-only fields when in basic mode
if (subBlockConfig.mode === 'trigger') {
if (!isTriggerContext && !isTriggerCategory) return false
} else if (isTriggerContext && !isTriggerCategory) {
return false
}
return true
}
const isCanonicalMember = Boolean(canonicalIndex.canonicalIdBySubBlockId[subBlockConfig.id])
if (isCanonicalMember) {
return evaluateSubBlockCondition(subBlockConfig.condition, values)
}
/**
* Evaluates a condition object against current field values.
* Used to determine if a conditionally-visible field should be included in params.
*/
function evaluateCondition(
condition:
| {
field: string
value: any
not?: boolean
and?: { field: string; value: any; not?: boolean }
}
| (() => {
field: string
value: any
not?: boolean
and?: { field: string; value: any; not?: boolean }
})
| undefined,
values: Record<string, any>
): boolean {
if (!condition) return true
if (subBlockConfig.mode === 'advanced' && !displayAdvancedOptions) {
return isNonEmptyValue(values[subBlockConfig.id])
}
if (subBlockConfig.mode === 'basic' && displayAdvancedOptions) {
return false
}
const actual = typeof condition === 'function' ? condition() : condition
const fieldValue = values[actual.field]
const valueMatch = Array.isArray(actual.value)
? fieldValue != null &&
(actual.not ? !actual.value.includes(fieldValue) : actual.value.includes(fieldValue))
: actual.not
? fieldValue !== actual.value
: fieldValue === actual.value
const andMatch = !actual.and
? true
: (() => {
const andFieldValue = values[actual.and!.field]
const andValueMatch = Array.isArray(actual.and!.value)
? andFieldValue != null &&
(actual.and!.not
? !actual.and!.value.includes(andFieldValue)
: actual.and!.value.includes(andFieldValue))
: actual.and!.not
? andFieldValue !== actual.and!.value
: andFieldValue === actual.and!.value
return andValueMatch
})()
return valueMatch && andMatch
return evaluateSubBlockCondition(subBlockConfig.condition, values)
}
/**
@@ -241,16 +219,12 @@ export class Serializer {
// Extract parameters from UI state
const params = this.extractParams(block)
try {
const isTriggerCategory = blockConfig.category === 'triggers'
if (block.triggerMode === true || isTriggerCategory) {
params.triggerMode = true
}
if (block.advancedMode === true) {
params.advancedMode = true
}
} catch (_) {
// no-op: conservative, avoid blocking serialization if blockConfig is unexpected
const isTriggerCategory = blockConfig.category === 'triggers'
if (block.triggerMode === true || isTriggerCategory) {
params.triggerMode = true
}
if (block.advancedMode === true) {
params.advancedMode = true
}
// Validate required fields that only users can provide (before execution starts)
@@ -271,16 +245,7 @@ export class Serializer {
// For non-custom tools, we determine the tool ID
const nonCustomTools = tools.filter((tool: any) => tool.type !== 'custom-tool')
if (nonCustomTools.length > 0) {
try {
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during serialization, using default:', {
error: error instanceof Error ? error.message : String(error),
})
toolId = blockConfig.tools.access[0]
}
toolId = this.selectToolId(blockConfig, params)
}
} catch (error) {
logger.error('Error processing tools in agent block:', { error })
@@ -289,16 +254,7 @@ export class Serializer {
}
} else {
// For non-agent blocks, get tool ID from block config as usual
try {
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during serialization, using default:', {
error: error instanceof Error ? error.message : String(error),
})
toolId = blockConfig.tools.access[0]
}
toolId = this.selectToolId(blockConfig, params)
}
// Get inputs from block config
@@ -322,7 +278,10 @@ export class Serializer {
// Include response format fields if available
...(params.responseFormat
? {
responseFormat: this.parseResponseFormatSafely(params.responseFormat),
responseFormat:
parseResponseFormatSafely(params.responseFormat, block.id, {
allowReferences: true,
}) ?? undefined,
}
: {}),
},
@@ -337,52 +296,9 @@ export class Serializer {
}
}
private parseResponseFormatSafely(responseFormat: any): any {
if (!responseFormat) {
return undefined
}
// If already an object, return as-is
if (typeof responseFormat === 'object' && responseFormat !== null) {
return responseFormat
}
// Handle string values
if (typeof responseFormat === 'string') {
const trimmedValue = responseFormat.trim()
// Check for variable references like <start.input>
if (trimmedValue.startsWith(REFERENCE.START) && trimmedValue.includes(REFERENCE.END)) {
// Keep variable references as-is
return trimmedValue
}
if (trimmedValue === '') {
return undefined
}
// Try to parse as JSON
try {
return JSON.parse(trimmedValue)
} catch (error) {
// If parsing fails, return undefined to avoid crashes
// This allows the workflow to continue without structured response format
logger.warn('Failed to parse response format as JSON in serializer, using undefined:', {
value: trimmedValue,
error: error instanceof Error ? error.message : String(error),
})
return undefined
}
}
// For any other type, return undefined
return undefined
}
private extractParams(block: BlockState): Record<string, any> {
// Special handling for subflow blocks (loops, parallels, etc.)
if (block.type === 'loop' || block.type === 'parallel') {
return {} // Loop and parallel blocks don't have traditional params
return {}
}
const blockConfig = getBlock(block.type)
@@ -391,43 +307,42 @@ export class Serializer {
}
const params: Record<string, any> = {}
const isAdvancedMode = block.advancedMode ?? false
const legacyAdvancedMode = block.advancedMode ?? false
const canonicalModeOverrides = block.data?.canonicalModes
const isStarterBlock = block.type === 'starter'
const isAgentBlock = block.type === 'agent'
const isTriggerContext = block.triggerMode ?? false
const isTriggerCategory = blockConfig.category === 'triggers'
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
const allValues = buildSubBlockValues(block.subBlocks)
// First pass: collect ALL raw values for condition evaluation
const allValues: Record<string, any> = {}
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
allValues[id] = subBlock.value
})
// Second pass: filter by mode and conditions
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
const matchingConfigs = blockConfig.subBlocks.filter((config) => config.id === id)
// Include field if it matches current mode OR if it's the starter inputFormat with values
const hasStarterInputFormatValues =
isStarterBlock &&
id === 'inputFormat' &&
Array.isArray(subBlock.value) &&
subBlock.value.length > 0
// Include legacy agent block fields (systemPrompt, userPrompt, memories) even if not in current config
// This ensures backward compatibility with old workflows that were exported before the messages array migration
const isLegacyAgentField =
isAgentBlock && ['systemPrompt', 'userPrompt', 'memories'].includes(id)
const anyConditionMet =
matchingConfigs.length === 0
? true
: matchingConfigs.some(
(config) =>
shouldIncludeField(config, isAdvancedMode) &&
evaluateCondition(config.condition, allValues)
)
const shouldInclude =
matchingConfigs.length === 0 ||
matchingConfigs.some((config) =>
shouldSerializeSubBlock(
config,
allValues,
legacyAdvancedMode,
isTriggerContext,
isTriggerCategory,
canonicalIndex
)
)
if (
(matchingConfigs.length > 0 && anyConditionMet) ||
(matchingConfigs.length > 0 && shouldInclude) ||
hasStarterInputFormatValues ||
isLegacyAgentField
) {
@@ -435,56 +350,38 @@ export class Serializer {
}
})
// Then check for any subBlocks with default values
blockConfig.subBlocks.forEach((subBlockConfig) => {
const id = subBlockConfig.id
if (
(params[id] === null || params[id] === undefined) &&
params[id] == null &&
subBlockConfig.value &&
shouldIncludeField(subBlockConfig, isAdvancedMode)
shouldSerializeSubBlock(
subBlockConfig,
allValues,
legacyAdvancedMode,
isTriggerContext,
isTriggerCategory,
canonicalIndex
)
) {
// If the value is absent and there's a default value function, use it
params[id] = subBlockConfig.value(params)
}
})
// Finally, consolidate canonical parameters (e.g., selector and manual ID into a single param)
const canonicalGroups: Record<string, { basic?: string; advanced: string[] }> = {}
blockConfig.subBlocks.forEach((sb) => {
if (!sb.canonicalParamId) return
const key = sb.canonicalParamId
if (!canonicalGroups[key]) canonicalGroups[key] = { basic: undefined, advanced: [] }
if (sb.mode === 'advanced') canonicalGroups[key].advanced.push(sb.id)
else canonicalGroups[key].basic = sb.id
})
Object.values(canonicalIndex.groupsById).forEach((group) => {
const { basicValue, advancedValue } = getCanonicalValues(group, params)
const pairMode =
canonicalModeOverrides?.[group.canonicalId] ?? (legacyAdvancedMode ? 'advanced' : 'basic')
const chosen = pairMode === 'advanced' ? advancedValue : basicValue
Object.entries(canonicalGroups).forEach(([canonicalKey, group]) => {
const basicId = group.basic
const advancedIds = group.advanced
const basicVal = basicId ? params[basicId] : undefined
const advancedVal = advancedIds
.map((id) => params[id])
.find(
(v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0)
)
let chosen: any
if (advancedVal !== undefined && basicVal !== undefined) {
chosen = isAdvancedMode ? advancedVal : basicVal
} else if (advancedVal !== undefined) {
chosen = advancedVal
} else if (basicVal !== undefined) {
chosen = isAdvancedMode ? undefined : basicVal
} else {
chosen = undefined
}
const sourceIds = [basicId, ...advancedIds].filter(Boolean) as string[]
const sourceIds = [group.basicId, ...group.advancedIds].filter(Boolean) as string[]
sourceIds.forEach((id) => {
if (id !== canonicalKey) delete params[id]
if (id !== group.canonicalId) delete params[id]
})
if (chosen !== undefined) params[canonicalKey] = chosen
else delete params[canonicalKey]
if (chosen !== undefined) {
params[group.canonicalId] = chosen
}
})
return params
@@ -520,17 +417,7 @@ export class Serializer {
}
// Determine the current tool ID using the same logic as the serializer
let currentToolId = ''
try {
currentToolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during validation, using default:', {
error: error instanceof Error ? error.message : String(error),
})
currentToolId = blockConfig.tools.access[0]
}
const currentToolId = this.selectToolId(blockConfig, params)
// Get the specific tool to validate against
const currentTool = getTool(currentToolId)
@@ -538,8 +425,11 @@ export class Serializer {
return // Tool not found, skip validation
}
// Check required user-only parameters for the current tool
const missingFields: string[] = []
const displayAdvancedOptions = block.advancedMode ?? false
const isTriggerContext = block.triggerMode ?? false
const isTriggerCategory = blockConfig.category === 'triggers'
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks || [])
// Iterate through the tool's parameters, not the block's subBlocks
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
@@ -549,20 +439,23 @@ export class Serializer {
let shouldValidateParam = true
if (matchingConfigs.length > 0) {
const isAdvancedMode = block.advancedMode ?? false
shouldValidateParam = matchingConfigs.some((subBlockConfig: any) => {
const includedByMode = shouldIncludeField(subBlockConfig, isAdvancedMode)
const includedByCondition = evaluateCondition(subBlockConfig.condition, params)
const includedByMode = shouldSerializeSubBlock(
subBlockConfig,
params,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex
)
const isRequired = (() => {
if (!subBlockConfig.required) return false
if (typeof subBlockConfig.required === 'boolean') return subBlockConfig.required
return evaluateCondition(subBlockConfig.required, params)
return evaluateSubBlockCondition(subBlockConfig.required, params)
})()
return includedByMode && includedByCondition && isRequired
return includedByMode && isRequired
})
}
@@ -572,10 +465,15 @@ export class Serializer {
const fieldValue = params[paramId]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
const activeConfig = matchingConfigs.find(
(config: any) =>
shouldIncludeField(config, block.advancedMode ?? false) &&
evaluateCondition(config.condition, params)
const activeConfig = matchingConfigs.find((config: any) =>
shouldSerializeSubBlock(
config,
params,
displayAdvancedOptions,
isTriggerContext,
isTriggerCategory,
canonicalIndex
)
)
const displayName = activeConfig?.title || paramId
missingFields.push(displayName)
@@ -629,6 +527,19 @@ export class Serializer {
return accessibleMap
}
private selectToolId(blockConfig: any, params: Record<string, any>): string {
try {
return blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during serialization, using default:', {
error: error instanceof Error ? error.message : String(error),
})
return blockConfig.tools.access[0]
}
}
deserializeWorkflow(workflow: SerializedWorkflow): {
blocks: Record<string, BlockState>
edges: Edge[]

View File

@@ -147,20 +147,19 @@ const { mockBlockConfigs, createMockGetBlock, slackWithCanonicalParam } = vi.hoi
config: { tool: () => 'slack_send_message' },
},
subBlocks: [
{ id: 'channel', type: 'dropdown', label: 'Channel', mode: 'basic' },
{
id: 'channel',
type: 'dropdown',
label: 'Channel',
mode: 'basic',
canonicalParamId: 'channel',
},
{
id: 'manualChannel',
type: 'short-input',
label: 'Channel ID',
mode: 'advanced',
canonicalParamId: 'targetChannel',
},
{
id: 'channelSelector',
type: 'dropdown',
label: 'Channel Selector',
mode: 'basic',
canonicalParamId: 'targetChannel',
canonicalParamId: 'channel',
},
{ id: 'text', type: 'long-input', label: 'Message' },
{ id: 'username', type: 'short-input', label: 'Username', mode: 'both' },
@@ -656,16 +655,18 @@ describe('Serializer Extended Tests', () => {
})
describe('canonical parameter handling', () => {
it('should consolidate basic/advanced mode fields into canonical param in advanced mode', () => {
it('should use advanced value when canonicalModes specifies advanced', () => {
const serializer = new Serializer()
const block: BlockState = {
id: 'slack-1',
type: 'slack',
name: 'Slack',
position: { x: 0, y: 0 },
advancedMode: true,
data: {
canonicalModes: { channel: 'advanced' },
},
subBlocks: {
channelSelector: { id: 'channelSelector', type: 'dropdown', value: 'general' },
channel: { id: 'channel', type: 'channel-selector', value: 'general' },
manualChannel: { id: 'manualChannel', type: 'short-input', value: 'C12345' },
text: { id: 'text', type: 'long-input', value: 'Hello' },
},
@@ -676,22 +677,23 @@ describe('Serializer Extended Tests', () => {
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock?.config.params.targetChannel).toBe('C12345')
expect(slackBlock?.config.params.channelSelector).toBeUndefined()
expect(slackBlock?.config.params.channel).toBe('C12345')
expect(slackBlock?.config.params.manualChannel).toBeUndefined()
})
it('should consolidate to basic value when in basic mode', () => {
it('should use basic value when canonicalModes specifies basic', () => {
const serializer = new Serializer()
const block: BlockState = {
id: 'slack-1',
type: 'slack',
name: 'Slack',
position: { x: 0, y: 0 },
advancedMode: false,
data: {
canonicalModes: { channel: 'basic' },
},
subBlocks: {
channelSelector: { id: 'channelSelector', type: 'dropdown', value: 'general' },
manualChannel: { id: 'manualChannel', type: 'short-input', value: '' },
channel: { id: 'channel', type: 'channel-selector', value: 'general' },
manualChannel: { id: 'manualChannel', type: 'short-input', value: 'C12345' },
text: { id: 'text', type: 'long-input', value: 'Hello' },
},
outputs: {},
@@ -701,7 +703,7 @@ describe('Serializer Extended Tests', () => {
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock?.config.params.targetChannel).toBe('general')
expect(slackBlock?.config.params.channel).toBe('general')
})
it('should handle missing canonical param values', () => {
@@ -711,9 +713,8 @@ describe('Serializer Extended Tests', () => {
type: 'slack',
name: 'Slack',
position: { x: 0, y: 0 },
advancedMode: false,
subBlocks: {
channelSelector: { id: 'channelSelector', type: 'dropdown', value: null },
channel: { id: 'channel', type: 'channel-selector', value: null },
manualChannel: { id: 'manualChannel', type: 'short-input', value: null },
text: { id: 'text', type: 'long-input', value: 'Hello' },
},
@@ -724,8 +725,7 @@ describe('Serializer Extended Tests', () => {
const serialized = serializer.serializeWorkflow({ 'slack-1': block }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
// When both values are null, the canonical param is set to null (preserving the null value)
expect(slackBlock?.config.params.targetChannel).toBeNull()
expect(slackBlock?.config.params.channel).toBeNull()
})
})