mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix: prevent raw workflowInput from overwriting coerced start block values (#3347)
buildUnifiedStartOutput and buildIntegrationTriggerOutput first populate output with schema-coerced structuredInput values (via coerceValue), then iterate workflowInput and unconditionally overwrite those keys with raw strings. This causes typed values (arrays, objects, numbers, booleans) passed to child workflows to arrive as stringified versions. Add a structuredKeys guard so the workflowInput loop skips keys already set by the coerced structuredInput, letting coerceValue's type-aware parsing (JSON.parse for objects/arrays, Number() for numbers, etc.) take effect. Fixes #3105 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5ae0115444
commit
dde098e8e5
@@ -215,5 +215,115 @@ describe('start-block utilities', () => {
|
||||
|
||||
expect(output.customField).toBe('defaultValue')
|
||||
})
|
||||
|
||||
it.concurrent('preserves coerced types for unified start payload', () => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [
|
||||
{ name: 'conversation_id', type: 'number' },
|
||||
{ name: 'sender', type: 'object' },
|
||||
{ name: 'is_active', type: 'boolean' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: {
|
||||
conversation_id: '149',
|
||||
sender: '{"id":10,"email":"user@example.com"}',
|
||||
is_active: 'true',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output.conversation_id).toBe(149)
|
||||
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
|
||||
expect(output.is_active).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'prefers coerced inputFormat values over duplicated top-level workflowInput keys',
|
||||
() => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [
|
||||
{ name: 'conversation_id', type: 'number' },
|
||||
{ name: 'sender', type: 'object' },
|
||||
{ name: 'is_active', type: 'boolean' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: {
|
||||
input: {
|
||||
conversation_id: '149',
|
||||
sender: '{"id":10,"email":"user@example.com"}',
|
||||
is_active: 'false',
|
||||
},
|
||||
conversation_id: '150',
|
||||
sender: '{"id":99,"email":"wrong@example.com"}',
|
||||
is_active: 'true',
|
||||
extra: 'keep-me',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output.conversation_id).toBe(149)
|
||||
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
|
||||
expect(output.is_active).toBe(false)
|
||||
expect(output.extra).toBe('keep-me')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('EXTERNAL_TRIGGER path', () => {
|
||||
it.concurrent('preserves coerced types for integration trigger payload', () => {
|
||||
const block = createBlock('webhook', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [
|
||||
{ name: 'count', type: 'number' },
|
||||
{ name: 'payload', type: 'object' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.EXTERNAL_TRIGGER,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: {
|
||||
count: '5',
|
||||
payload: '{"event":"push"}',
|
||||
extra: 'untouched',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output.count).toBe(5)
|
||||
expect(output.payload).toEqual({ event: 'push' })
|
||||
expect(output.extra).toBe('untouched')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -262,6 +262,7 @@ function buildUnifiedStartOutput(
|
||||
hasStructured: boolean
|
||||
): NormalizedBlockOutput {
|
||||
const output: NormalizedBlockOutput = {}
|
||||
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
|
||||
|
||||
if (hasStructured) {
|
||||
for (const [key, value] of Object.entries(structuredInput)) {
|
||||
@@ -272,6 +273,9 @@ function buildUnifiedStartOutput(
|
||||
if (isPlainObject(workflowInput)) {
|
||||
for (const [key, value] of Object.entries(workflowInput)) {
|
||||
if (key === 'onUploadError') continue
|
||||
// Skip keys already set by schema-coerced structuredInput to
|
||||
// prevent raw workflowInput strings from overwriting typed values.
|
||||
if (structuredKeys?.has(key)) continue
|
||||
// Runtime values override defaults (except undefined/null which mean "not provided")
|
||||
if (value !== undefined && value !== null) {
|
||||
output[key] = value
|
||||
@@ -384,6 +388,7 @@ function buildIntegrationTriggerOutput(
|
||||
hasStructured: boolean
|
||||
): NormalizedBlockOutput {
|
||||
const output: NormalizedBlockOutput = {}
|
||||
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
|
||||
|
||||
if (hasStructured) {
|
||||
for (const [key, value] of Object.entries(structuredInput)) {
|
||||
@@ -393,6 +398,7 @@ function buildIntegrationTriggerOutput(
|
||||
|
||||
if (isPlainObject(workflowInput)) {
|
||||
for (const [key, value] of Object.entries(workflowInput)) {
|
||||
if (structuredKeys?.has(key)) continue
|
||||
if (value !== undefined && value !== null) {
|
||||
output[key] = value
|
||||
} else if (!Object.hasOwn(output, key)) {
|
||||
|
||||
Reference in New Issue
Block a user