Skip to content

Commit 3d5d747

Browse files
authored
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
1 parent 88668fe commit 3d5d747

File tree

7 files changed

+253
-29
lines changed

7 files changed

+253
-29
lines changed

apps/sim/executor/__test-utils__/executor-mocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export const setupExecutorCoreMocks = () => {
110110
InputResolver: vi.fn().mockImplementation(() => ({
111111
resolveInputs: vi.fn().mockReturnValue({}),
112112
resolveBlockReferences: vi.fn().mockImplementation((value) => value),
113+
resolveVariableReferences: vi.fn().mockImplementation((value) => value),
114+
resolveEnvVariables: vi.fn().mockImplementation((value) => value),
113115
})),
114116
}))
115117

apps/sim/executor/handlers/condition/condition-handler.test.ts

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ describe('ConditionBlockHandler', () => {
8585
{}
8686
) as Mocked<InputResolver>
8787

88-
// Ensure the method exists as a mock function on the instance
88+
// Ensure the methods exist as mock functions on the instance
8989
mockResolver.resolveBlockReferences = vi.fn()
90+
mockResolver.resolveVariableReferences = vi.fn()
91+
mockResolver.resolveEnvVariables = vi.fn()
9092

9193
handler = new ConditionBlockHandler(mockPathTracker, mockResolver)
9294

@@ -147,16 +149,23 @@ describe('ConditionBlockHandler', () => {
147149
selectedConditionId: 'cond1',
148150
}
149151

150-
// Mock directly in the test
152+
// Mock the full resolution pipeline
153+
mockResolver.resolveVariableReferences.mockReturnValue('context.value > 5')
151154
mockResolver.resolveBlockReferences.mockReturnValue('context.value > 5')
155+
mockResolver.resolveEnvVariables.mockReturnValue('context.value > 5')
152156

153157
const result = await handler.execute(mockBlock, inputs, mockContext)
154158

159+
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
160+
'context.value > 5',
161+
mockBlock
162+
)
155163
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
156164
'context.value > 5',
157165
mockContext,
158166
mockBlock
159167
)
168+
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5', true)
160169
expect(result).toEqual(expectedOutput)
161170
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
162171
})
@@ -180,16 +189,23 @@ describe('ConditionBlockHandler', () => {
180189
selectedConditionId: 'else1',
181190
}
182191

183-
// Mock directly in the test
192+
// Mock the full resolution pipeline
193+
mockResolver.resolveVariableReferences.mockReturnValue('context.value < 0')
184194
mockResolver.resolveBlockReferences.mockReturnValue('context.value < 0')
195+
mockResolver.resolveEnvVariables.mockReturnValue('context.value < 0')
185196

186197
const result = await handler.execute(mockBlock, inputs, mockContext)
187198

199+
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
200+
'context.value < 0',
201+
mockBlock
202+
)
188203
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
189204
'context.value < 0',
190205
mockContext,
191206
mockBlock
192207
)
208+
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0', true)
193209
expect(result).toEqual(expectedOutput)
194210
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
195211
})
@@ -209,16 +225,77 @@ describe('ConditionBlockHandler', () => {
209225
]
210226
const inputs = { conditions: JSON.stringify(conditions) }
211227

212-
// Mock directly in the test
228+
// Mock the full resolution pipeline
229+
mockResolver.resolveVariableReferences.mockReturnValue('{{source-block-1.value}} > 5')
213230
mockResolver.resolveBlockReferences.mockReturnValue('10 > 5')
231+
mockResolver.resolveEnvVariables.mockReturnValue('10 > 5')
214232

215-
const _result = await handler.execute(mockBlock, inputs, mockContext)
233+
await handler.execute(mockBlock, inputs, mockContext)
216234

235+
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
236+
'{{source-block-1.value}} > 5',
237+
mockBlock
238+
)
217239
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
218240
'{{source-block-1.value}} > 5',
219241
mockContext,
220242
mockBlock
221243
)
244+
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5', true)
245+
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
246+
})
247+
248+
it('should resolve variable references in conditions', async () => {
249+
const conditions = [
250+
{ id: 'cond1', title: 'if', value: '<variable.userName> !== null' },
251+
{ id: 'else1', title: 'else', value: '' },
252+
]
253+
const inputs = { conditions: JSON.stringify(conditions) }
254+
255+
// Mock the full resolution pipeline for variable resolution
256+
mockResolver.resolveVariableReferences.mockReturnValue('"john" !== null')
257+
mockResolver.resolveBlockReferences.mockReturnValue('"john" !== null')
258+
mockResolver.resolveEnvVariables.mockReturnValue('"john" !== null')
259+
260+
await handler.execute(mockBlock, inputs, mockContext)
261+
262+
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
263+
'<variable.userName> !== null',
264+
mockBlock
265+
)
266+
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
267+
'"john" !== null',
268+
mockContext,
269+
mockBlock
270+
)
271+
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null', true)
272+
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
273+
})
274+
275+
it('should resolve environment variables in conditions', async () => {
276+
const conditions = [
277+
{ id: 'cond1', title: 'if', value: '{{POOP}} === "hi"' },
278+
{ id: 'else1', title: 'else', value: '' },
279+
]
280+
const inputs = { conditions: JSON.stringify(conditions) }
281+
282+
// Mock the full resolution pipeline for env variable resolution
283+
mockResolver.resolveVariableReferences.mockReturnValue('{{POOP}} === "hi"')
284+
mockResolver.resolveBlockReferences.mockReturnValue('{{POOP}} === "hi"')
285+
mockResolver.resolveEnvVariables.mockReturnValue('"hi" === "hi"')
286+
287+
await handler.execute(mockBlock, inputs, mockContext)
288+
289+
expect(mockResolver.resolveVariableReferences).toHaveBeenCalledWith(
290+
'{{POOP}} === "hi"',
291+
mockBlock
292+
)
293+
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
294+
'{{POOP}} === "hi"',
295+
mockContext,
296+
mockBlock
297+
)
298+
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"', true)
222299
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
223300
})
224301

@@ -230,8 +307,8 @@ describe('ConditionBlockHandler', () => {
230307
const inputs = { conditions: JSON.stringify(conditions) }
231308

232309
const resolutionError = new Error('Could not resolve reference: invalid-ref')
233-
// Mock directly in the test
234-
mockResolver.resolveBlockReferences.mockImplementation(() => {
310+
// Mock the pipeline to throw at the variable resolution stage
311+
mockResolver.resolveVariableReferences.mockImplementation(() => {
235312
throw resolutionError
236313
})
237314

@@ -247,8 +324,12 @@ describe('ConditionBlockHandler', () => {
247324
]
248325
const inputs = { conditions: JSON.stringify(conditions) }
249326

250-
// Mock directly in the test
327+
// Mock the full resolution pipeline
328+
mockResolver.resolveVariableReferences.mockReturnValue(
329+
'context.nonExistentProperty.doSomething()'
330+
)
251331
mockResolver.resolveBlockReferences.mockReturnValue('context.nonExistentProperty.doSomething()')
332+
mockResolver.resolveEnvVariables.mockReturnValue('context.nonExistentProperty.doSomething()')
252333

253334
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
254335
/^Evaluation error in condition "if": Cannot read properties of undefined \(reading 'doSomething'\)\. \(Resolved: context\.nonExistentProperty\.doSomething\(\)\)$/
@@ -271,8 +352,10 @@ describe('ConditionBlockHandler', () => {
271352

272353
mockContext.workflow!.blocks = [mockSourceBlock, mockBlock, mockTargetBlock2]
273354

274-
// Mock directly in the test
355+
// Mock the full resolution pipeline
356+
mockResolver.resolveVariableReferences.mockReturnValue('true')
275357
mockResolver.resolveBlockReferences.mockReturnValue('true')
358+
mockResolver.resolveEnvVariables.mockReturnValue('true')
276359

277360
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
278361
`Target block ${mockTargetBlock1.id} not found`
@@ -295,10 +378,16 @@ describe('ConditionBlockHandler', () => {
295378
},
296379
]
297380

298-
// Mock directly in the test
381+
// Mock the full resolution pipeline
382+
mockResolver.resolveVariableReferences
383+
.mockReturnValueOnce('false')
384+
.mockReturnValueOnce('context.value === 99')
299385
mockResolver.resolveBlockReferences
300386
.mockReturnValueOnce('false')
301387
.mockReturnValueOnce('context.value === 99')
388+
mockResolver.resolveEnvVariables
389+
.mockReturnValueOnce('false')
390+
.mockReturnValueOnce('context.value === 99')
302391

303392
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
304393
`No matching path found for condition block "${mockBlock.metadata?.name}", and no 'else' block exists.`
@@ -314,8 +403,10 @@ describe('ConditionBlockHandler', () => {
314403

315404
mockContext.loopItems.set(mockBlock.id, { item: 'apple' })
316405

317-
// Mock directly in the test
406+
// Mock the full resolution pipeline
407+
mockResolver.resolveVariableReferences.mockReturnValue('context.item === "apple"')
318408
mockResolver.resolveBlockReferences.mockReturnValue('context.item === "apple"')
409+
mockResolver.resolveEnvVariables.mockReturnValue('context.item === "apple"')
319410

320411
const result = await handler.execute(mockBlock, inputs, mockContext)
321412

apps/sim/executor/handlers/condition/condition-handler.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,10 @@ export class ConditionBlockHandler implements BlockHandler {
105105
// 2. Resolve references WITHIN the specific condition's value string
106106
let resolvedConditionValue = condition.value
107107
try {
108-
// Use the resolver instance to process block references within the condition string
109-
resolvedConditionValue = this.resolver.resolveBlockReferences(
110-
condition.value,
111-
context,
112-
block // Pass the current condition block as context
113-
)
108+
// Use full resolution pipeline: variables -> block references -> env vars
109+
const resolvedVars = this.resolver.resolveVariableReferences(condition.value, block)
110+
const resolvedRefs = this.resolver.resolveBlockReferences(resolvedVars, context, block)
111+
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs, true)
114112
logger.info(
115113
`Resolved condition "${condition.title}" (${condition.id}): from "${condition.value}" to "${resolvedConditionValue}"`
116114
)

apps/sim/executor/resolver/resolver.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1446,7 +1446,130 @@ describe('InputResolver', () => {
14461446
}
14471447

14481448
const result = connectionResolver.resolveInputs(testBlock, contextWithConnections)
1449-
expect(result.code).toBe('return Hello World') // Should not be quoted for function blocks
1449+
expect(result.code).toBe('return "Hello World"') // Should be quoted for function blocks
1450+
})
1451+
1452+
it('should format start.input properly for different block types', () => {
1453+
// Test function block - should quote strings
1454+
const functionBlock: SerializedBlock = {
1455+
id: 'test-function',
1456+
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
1457+
position: { x: 100, y: 100 },
1458+
config: {
1459+
tool: BlockType.FUNCTION,
1460+
params: {
1461+
code: 'return <start.input>',
1462+
},
1463+
},
1464+
inputs: {},
1465+
outputs: {},
1466+
enabled: true,
1467+
}
1468+
1469+
// Test condition block - should quote strings
1470+
const conditionBlock: SerializedBlock = {
1471+
id: 'test-condition',
1472+
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
1473+
position: { x: 200, y: 100 },
1474+
config: {
1475+
tool: BlockType.CONDITION,
1476+
params: {
1477+
conditions: JSON.stringify([
1478+
{ id: 'cond1', title: 'if', value: '<start.input> === "Hello World"' },
1479+
]),
1480+
},
1481+
},
1482+
inputs: {},
1483+
outputs: {},
1484+
enabled: true,
1485+
}
1486+
1487+
// Test response block - should use raw string
1488+
const responseBlock: SerializedBlock = {
1489+
id: 'test-response',
1490+
metadata: { id: BlockType.RESPONSE, name: 'Test Response' },
1491+
position: { x: 300, y: 100 },
1492+
config: {
1493+
tool: BlockType.RESPONSE,
1494+
params: {
1495+
content: '<start.input>',
1496+
},
1497+
},
1498+
inputs: {},
1499+
outputs: {},
1500+
enabled: true,
1501+
}
1502+
1503+
const functionResult = connectionResolver.resolveInputs(functionBlock, contextWithConnections)
1504+
expect(functionResult.code).toBe('return "Hello World"') // Quoted for function
1505+
1506+
const conditionResult = connectionResolver.resolveInputs(
1507+
conditionBlock,
1508+
contextWithConnections
1509+
)
1510+
expect(conditionResult.conditions).toBe(
1511+
'[{"id":"cond1","title":"if","value":"<start.input> === \\"Hello World\\""}]'
1512+
) // Conditions not resolved at input level
1513+
1514+
const responseResult = connectionResolver.resolveInputs(responseBlock, contextWithConnections)
1515+
expect(responseResult.content).toBe('Hello World') // Raw string for response
1516+
})
1517+
1518+
it('should properly format start.input when resolved directly via resolveBlockReferences', () => {
1519+
// Test that start.input gets proper formatting for different block types
1520+
const functionBlock: SerializedBlock = {
1521+
id: 'test-function',
1522+
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
1523+
position: { x: 100, y: 100 },
1524+
config: { tool: BlockType.FUNCTION, params: {} },
1525+
inputs: {},
1526+
outputs: {},
1527+
enabled: true,
1528+
}
1529+
1530+
const conditionBlock: SerializedBlock = {
1531+
id: 'test-condition',
1532+
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
1533+
position: { x: 200, y: 100 },
1534+
config: { tool: BlockType.CONDITION, params: {} },
1535+
inputs: {},
1536+
outputs: {},
1537+
enabled: true,
1538+
}
1539+
1540+
// Test function block - should quote strings
1541+
const functionResult = connectionResolver.resolveBlockReferences(
1542+
'return <start.input>',
1543+
contextWithConnections,
1544+
functionBlock
1545+
)
1546+
expect(functionResult).toBe('return "Hello World"')
1547+
1548+
// Test condition block - should quote strings
1549+
const conditionResult = connectionResolver.resolveBlockReferences(
1550+
'<start.input> === "test"',
1551+
contextWithConnections,
1552+
conditionBlock
1553+
)
1554+
expect(conditionResult).toBe('"Hello World" === "test"')
1555+
1556+
// Test other block types - should use raw string
1557+
const otherBlock: SerializedBlock = {
1558+
id: 'test-other',
1559+
metadata: { id: 'other', name: 'Other Block' },
1560+
position: { x: 300, y: 100 },
1561+
config: { tool: 'other', params: {} },
1562+
inputs: {},
1563+
outputs: {},
1564+
enabled: true,
1565+
}
1566+
1567+
const otherResult = connectionResolver.resolveBlockReferences(
1568+
'content: <start.input>',
1569+
contextWithConnections,
1570+
otherBlock
1571+
)
1572+
expect(otherResult).toBe('content: Hello World')
14501573
})
14511574

14521575
it('should provide helpful error messages for unconnected blocks', () => {

apps/sim/executor/resolver/resolver.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,18 @@ export class InputResolver {
449449
formattedValue = JSON.stringify(replacementValue)
450450
}
451451
} else {
452-
// For primitive values
453-
formattedValue = String(replacementValue)
452+
// For primitive values, format based on target block type
453+
if (blockType === 'function') {
454+
formattedValue = this.formatValueForCodeContext(
455+
replacementValue,
456+
currentBlock,
457+
isInTemplateLiteral
458+
)
459+
} else if (blockType === 'condition') {
460+
formattedValue = this.stringifyForCondition(replacementValue)
461+
} else {
462+
formattedValue = String(replacementValue)
463+
}
454464
}
455465
} else {
456466
// Standard handling for non-input references

0 commit comments

Comments
 (0)