fix(condition): fixed condition block to resolve envvars and vars (#718)

* fix(condition): fixed condition block to resolve envvars and vars

* upgrade turbo

* fixed starter block input not resolving as string
This commit is contained in:
Waleed Latif
2025-07-17 17:57:23 -07:00
committed by GitHub
parent 88668fed84
commit 3d5d7474ed
7 changed files with 253 additions and 29 deletions

View File

@@ -110,6 +110,8 @@ export const setupExecutorCoreMocks = () => {
InputResolver: vi.fn().mockImplementation(() => ({
resolveInputs: vi.fn().mockReturnValue({}),
resolveBlockReferences: vi.fn().mockImplementation((value) => value),
resolveVariableReferences: vi.fn().mockImplementation((value) => value),
resolveEnvVariables: vi.fn().mockImplementation((value) => value),
})),
}))

View File

@@ -85,8 +85,10 @@ describe('ConditionBlockHandler', () => {
{}
) as Mocked<InputResolver>
// Ensure the method exists as a mock function on the instance
// Ensure the methods exist as mock functions on the instance
mockResolver.resolveBlockReferences = vi.fn()
mockResolver.resolveVariableReferences = vi.fn()
mockResolver.resolveEnvVariables = vi.fn()
handler = new ConditionBlockHandler(mockPathTracker, mockResolver)
@@ -147,16 +149,23 @@ describe('ConditionBlockHandler', () => {
selectedConditionId: 'cond1',
}
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue('context.value > 5')
mockResolver.resolveBlockReferences.mockReturnValue('context.value > 5')
mockResolver.resolveEnvVariables.mockReturnValue('context.value > 5')
const result = await handler.execute(mockBlock, inputs, mockContext)
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
'context.value > 5',
mockBlock
)
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
'context.value > 5',
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5', true)
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
@@ -180,16 +189,23 @@ describe('ConditionBlockHandler', () => {
selectedConditionId: 'else1',
}
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue('context.value < 0')
mockResolver.resolveBlockReferences.mockReturnValue('context.value < 0')
mockResolver.resolveEnvVariables.mockReturnValue('context.value < 0')
const result = await handler.execute(mockBlock, inputs, mockContext)
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
'context.value < 0',
mockBlock
)
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
'context.value < 0',
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0', true)
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
})
@@ -209,16 +225,77 @@ describe('ConditionBlockHandler', () => {
]
const inputs = { conditions: JSON.stringify(conditions) }
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue('{{source-block-1.value}} > 5')
mockResolver.resolveBlockReferences.mockReturnValue('10 > 5')
mockResolver.resolveEnvVariables.mockReturnValue('10 > 5')
const _result = await handler.execute(mockBlock, inputs, mockContext)
await handler.execute(mockBlock, inputs, mockContext)
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
'{{source-block-1.value}} > 5',
mockBlock
)
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
'{{source-block-1.value}} > 5',
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5', true)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
it('should resolve variable references in conditions', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: '<variable.userName> !== null' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
// Mock the full resolution pipeline for variable resolution
mockResolver.resolveVariableReferences.mockReturnValue('"john" !== null')
mockResolver.resolveBlockReferences.mockReturnValue('"john" !== null')
mockResolver.resolveEnvVariables.mockReturnValue('"john" !== null')
await handler.execute(mockBlock, inputs, mockContext)
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
'<variable.userName> !== null',
mockBlock
)
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
'"john" !== null',
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null', true)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
it('should resolve environment variables in conditions', async () => {
const conditions = [
{ id: 'cond1', title: 'if', value: '{{POOP}} === "hi"' },
{ id: 'else1', title: 'else', value: '' },
]
const inputs = { conditions: JSON.stringify(conditions) }
// Mock the full resolution pipeline for env variable resolution
mockResolver.resolveVariableReferences.mockReturnValue('{{POOP}} === "hi"')
mockResolver.resolveBlockReferences.mockReturnValue('{{POOP}} === "hi"')
mockResolver.resolveEnvVariables.mockReturnValue('"hi" === "hi"')
await handler.execute(mockBlock, inputs, mockContext)
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
'{{POOP}} === "hi"',
mockBlock
)
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
'{{POOP}} === "hi"',
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"', true)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
@@ -230,8 +307,8 @@ describe('ConditionBlockHandler', () => {
const inputs = { conditions: JSON.stringify(conditions) }
const resolutionError = new Error('Could not resolve reference: invalid-ref')
// Mock directly in the test
mockResolver.resolveBlockReferences.mockImplementation(() => {
// Mock the pipeline to throw at the variable resolution stage
mockResolver.resolveVariableReferences.mockImplementation(() => {
throw resolutionError
})
@@ -247,8 +324,12 @@ describe('ConditionBlockHandler', () => {
]
const inputs = { conditions: JSON.stringify(conditions) }
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue(
'context.nonExistentProperty.doSomething()'
)
mockResolver.resolveBlockReferences.mockReturnValue('context.nonExistentProperty.doSomething()')
mockResolver.resolveEnvVariables.mockReturnValue('context.nonExistentProperty.doSomething()')
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
/^Evaluation error in condition "if": Cannot read properties of undefined \(reading 'doSomething'\)\. \(Resolved: context\.nonExistentProperty\.doSomething\(\)\)$/
@@ -271,8 +352,10 @@ describe('ConditionBlockHandler', () => {
mockContext.workflow!.blocks = [mockSourceBlock, mockBlock, mockTargetBlock2]
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue('true')
mockResolver.resolveBlockReferences.mockReturnValue('true')
mockResolver.resolveEnvVariables.mockReturnValue('true')
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
`Target block ${mockTargetBlock1.id} not found`
@@ -295,10 +378,16 @@ describe('ConditionBlockHandler', () => {
},
]
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences
.mockReturnValueOnce('false')
.mockReturnValueOnce('context.value === 99')
mockResolver.resolveBlockReferences
.mockReturnValueOnce('false')
.mockReturnValueOnce('context.value === 99')
mockResolver.resolveEnvVariables
.mockReturnValueOnce('false')
.mockReturnValueOnce('context.value === 99')
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
`No matching path found for condition block "${mockBlock.metadata?.name}", and no 'else' block exists.`
@@ -314,8 +403,10 @@ describe('ConditionBlockHandler', () => {
mockContext.loopItems.set(mockBlock.id, { item: 'apple' })
// Mock directly in the test
// Mock the full resolution pipeline
mockResolver.resolveVariableReferences.mockReturnValue('context.item === "apple"')
mockResolver.resolveBlockReferences.mockReturnValue('context.item === "apple"')
mockResolver.resolveEnvVariables.mockReturnValue('context.item === "apple"')
const result = await handler.execute(mockBlock, inputs, mockContext)

View File

@@ -105,12 +105,10 @@ export class ConditionBlockHandler implements BlockHandler {
// 2. Resolve references WITHIN the specific condition's value string
let resolvedConditionValue = condition.value
try {
// Use the resolver instance to process block references within the condition string
resolvedConditionValue = this.resolver.resolveBlockReferences(
condition.value,
context,
block // Pass the current condition block as context
)
// Use full resolution pipeline: variables -> block references -> env vars
const resolvedVars = this.resolver.resolveVariableReferences(condition.value, block)
const resolvedRefs = this.resolver.resolveBlockReferences(resolvedVars, context, block)
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs, true)
logger.info(
`Resolved condition "${condition.title}" (${condition.id}): from "${condition.value}" to "${resolvedConditionValue}"`
)

View File

@@ -1446,7 +1446,130 @@ describe('InputResolver', () => {
}
const result = connectionResolver.resolveInputs(testBlock, contextWithConnections)
expect(result.code).toBe('return Hello World') // Should not be quoted for function blocks
expect(result.code).toBe('return "Hello World"') // Should be quoted for function blocks
})
it('should format start.input properly for different block types', () => {
// Test function block - should quote strings
const functionBlock: SerializedBlock = {
id: 'test-function',
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
position: { x: 100, y: 100 },
config: {
tool: BlockType.FUNCTION,
params: {
code: 'return <start.input>',
},
},
inputs: {},
outputs: {},
enabled: true,
}
// Test condition block - should quote strings
const conditionBlock: SerializedBlock = {
id: 'test-condition',
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
position: { x: 200, y: 100 },
config: {
tool: BlockType.CONDITION,
params: {
conditions: JSON.stringify([
{ id: 'cond1', title: 'if', value: '<start.input> === "Hello World"' },
]),
},
},
inputs: {},
outputs: {},
enabled: true,
}
// Test response block - should use raw string
const responseBlock: SerializedBlock = {
id: 'test-response',
metadata: { id: BlockType.RESPONSE, name: 'Test Response' },
position: { x: 300, y: 100 },
config: {
tool: BlockType.RESPONSE,
params: {
content: '<start.input>',
},
},
inputs: {},
outputs: {},
enabled: true,
}
const functionResult = connectionResolver.resolveInputs(functionBlock, contextWithConnections)
expect(functionResult.code).toBe('return "Hello World"') // Quoted for function
const conditionResult = connectionResolver.resolveInputs(
conditionBlock,
contextWithConnections
)
expect(conditionResult.conditions).toBe(
'[{"id":"cond1","title":"if","value":"<start.input> === \\"Hello World\\""}]'
) // Conditions not resolved at input level
const responseResult = connectionResolver.resolveInputs(responseBlock, contextWithConnections)
expect(responseResult.content).toBe('Hello World') // Raw string for response
})
it('should properly format start.input when resolved directly via resolveBlockReferences', () => {
// Test that start.input gets proper formatting for different block types
const functionBlock: SerializedBlock = {
id: 'test-function',
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
position: { x: 100, y: 100 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: {},
outputs: {},
enabled: true,
}
const conditionBlock: SerializedBlock = {
id: 'test-condition',
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
position: { x: 200, y: 100 },
config: { tool: BlockType.CONDITION, params: {} },
inputs: {},
outputs: {},
enabled: true,
}
// Test function block - should quote strings
const functionResult = connectionResolver.resolveBlockReferences(
'return <start.input>',
contextWithConnections,
functionBlock
)
expect(functionResult).toBe('return "Hello World"')
// Test condition block - should quote strings
const conditionResult = connectionResolver.resolveBlockReferences(
'<start.input> === "test"',
contextWithConnections,
conditionBlock
)
expect(conditionResult).toBe('"Hello World" === "test"')
// Test other block types - should use raw string
const otherBlock: SerializedBlock = {
id: 'test-other',
metadata: { id: 'other', name: 'Other Block' },
position: { x: 300, y: 100 },
config: { tool: 'other', params: {} },
inputs: {},
outputs: {},
enabled: true,
}
const otherResult = connectionResolver.resolveBlockReferences(
'content: <start.input>',
contextWithConnections,
otherBlock
)
expect(otherResult).toBe('content: Hello World')
})
it('should provide helpful error messages for unconnected blocks', () => {

View File

@@ -449,8 +449,18 @@ export class InputResolver {
formattedValue = JSON.stringify(replacementValue)
}
} else {
// For primitive values
formattedValue = String(replacementValue)
// For primitive values, format based on target block type
if (blockType === 'function') {
formattedValue = this.formatValueForCodeContext(
replacementValue,
currentBlock,
isInTemplateLiteral
)
} else if (blockType === 'condition') {
formattedValue = this.stringifyForCondition(replacementValue)
} else {
formattedValue = String(replacementValue)
}
}
} else {
// Standard handling for non-input references

View File

@@ -20,7 +20,7 @@
"lint-staged": "16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.4",
"turbo": "2.5.5",
},
},
"apps/docs": {
@@ -2953,19 +2953,19 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"turbo": ["turbo@2.5.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.4", "turbo-darwin-arm64": "2.5.4", "turbo-linux-64": "2.5.4", "turbo-linux-arm64": "2.5.4", "turbo-windows-64": "2.5.4", "turbo-windows-arm64": "2.5.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA=="],
"turbo": ["turbo@2.5.5", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.5", "turbo-darwin-arm64": "2.5.5", "turbo-linux-64": "2.5.5", "turbo-linux-arm64": "2.5.5", "turbo-windows-64": "2.5.5", "turbo-windows-arm64": "2.5.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-eZ7wI6KjtT1eBqCnh2JPXWNUAxtoxxfi6VdBdZFvil0ychCOTxbm7YLRBi1JSt7U3c+u3CLxpoPxLdvr/Npr3A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Tk+ZeSNdBobZiMw9aFypQt0DlLsWSFWu1ymqsAdJLuPoAH05qCfYtRxE1pJuYHcJB5pqI+/HOxtJoQ40726Btw=="],
"turbo-linux-64": ["turbo-linux-64@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA=="],
"turbo-linux-64": ["turbo-linux-64@2.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-2/XvMGykD7VgsvWesZZYIIVXMlgBcQy+ZAryjugoTcvJv8TZzSU/B1nShcA7IAjZ0q7OsZ45uP2cOb8EgKT30w=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-DW+8CjCjybu0d7TFm9dovTTVg1VRnlkZ1rceO4zqsaLrit3DgHnN4to4uwyuf9s2V/BwS3IYcRy+HG9BL596Iw=="],
"turbo-windows-64": ["turbo-windows-64@2.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA=="],
"turbo-windows-64": ["turbo-windows-64@2.5.5", "", { "os": "win32", "cpu": "x64" }, "sha512-q5p1BOy8ChtSZfULuF1BhFMYIx6bevXu4fJ+TE/hyNfyHJIfjl90Z6jWdqAlyaFLmn99X/uw+7d6T/Y/dr5JwQ=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-AXbF1KmpHUq3PKQwddMGoKMYhHsy5t1YBQO8HZ04HLMR0rWv9adYlQ8kaeQJTko1Ay1anOBFTqaxfVOOsu7+1Q=="],
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],

View File

@@ -44,7 +44,7 @@
"lint-staged": "16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"turbo": "2.5.4"
"turbo": "2.5.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [