Files
simstudioai--sim/apps/sim/executor/handlers/function/function-handler.test.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

245 lines
7.9 KiB
TypeScript

import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
import { BlockType } from '@/executor/constants'
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
import type { ExecutionContext } from '@/executor/types'
import {
FUNCTION_BLOCK_CONTEXT_VARS_KEY,
FUNCTION_BLOCK_DISPLAY_CODE_KEY,
} from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'
vi.mock('@/tools', () => ({
executeTool: vi.fn(),
}))
const mockExecuteTool = executeTool as Mock
describe('FunctionBlockHandler', () => {
let handler: FunctionBlockHandler
let mockBlock: SerializedBlock
let mockContext: ExecutionContext
beforeEach(() => {
handler = new FunctionBlockHandler()
mockBlock = {
id: 'func-block-1',
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
position: { x: 30, y: 30 },
config: { tool: BlockType.FUNCTION, params: {} },
inputs: { code: 'string', timeout: 'number' }, // Using ParamType strings
outputs: {},
enabled: true,
}
mockContext = {
workflowId: 'test-workflow-id',
blockStates: new Map(),
blockLogs: [],
metadata: { duration: 0 },
environmentVariables: {},
decisions: { router: new Map(), condition: new Map() },
loopExecutions: new Map(),
executedBlocks: new Set(),
activeExecutionPath: new Set(),
completedLoops: new Set(),
}
// Reset mocks using vi
vi.clearAllMocks()
// Default mock implementation for executeTool
mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'Success' } })
})
it('should handle function blocks', () => {
expect(handler.canHandle(mockBlock)).toBe(true)
const nonFuncBlock: SerializedBlock = { ...mockBlock, metadata: { id: 'other' } }
expect(handler.canHandle(nonFuncBlock)).toBe(false)
})
it('should execute function block with string code', async () => {
const inputs = {
code: 'console.log("Hello"); return 1 + 1;',
timeout: 10000,
envVars: {},
isCustomTool: false,
workflowId: undefined,
}
const expectedToolParams = {
code: inputs.code,
language: 'javascript',
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
executionId: mockContext.executionId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}
const expectedOutput: any = { result: 'Success' }
const result = await handler.execute(mockContext, mockBlock, inputs)
expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams, {
executionContext: mockContext,
})
expect(result).toEqual(expectedOutput)
})
it('should execute function block with array code', async () => {
const inputs = {
code: [{ content: 'const x = 5;' }, { content: 'return x * 2;' }],
timeout: 5000,
envVars: {},
isCustomTool: false,
workflowId: undefined,
}
const expectedCode = 'const x = 5;\nreturn x * 2;'
const expectedToolParams = {
code: expectedCode,
language: 'javascript',
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
executionId: mockContext.executionId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}
const expectedOutput: any = { result: 'Success' }
const result = await handler.execute(mockContext, mockBlock, inputs)
expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams, {
executionContext: mockContext,
})
expect(result).toEqual(expectedOutput)
})
it('should use default timeout if not provided', async () => {
const inputs = { code: 'return true;' }
const expectedToolParams = {
code: inputs.code,
language: 'javascript',
timeout: DEFAULT_EXECUTION_TIMEOUT_MS,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
executionId: mockContext.executionId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}
await handler.execute(mockContext, mockBlock, inputs)
expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams, {
executionContext: mockContext,
})
})
it('should handle execution errors from the tool', async () => {
const inputs = { code: 'throw new Error("Code failed");' }
const errorResult = { success: false, error: 'Function execution failed: Code failed' }
mockExecuteTool.mockResolvedValue(errorResult)
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'Function execution failed: Code failed'
)
expect(mockExecuteTool).toHaveBeenCalled()
})
it('should pass runtime context variables to function_execute', async () => {
const contextVariables = { __blockRef_0: { result: 'from-block' } }
await handler.execute(mockContext, mockBlock, {
code: 'return globalThis["__blockRef_0"]',
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables,
})
expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
contextVariables,
}),
{ executionContext: mockContext }
)
})
it('should pass display-resolved function code for error display', async () => {
mockBlock.config.params = { code: 'retur <start.reqerror>' }
await handler.execute(mockContext, mockBlock, {
code: 'retur globalThis["__blockRef_0"]',
[FUNCTION_BLOCK_DISPLAY_CODE_KEY]: 'retur "value"',
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: { __blockRef_0: 'value' },
})
expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
code: 'retur globalThis["__blockRef_0"]',
sourceCode: 'retur "value"',
}),
{ executionContext: mockContext }
)
})
it('should normalize malformed execution context records before calling function_execute', async () => {
const legacyVariable = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' }
mockContext.workflowVariables = [legacyVariable] as unknown as Record<string, any>
mockContext.environmentVariables = ['invalid-env'] as unknown as Record<string, string>
await handler.execute(mockContext, mockBlock, {
code: 'return "myfitness"',
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: ['invalid-context'],
})
expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
envVars: {},
workflowVariables: { 'var-1': legacyVariable },
contextVariables: {},
}),
{ executionContext: mockContext }
)
})
it('should handle tool error with no specific message', async () => {
const inputs = { code: 'some code' }
const errorResult = { success: false }
mockExecuteTool.mockResolvedValue(errorResult)
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
'Function execution failed'
)
})
})