diff --git a/apps/sim/executor/__test-utils__/executor-mocks.ts b/apps/sim/executor/__test-utils__/executor-mocks.ts index 6a058981be..5c834d6706 100644 --- a/apps/sim/executor/__test-utils__/executor-mocks.ts +++ b/apps/sim/executor/__test-utils__/executor-mocks.ts @@ -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), })), })) diff --git a/apps/sim/executor/handlers/condition/condition-handler.test.ts b/apps/sim/executor/handlers/condition/condition-handler.test.ts index 226900cec2..5873bc74a5 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.test.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.test.ts @@ -85,8 +85,10 @@ describe('ConditionBlockHandler', () => { {} ) as Mocked - // 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: ' !== 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( + ' !== 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) diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index be4a4c841a..57667c25f7 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -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}"` ) diff --git a/apps/sim/executor/resolver/resolver.test.ts b/apps/sim/executor/resolver/resolver.test.ts index 8e0fd2756f..81b34e9bd9 100644 --- a/apps/sim/executor/resolver/resolver.test.ts +++ b/apps/sim/executor/resolver/resolver.test.ts @@ -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 ', + }, + }, + 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: ' === "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: '', + }, + }, + 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":" === \\"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 ', + contextWithConnections, + functionBlock + ) + expect(functionResult).toBe('return "Hello World"') + + // Test condition block - should quote strings + const conditionResult = connectionResolver.resolveBlockReferences( + ' === "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: ', + contextWithConnections, + otherBlock + ) + expect(otherResult).toBe('content: Hello World') }) it('should provide helpful error messages for unconnected blocks', () => { diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index dcca941daf..0e4d926148 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -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 diff --git a/bun.lock b/bun.lock index c6bbe7a282..2f6f46f7c9 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 9ec930a097..3aadf2e426 100644 --- a/package.json +++ b/package.json @@ -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}": [