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:
Waleed
2025-12-04 11:41:08 -08:00
committed by GitHub
parent 6bfb643ef8
commit f44e7e34ec
3 changed files with 128 additions and 16 deletions

View File

@@ -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, {

View File

@@ -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')
})
})
})

View File

@@ -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: