Files
sim/apps/sim/app/api/workflows/[id]/execute/route.test.ts
Vikhyath Mondreti b7876ca466 improvement(copilot): structured metadata context + start block deprecation (#1362)
* progress

* progress

* deploy command update

* add trigger mode modal

* fix trigger icons'

* fix corners for add trigger card

* update serialization error visual in console

* works

* improvement(copilot-context): structured context for copilot

* forgot long description

* Update metadata params

* progress

* add better workflow ux

* progress

* highlighting works

* trigger card

* default agent workflow change

* fix build error

* remove any casts

* address greptile comments

* Diff input format

* address greptile comments

* improvement: ui/ux

* improvement: changed to vertical scrolling

* fix(workflow): ensure new blocks from sidebar click/drag use getUniqueBlockName (with semantic trigger base when applicable)

* Validation + build/edit mark complete

* fix trigger dropdown

* Copilot stuff (lots of it)

* Temp update prod dns

* fix trigger check

* fix

* fix trigger mode check

* Fix yaml imports

* Fix autolayout error

* fix deployed chat

* Fix copilot input text overflow

* fix trigger mode persistence in addBlock with enableTriggerMode flag passed in

* Lint

* Fix failing tests

* Reset ishosted

* Lint

* input format for legacy starter

* Fix executor

---------

Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
2025-09-22 23:24:50 -07:00

620 lines
18 KiB
TypeScript

import { NextRequest } from 'next/server'
/**
* Integration tests for workflow execution API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('Workflow Execution API Route', () => {
let executeMock = vi.fn().mockResolvedValue({
success: true,
output: {
response: 'Test response',
},
logs: [],
metadata: {
duration: 123,
startTime: new Date().toISOString(),
endTime: new Date().toISOString(),
},
})
beforeEach(() => {
vi.resetModules()
vi.doMock('@/app/api/workflows/middleware', () => ({
validateWorkflowAccess: vi.fn().mockResolvedValue({
workflow: {
id: 'workflow-id',
userId: 'user-id',
},
}),
}))
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
vi.doMock('@/services/queue', () => ({
RateLimiter: vi.fn().mockImplementation(() => ({
checkRateLimit: vi.fn().mockResolvedValue({
allowed: true,
remaining: 10,
resetAt: new Date(),
}),
checkRateLimitWithSubscription: vi.fn().mockResolvedValue({
allowed: true,
remaining: 10,
resetAt: new Date(),
}),
})),
RateLimitError: class RateLimitError extends Error {
constructor(
message: string,
public statusCode = 429
) {
super(message)
this.name = 'RateLimitError'
}
},
}))
vi.doMock('@/lib/billing', () => ({
checkServerSideUsageLimits: vi.fn().mockResolvedValue({
isExceeded: false,
currentUsage: 10,
limit: 100,
}),
}))
vi.doMock('@/lib/billing/core/subscription', () => ({
getHighestPrioritySubscription: vi.fn().mockResolvedValue({
plan: 'free',
referenceId: 'user-id',
}),
}))
vi.doMock('@sim/db/schema', () => ({
subscription: {
plan: 'plan',
referenceId: 'referenceId',
},
apiKey: {
userId: 'userId',
key: 'key',
},
userStats: {
userId: 'userId',
totalApiCalls: 'totalApiCalls',
lastActive: 'lastActive',
},
environment: {
userId: 'userId',
variables: 'variables',
},
}))
vi.doMock('@/lib/workflows/db-helpers', () => ({
loadDeployedWorkflowState: vi.fn().mockResolvedValue({
blocks: {
'starter-id': {
id: 'starter-id',
type: 'starter',
name: 'Start',
position: { x: 100, y: 100 },
enabled: true,
subBlocks: {},
outputs: {},
data: {},
},
'agent-id': {
id: 'agent-id',
type: 'agent',
name: 'Agent',
position: { x: 300, y: 100 },
enabled: true,
subBlocks: {},
outputs: {},
data: {},
},
},
edges: [
{
id: 'edge-1',
source: 'starter-id',
target: 'agent-id',
sourceHandle: 'source',
targetHandle: 'target',
},
],
loops: {},
parallels: {},
isFromNormalizedTables: false, // Changed to false since it's from deployed state
}),
}))
executeMock = vi.fn().mockResolvedValue({
success: true,
output: {
response: 'Test response',
},
logs: [],
metadata: {
duration: 123,
startTime: new Date().toISOString(),
endTime: new Date().toISOString(),
},
})
vi.doMock('@/executor', () => ({
Executor: vi.fn().mockImplementation(() => ({
execute: executeMock,
})),
}))
vi.doMock('@/lib/utils', () => ({
decryptSecret: vi.fn().mockResolvedValue({
decrypted: 'decrypted-secret-value',
}),
isHosted: vi.fn().mockReturnValue(false),
getRotatingApiKey: vi.fn().mockReturnValue('rotated-api-key'),
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.doMock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue(undefined),
safeComplete: vi.fn().mockResolvedValue(undefined),
safeCompleteWithError: vi.fn().mockResolvedValue(undefined),
setupExecutor: vi.fn(),
})),
}))
vi.doMock('@/lib/logs/execution/logger', () => ({
executionLogger: {
startWorkflowExecution: vi.fn().mockResolvedValue(undefined),
logBlockExecution: vi.fn().mockResolvedValue(undefined),
completeWorkflowExecution: vi.fn().mockResolvedValue(undefined),
},
}))
vi.doMock('@/lib/logs/execution/trace-spans/trace-spans', () => ({
buildTraceSpans: vi.fn().mockReturnValue({
traceSpans: [],
totalDuration: 100,
}),
}))
vi.doMock('@/lib/workflows/utils', () => ({
updateWorkflowRunCounts: vi.fn().mockResolvedValue(undefined),
workflowHasResponseBlock: vi.fn().mockReturnValue(false),
createHttpResponseFromBlock: vi.fn().mockReturnValue(new Response('OK')),
}))
vi.doMock('@/stores/workflows/server-utils', () => ({
mergeSubblockState: vi.fn().mockReturnValue({
'starter-id': {
id: 'starter-id',
type: 'starter',
subBlocks: {},
},
}),
}))
vi.doMock('@sim/db', () => {
const mockDb = {
select: vi.fn().mockImplementation((columns) => ({
from: vi.fn().mockImplementation((table) => ({
where: vi.fn().mockImplementation(() => ({
limit: vi.fn().mockImplementation(() => {
if (table === 'subscription' || columns?.plan) {
return [{ plan: 'free' }]
}
if (table === 'apiKey' || columns?.userId) {
return [{ userId: 'user-id' }]
}
return [
{
id: 'env-id',
userId: 'user-id',
variables: {
OPENAI_API_KEY: 'encrypted:key-value',
},
},
]
}),
})),
})),
})),
update: vi.fn().mockImplementation(() => ({
set: vi.fn().mockImplementation(() => ({
where: vi.fn().mockResolvedValue(undefined),
})),
})),
}
return { db: mockDb }
})
vi.doMock('@/serializer', () => ({
Serializer: vi.fn().mockImplementation(() => ({
serializeWorkflow: vi.fn().mockReturnValue({
version: '1.0',
blocks: [],
connections: [],
loops: {},
}),
})),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
/**
* Test GET execution route
* Simulates direct execution with URL-based parameters
*/
it('should execute workflow with GET request successfully', async () => {
const req = createMockRequest('GET')
const params = Promise.resolve({ id: 'workflow-id' })
const { GET } = await import('@/app/api/workflows/[id]/execute/route')
const response = await GET(req, { params })
expect(response).toBeDefined()
let data
try {
data = await response.json()
} catch (e) {
console.error('Response could not be parsed as JSON:', await response.text())
throw e
}
if (response.status === 200) {
expect(data).toHaveProperty('success', true)
expect(data).toHaveProperty('output')
expect(data.output).toHaveProperty('response')
}
const validateWorkflowAccess = (await import('@/app/api/workflows/middleware'))
.validateWorkflowAccess
expect(validateWorkflowAccess).toHaveBeenCalledWith(expect.any(Object), 'workflow-id')
const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalled()
expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
})
/**
* Test POST execution route
* Simulates execution with a JSON body containing parameters
*/
it('should execute workflow with POST request successfully', async () => {
const requestBody = {
inputs: {
message: 'Test input message',
},
}
const req = createMockRequest('POST', requestBody)
const params = Promise.resolve({ id: 'workflow-id' })
const { POST } = await import('@/app/api/workflows/[id]/execute/route')
const response = await POST(req, { params })
expect(response).toBeDefined()
let data
try {
data = await response.json()
} catch (e) {
console.error('Response could not be parsed as JSON:', await response.text())
throw e
}
if (response.status === 200) {
expect(data).toHaveProperty('success', true)
expect(data).toHaveProperty('output')
expect(data.output).toHaveProperty('response')
}
const validateWorkflowAccess = (await import('@/app/api/workflows/middleware'))
.validateWorkflowAccess
expect(validateWorkflowAccess).toHaveBeenCalledWith(expect.any(Object), 'workflow-id')
const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalled()
expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
expect(Executor).toHaveBeenCalledWith(
expect.objectContaining({
workflow: expect.any(Object), // serializedWorkflow
currentBlockStates: expect.any(Object), // processedBlockStates
envVarValues: expect.any(Object), // decryptedEnvVars
workflowInput: requestBody, // processedInput (direct input, not wrapped)
workflowVariables: expect.any(Object),
contextExtensions: expect.any(Object), // Allow any context extensions object
})
)
})
/**
* Test POST execution with structured input matching the input format
*/
it('should execute workflow with structured input matching the input format', async () => {
const structuredInput = {
firstName: 'John',
age: 30,
isActive: true,
preferences: { theme: 'dark' },
tags: ['test', 'api'],
}
const req = createMockRequest('POST', structuredInput)
const params = Promise.resolve({ id: 'workflow-id' })
const { POST } = await import('@/app/api/workflows/[id]/execute/route')
const response = await POST(req, { params })
expect(response).toBeDefined()
expect(response.status).toBe(200)
const data = await response.json()
expect(data).toHaveProperty('success', true)
const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalledWith(
expect.objectContaining({
workflow: expect.any(Object), // serializedWorkflow
currentBlockStates: expect.any(Object), // processedBlockStates
envVarValues: expect.any(Object), // decryptedEnvVars
workflowInput: structuredInput, // processedInput (direct input, not wrapped)
workflowVariables: expect.any(Object),
contextExtensions: expect.any(Object), // Allow any context extensions object
})
)
})
/**
* Test POST execution with empty request body
*/
it('should execute workflow with empty request body', async () => {
const req = createMockRequest('POST')
const params = Promise.resolve({ id: 'workflow-id' })
const { POST } = await import('@/app/api/workflows/[id]/execute/route')
const response = await POST(req, { params })
expect(response).toBeDefined()
expect(response.status).toBe(200)
const data = await response.json()
expect(data).toHaveProperty('success', true)
const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalledWith(
expect.objectContaining({
workflow: expect.any(Object), // serializedWorkflow
currentBlockStates: expect.any(Object), // processedBlockStates
envVarValues: expect.any(Object), // decryptedEnvVars
workflowInput: expect.objectContaining({}), // processedInput with empty input
workflowVariables: expect.any(Object),
contextExtensions: expect.any(Object), // Allow any context extensions object
})
)
})
/**
* Test POST execution with invalid JSON body
*/
it('should handle invalid JSON in request body', async () => {
// Create a mock request with invalid JSON text
const req = new NextRequest('https://example.com/api/workflows/workflow-id/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: 'this is not valid JSON',
})
const params = Promise.resolve({ id: 'workflow-id' })
const { POST } = await import('@/app/api/workflows/[id]/execute/route')
const response = await POST(req, { params })
expect(response.status).toBe(400)
const data = await response.json()
expect(data).toHaveProperty('error')
expect(data.error).toContain('Invalid JSON')
})
/**
* Test handling of incorrect workflow ID
*/
it('should return 403 for unauthorized workflow access', async () => {
vi.doMock('@/app/api/workflows/middleware', () => ({
validateWorkflowAccess: vi.fn().mockResolvedValue({
error: {
message: 'Unauthorized',
status: 403,
},
}),
}))
const req = createMockRequest('GET')
const params = Promise.resolve({ id: 'invalid-workflow-id' })
const { GET } = await import('@/app/api/workflows/[id]/execute/route')
const response = await GET(req, { params })
expect(response.status).toBe(403)
const data = await response.json()
expect(data).toHaveProperty('error', 'Unauthorized')
})
/**
* Test handling of execution errors
*/
it('should handle execution errors gracefully', async () => {
const mockCompleteWorkflowExecution = vi.fn().mockResolvedValue({})
vi.doMock('@/lib/logs/execution/logger', () => ({
executionLogger: {
completeWorkflowExecution: mockCompleteWorkflowExecution,
},
}))
const mockSafeCompleteWithError = vi.fn().mockResolvedValue({})
vi.doMock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue({}),
safeComplete: vi.fn().mockResolvedValue({}),
safeCompleteWithError: mockSafeCompleteWithError,
setupExecutor: vi.fn(),
})),
}))
vi.doMock('@/executor', () => ({
Executor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockRejectedValue(new Error('Execution failed')),
})),
}))
const req = createMockRequest('GET')
const params = Promise.resolve({ id: 'workflow-id' })
const { GET } = await import('@/app/api/workflows/[id]/execute/route')
const response = await GET(req, { params })
expect(response.status).toBe(500)
const data = await response.json()
expect(data).toHaveProperty('error')
expect(data.error).toContain('Execution failed')
expect(mockSafeCompleteWithError).toHaveBeenCalled()
})
/**
* Test that workflow variables are properly passed to the Executor
*/
it('should pass workflow variables to the Executor', async () => {
const workflowVariables = {
variable1: { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' },
variable2: { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' },
}
vi.doMock('@/app/api/workflows/middleware', () => ({
validateWorkflowAccess: vi.fn().mockResolvedValue({
workflow: {
id: 'workflow-with-vars-id',
userId: 'user-id',
variables: workflowVariables,
},
}),
}))
vi.doMock('@/lib/workflows/db-helpers', () => ({
loadDeployedWorkflowState: vi.fn().mockResolvedValue({
blocks: {
'starter-id': {
id: 'starter-id',
type: 'starter',
name: 'Start',
position: { x: 100, y: 100 },
enabled: true,
subBlocks: {},
outputs: {},
data: {},
},
'agent-id': {
id: 'agent-id',
type: 'agent',
name: 'Agent',
position: { x: 300, y: 100 },
enabled: true,
subBlocks: {},
outputs: {},
data: {},
},
},
edges: [
{
id: 'edge-1',
source: 'starter-id',
target: 'agent-id',
sourceHandle: 'source',
targetHandle: 'target',
},
],
loops: {},
parallels: {},
isFromNormalizedTables: false, // Changed to false since it's from deployed state
}),
}))
const executorConstructorMock = vi.fn().mockImplementation(() => ({
execute: vi.fn().mockResolvedValue({
success: true,
output: { response: 'Execution completed with variables' },
logs: [],
metadata: {
duration: 100,
startTime: new Date().toISOString(),
endTime: new Date().toISOString(),
},
}),
}))
vi.doMock('@/executor', () => ({
Executor: executorConstructorMock,
}))
const req = createMockRequest('POST', { testInput: 'value' })
const params = Promise.resolve({ id: 'workflow-with-vars-id' })
const { POST } = await import('@/app/api/workflows/[id]/execute/route')
await POST(req, { params })
expect(executorConstructorMock).toHaveBeenCalled()
const executorCalls = executorConstructorMock.mock.calls
expect(executorCalls.length).toBeGreaterThan(0)
const lastCall = executorCalls[executorCalls.length - 1]
expect(lastCall.length).toBeGreaterThanOrEqual(1)
// Check that workflowVariables are passed in the options object
expect(lastCall[0]).toEqual(
expect.objectContaining({
workflowVariables: workflowVariables,
})
)
})
})