Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/sim/executor/execution/block-executor.retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import { BlockType, EDGE } from '@/executor/constants'
import type { DAGNode } from '@/executor/dag/builder'
import { BlockExecutor } from '@/executor/execution/block-executor'
import { ExecutionState } from '@/executor/execution/state'
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { attachTrustedExecutionCost } from '@/executor/utils/errors'
import { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import { executeTool } from '@/tools'
import { getTool } from '@/tools/utils'

vi.mock('@/blocks/index', () => ({ getBlock: vi.fn() }))
vi.mock('@/tools', () => ({ executeTool: vi.fn() }))
vi.mock('@/tools/utils', () => ({ getTool: vi.fn() }))

vi.mock('@/ee/access-control/utils/permission-check', () => ({
validateBlockType: vi.fn(),
Expand Down Expand Up @@ -139,6 +146,40 @@ describe('BlockExecutor retry', () => {
expect(ctx.blockLogs[0]?.tries).toBe(2)
})

it.each([
{ retryable: false, attempts: 1 },
{ retryable: true, attempts: 3 },
{ retryable: undefined, attempts: 3 },
])(
'executes a generic tool $attempts times when retryable is $retryable',
async ({ retryable, attempts }) => {
const block = createBlock(enabled)
block.config.tool = 'synthetic_write'
vi.mocked(getTool).mockReturnValue({
id: 'synthetic_write',
name: 'Synthetic Write',
description: 'A synthetic write for retry testing',
version: '1.0',
params: {},
request: { url: 'https://example.com/write', method: 'POST' },
})
vi.mocked(executeTool).mockResolvedValue({
success: false,
error: 'Write outcome is unknown',
output: { detail: 'Response was lost' },
...(retryable !== undefined ? { retryable } : {}),
})
const state = new ExecutionState()
const ctx = createContext(state)
const executor = buildExecutor(block, new GenericBlockHandler(), state)

await expect(executor.execute(ctx, createNode(block), block)).rejects.toThrow(
'Write outcome is unknown'
)
expect(executeTool).toHaveBeenCalledTimes(attempts)
}
)

it('adds the trusted cost of failed Function tries to the successful result', async () => {
const block = createBlock(enabled)
const firstFailure = new Error('first attempt failed')
Expand Down
55 changes: 34 additions & 21 deletions apps/sim/executor/handlers/generic/generic-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@sim/testing/mocks/executor'

import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { NonRetryableExecutionError } from '@/lib/execution/non-retryable-error'
import { HarmonicBlock } from '@/blocks/blocks/harmonic'
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { getBlock } from '@/blocks/index'
Expand Down Expand Up @@ -593,30 +594,42 @@ describe('GenericBlockHandler', () => {
expect(mockExecuteTool).not.toHaveBeenCalled()
})

it('should handle tool execution errors correctly', async () => {
const inputs = { param1: 'value' }
const errorResult = {
success: false,
error: 'Custom tool failed',
output: { detail: 'error detail' },
}
mockExecuteTool.mockResolvedValue(errorResult)
it.each([undefined, true, false])(
'preserves failure details when retryable is %s',
async (retryable) => {
const inputs = { param1: 'value' }
const errorResult = {
success: false,
error: 'Custom tool failed',
output: { detail: 'error detail' },
statusCode: 503,
...(retryable !== undefined ? { retryable } : {}),
}
mockExecuteTool.mockResolvedValue(errorResult)

await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'Custom tool failed'
)
let thrown: unknown
try {
await handler.execute(mockContext, mockBlock, inputs)
} catch (error) {
thrown = error
}

// Re-execute to check error properties after catching
try {
await handler.execute(mockContext, mockBlock, inputs)
} catch (e: any) {
expect(e.toolId).toBe('some_custom_tool')
expect(e.blockName).toBe('Test Generic Block')
expect(e.output).toEqual({ detail: 'error detail' })
expect(thrown).toBeInstanceOf(Error)
expect(thrown instanceof NonRetryableExecutionError).toBe(retryable === false)
expect(thrown).toMatchObject({
message: 'Custom tool failed',
toolId: 'some_custom_tool',
toolName: 'Some Custom Tool',
blockId: 'generic-block-1',
blockName: 'Test Generic Block',
output: { detail: 'error detail' },
statusCode: 503,
timestamp: expect.any(String),
...(retryable === false ? { retryable: false } : {}),
})
expect(mockExecuteTool).toHaveBeenCalledTimes(1)
}

expect(mockExecuteTool).toHaveBeenCalledTimes(2) // Called twice now
})
)

it.concurrent('should handle tool execution errors with no specific message', async () => {
const inputs = { param1: 'value' }
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/executor/handlers/generic/generic-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isDeepStrictEqual } from 'node:util'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { isPlainRecord } from '@sim/utils/object'
import { NonRetryableExecutionError } from '@/lib/execution/non-retryable-error'
import { getBlock } from '@/blocks/index'
import { isMcpTool } from '@/executor/constants'
import type { BlockHandler, BlockNodeMetadata, ExecutionContext } from '@/executor/types'
Expand Down Expand Up @@ -344,7 +345,10 @@ export class GenericBlockHandler implements BlockHandler {
? errorDetails.join(' - ')
: `Block execution of ${tool?.name || block.config.tool} failed with no error message`

const error = new Error(errorMessage)
const error =
result.retryable === false
? new NonRetryableExecutionError(errorMessage)
: new Error(errorMessage)

Object.assign(error, {
toolId: block.config.tool,
Expand Down
Loading