mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-17 09:52:38 -05:00
fix(input): allow test value if no real value provided for inputs in deployed executions (#2186)
* fix(input): allow test value if no real value provided for inputs in deployed executions * ack PR comments
This commit is contained in:
@@ -250,7 +250,6 @@ export class DAGExecutor {
|
||||
const blockOutput = buildStartBlockOutput({
|
||||
resolution: startResolution,
|
||||
workflowInput: this.workflowInput,
|
||||
isDeployedExecution: this.contextExtensions?.isDeployedContext === true,
|
||||
})
|
||||
|
||||
state.setBlockState(startResolution.block.id, {
|
||||
|
||||
@@ -33,14 +33,14 @@ function createBlock(
|
||||
}
|
||||
|
||||
describe('start-block utilities', () => {
|
||||
it('buildResolutionFromBlock returns null when metadata id missing', () => {
|
||||
it.concurrent('buildResolutionFromBlock returns null when metadata id missing', () => {
|
||||
const block = createBlock('api_trigger')
|
||||
;(block.metadata as Record<string, unknown>).id = undefined
|
||||
|
||||
expect(buildResolutionFromBlock(block)).toBeNull()
|
||||
})
|
||||
|
||||
it('resolveExecutorStartBlock prefers unified start block', () => {
|
||||
it.concurrent('resolveExecutorStartBlock prefers unified start block', () => {
|
||||
const blocks = [
|
||||
createBlock('api_trigger', 'api'),
|
||||
createBlock('starter', 'starter'),
|
||||
@@ -56,7 +56,7 @@ describe('start-block utilities', () => {
|
||||
expect(resolution?.path).toBe(StartBlockPath.UNIFIED)
|
||||
})
|
||||
|
||||
it('buildStartBlockOutput normalizes unified start payload', () => {
|
||||
it.concurrent('buildStartBlockOutput normalizes unified start payload', () => {
|
||||
const block = createBlock('start_trigger', 'start')
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
@@ -67,7 +67,6 @@ describe('start-block utilities', () => {
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: { payload: 'value' },
|
||||
isDeployedExecution: true,
|
||||
})
|
||||
|
||||
expect(output.payload).toBe('value')
|
||||
@@ -75,7 +74,7 @@ describe('start-block utilities', () => {
|
||||
expect(output.conversationId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('buildStartBlockOutput uses trigger schema for API triggers', () => {
|
||||
it.concurrent('buildStartBlockOutput uses trigger schema for API triggers', () => {
|
||||
const apiBlock = createBlock('api_trigger', 'api', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
@@ -113,11 +112,108 @@ describe('start-block utilities', () => {
|
||||
},
|
||||
files,
|
||||
},
|
||||
isDeployedExecution: false,
|
||||
})
|
||||
|
||||
expect(output.name).toBe('Ada')
|
||||
expect(output.input).toEqual({ name: 'Ada', count: 5 })
|
||||
expect(output.files).toEqual(files)
|
||||
})
|
||||
|
||||
describe('inputFormat default values', () => {
|
||||
it.concurrent('uses default value when runtime does not provide the field', () => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [
|
||||
{ name: 'input', type: 'string' },
|
||||
{ name: 'customField', type: 'string', value: 'defaultValue' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: { input: 'hello' },
|
||||
})
|
||||
|
||||
expect(output.input).toBe('hello')
|
||||
expect(output.customField).toBe('defaultValue')
|
||||
})
|
||||
|
||||
it.concurrent('runtime value overrides default value', () => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: { customField: 'runtimeValue' },
|
||||
})
|
||||
|
||||
expect(output.customField).toBe('runtimeValue')
|
||||
})
|
||||
|
||||
it.concurrent('empty string from runtime overrides default value', () => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: { customField: '' },
|
||||
})
|
||||
|
||||
expect(output.customField).toBe('')
|
||||
})
|
||||
|
||||
it.concurrent('null from runtime does not override default value', () => {
|
||||
const block = createBlock('start_trigger', 'start', {
|
||||
subBlocks: {
|
||||
inputFormat: {
|
||||
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const resolution = {
|
||||
blockId: 'start',
|
||||
block,
|
||||
path: StartBlockPath.UNIFIED,
|
||||
} as const
|
||||
|
||||
const output = buildStartBlockOutput({
|
||||
resolution,
|
||||
workflowInput: { customField: null },
|
||||
})
|
||||
|
||||
expect(output.customField).toBe('defaultValue')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -176,8 +176,7 @@ interface DerivedInputResult {
|
||||
|
||||
function deriveInputFromFormat(
|
||||
inputFormat: InputFormatField[],
|
||||
workflowInput: unknown,
|
||||
isDeployedExecution: boolean
|
||||
workflowInput: unknown
|
||||
): DerivedInputResult {
|
||||
const structuredInput: Record<string, unknown> = {}
|
||||
|
||||
@@ -205,7 +204,8 @@ function deriveInputFromFormat(
|
||||
}
|
||||
}
|
||||
|
||||
if ((fieldValue === undefined || fieldValue === null) && !isDeployedExecution) {
|
||||
// Use the default value from inputFormat if the field value wasn't provided at runtime
|
||||
if (fieldValue === undefined || fieldValue === null) {
|
||||
fieldValue = field.value
|
||||
}
|
||||
|
||||
@@ -255,13 +255,28 @@ function ensureString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function buildUnifiedStartOutput(workflowInput: unknown): NormalizedBlockOutput {
|
||||
function buildUnifiedStartOutput(
|
||||
workflowInput: unknown,
|
||||
structuredInput: Record<string, unknown>,
|
||||
hasStructured: boolean
|
||||
): NormalizedBlockOutput {
|
||||
const output: NormalizedBlockOutput = {}
|
||||
|
||||
if (hasStructured) {
|
||||
for (const [key, value] of Object.entries(structuredInput)) {
|
||||
output[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlainObject(workflowInput)) {
|
||||
for (const [key, value] of Object.entries(workflowInput)) {
|
||||
if (key === 'onUploadError') continue
|
||||
output[key] = value
|
||||
// Runtime values override defaults (except undefined/null which mean "not provided")
|
||||
if (value !== undefined && value !== null) {
|
||||
output[key] = value
|
||||
} else if (!Object.hasOwn(output, key)) {
|
||||
output[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,17 +416,19 @@ function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | und
|
||||
export interface StartBlockOutputOptions {
|
||||
resolution: ExecutorStartResolution
|
||||
workflowInput: unknown
|
||||
isDeployedExecution: boolean
|
||||
}
|
||||
|
||||
export function buildStartBlockOutput(options: StartBlockOutputOptions): NormalizedBlockOutput {
|
||||
const { resolution, workflowInput, isDeployedExecution } = options
|
||||
const { resolution, workflowInput } = options
|
||||
const inputFormat = extractInputFormat(resolution.block)
|
||||
const { finalInput } = deriveInputFromFormat(inputFormat, workflowInput, isDeployedExecution)
|
||||
const { finalInput, structuredInput, hasStructured } = deriveInputFromFormat(
|
||||
inputFormat,
|
||||
workflowInput
|
||||
)
|
||||
|
||||
switch (resolution.path) {
|
||||
case StartBlockPath.UNIFIED:
|
||||
return buildUnifiedStartOutput(workflowInput)
|
||||
return buildUnifiedStartOutput(workflowInput, structuredInput, hasStructured)
|
||||
|
||||
case StartBlockPath.SPLIT_API:
|
||||
case StartBlockPath.SPLIT_INPUT:
|
||||
|
||||
Reference in New Issue
Block a user