fix(import): fixed trigger save on export/import flow (#2239)

* fix(import): fixed trigger save on export/import flow

* optimized test runners

* ack PR comments
This commit is contained in:
Waleed
2025-12-07 23:00:28 -08:00
committed by GitHub
parent 434d12956e
commit d09fd6cf92
71 changed files with 877 additions and 1010 deletions

View File

@@ -5,9 +5,15 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('OAuth Utils', () => {
const mockSession = { user: { id: 'test-user-id' } }
const mockDb = {
const mockSession = { user: { id: 'test-user-id' } }
const mockGetSession = vi.fn()
vi.mock('@/lib/auth', () => ({
getSession: () => mockGetSession(),
}))
vi.mock('@sim/db', () => ({
db: {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
@@ -15,33 +21,41 @@ describe('OAuth Utils', () => {
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
}
const mockRefreshOAuthToken = vi.fn()
const mockLogger = {
},
}))
vi.mock('@/lib/oauth/oauth', () => ({
refreshOAuthToken: vi.fn(),
}))
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
}),
}))
import { db } from '@sim/db'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshOAuthToken } from '@/lib/oauth/oauth'
import {
getCredential,
getUserId,
refreshAccessTokenIfNeeded,
refreshTokenIfNeeded,
} from '@/app/api/auth/oauth/utils'
const mockDb = db as any
const mockRefreshOAuthToken = refreshOAuthToken as any
const mockLogger = (createLogger as any)()
describe('OAuth Utils', () => {
beforeEach(() => {
vi.resetModules()
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(mockSession),
}))
vi.doMock('@sim/db', () => ({
db: mockDb,
}))
vi.doMock('@/lib/oauth/oauth', () => ({
refreshOAuthToken: mockRefreshOAuthToken,
}))
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.clearAllMocks()
mockGetSession.mockResolvedValue(mockSession)
mockDb.limit.mockReturnValue([])
})
afterEach(() => {
@@ -50,8 +64,6 @@ describe('OAuth Utils', () => {
describe('getUserId', () => {
it('should get user ID from session when no workflowId is provided', async () => {
const { getUserId } = await import('@/app/api/auth/oauth/utils')
const userId = await getUserId('request-id')
expect(userId).toBe('test-user-id')
@@ -60,8 +72,6 @@ describe('OAuth Utils', () => {
it('should get user ID from workflow when workflowId is provided', async () => {
mockDb.limit.mockReturnValueOnce([{ userId: 'workflow-owner-id' }])
const { getUserId } = await import('@/app/api/auth/oauth/utils')
const userId = await getUserId('request-id', 'workflow-id')
expect(mockDb.select).toHaveBeenCalled()
@@ -72,11 +82,7 @@ describe('OAuth Utils', () => {
})
it('should return undefined if no session is found', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
const { getUserId } = await import('@/app/api/auth/oauth/utils')
mockGetSession.mockResolvedValueOnce(null)
const userId = await getUserId('request-id')
@@ -87,8 +93,6 @@ describe('OAuth Utils', () => {
it('should return undefined if workflow is not found', async () => {
mockDb.limit.mockReturnValueOnce([])
const { getUserId } = await import('@/app/api/auth/oauth/utils')
const userId = await getUserId('request-id', 'nonexistent-workflow-id')
expect(userId).toBeUndefined()
@@ -101,8 +105,6 @@ describe('OAuth Utils', () => {
const mockCredential = { id: 'credential-id', userId: 'test-user-id' }
mockDb.limit.mockReturnValueOnce([mockCredential])
const { getCredential } = await import('@/app/api/auth/oauth/utils')
const credential = await getCredential('request-id', 'credential-id', 'test-user-id')
expect(mockDb.select).toHaveBeenCalled()
@@ -116,8 +118,6 @@ describe('OAuth Utils', () => {
it('should return undefined when credential is not found', async () => {
mockDb.limit.mockReturnValueOnce([])
const { getCredential } = await import('@/app/api/auth/oauth/utils')
const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id')
expect(credential).toBeUndefined()
@@ -135,8 +135,6 @@ describe('OAuth Utils', () => {
providerId: 'google',
}
const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
@@ -159,8 +157,6 @@ describe('OAuth Utils', () => {
refreshToken: 'new-refresh-token',
})
const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
@@ -183,8 +179,6 @@ describe('OAuth Utils', () => {
mockRefreshOAuthToken.mockResolvedValueOnce(null)
const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
await expect(
refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
).rejects.toThrow('Failed to refresh token')
@@ -201,8 +195,6 @@ describe('OAuth Utils', () => {
providerId: 'google',
}
const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
@@ -222,8 +214,6 @@ describe('OAuth Utils', () => {
}
mockDb.limit.mockReturnValueOnce([mockCredential])
const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
@@ -247,8 +237,6 @@ describe('OAuth Utils', () => {
refreshToken: 'new-refresh-token',
})
const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
@@ -260,8 +248,6 @@ describe('OAuth Utils', () => {
it('should return null if credential not found', async () => {
mockDb.limit.mockReturnValueOnce([])
const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
expect(token).toBeNull()
@@ -281,8 +267,6 @@ describe('OAuth Utils', () => {
mockRefreshOAuthToken.mockResolvedValueOnce(null)
const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils')
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
expect(token).toBeNull()

View File

@@ -1,47 +1,60 @@
import { NextRequest } from 'next/server'
/**
* Tests for function execution API route
*
* @vitest-environment node
*/
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
const mockCreateContext = vi.fn()
const mockRunInContext = vi.fn()
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}
const mockScript = vi.fn()
const mockExecuteInE2B = vi.fn()
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
})),
}))
vi.mock('vm', () => ({
createContext: vi.fn(),
Script: vi.fn(),
}))
vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
}))
import { createContext, Script } from 'vm'
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { executeInE2B } from '@/lib/execution/e2b'
import { createLogger } from '@/lib/logs/console/logger'
import { POST } from './route'
const mockedCreateContext = vi.mocked(createContext)
const mockedScript = vi.mocked(Script)
const mockedExecuteInE2B = vi.mocked(executeInE2B)
const mockedCreateLogger = vi.mocked(createLogger)
describe('Function Execute API Route', () => {
beforeEach(() => {
vi.resetModules()
vi.resetAllMocks()
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: vi.fn().mockImplementation(() => ({
runInContext: mockRunInContext,
})),
}))
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn().mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
}),
}))
vi.clearAllMocks()
mockedCreateContext.mockReturnValue({})
mockRunInContext.mockResolvedValue('vm success')
mockCreateContext.mockReturnValue({})
mockedScript.mockImplementation((): any => ({
runInContext: mockRunInContext,
}))
mockedExecuteInE2B.mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
})
})
afterEach(() => {
@@ -54,20 +67,17 @@ describe('Function Execute API Route', () => {
code: 'return "test"',
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
expect(mockCreateContext).toHaveBeenCalled()
const contextArgs = mockCreateContext.mock.calls[0][0]
expect(mockedCreateContext).toHaveBeenCalled()
const contextArgs = mockedCreateContext.mock.calls[0][0]
expect(contextArgs).toHaveProperty('fetch')
expect(typeof contextArgs.fetch).toBe('function')
expect(typeof (contextArgs as any).fetch).toBe('function')
expect(contextArgs.fetch.name).toBe('secureFetch')
expect((contextArgs as any).fetch?.name).toBe('secureFetch')
})
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false)
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
@@ -75,16 +85,12 @@ describe('Function Execute API Route', () => {
})
it.concurrent('should allow legitimate external URLs', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('https://api.github.com/user').isValid).toBe(true)
expect(validateProxyUrl('https://httpbin.org/get').isValid).toBe(true)
expect(validateProxyUrl('https://example.com/api').isValid).toBe(true)
})
it.concurrent('should block dangerous protocols', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('file:///etc/passwd').isValid).toBe(false)
expect(validateProxyUrl('ftp://internal.server/files').isValid).toBe(false)
expect(validateProxyUrl('gopher://old.server/menu').isValid).toBe(false)
@@ -98,7 +104,6 @@ describe('Function Execute API Route', () => {
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -113,7 +118,6 @@ describe('Function Execute API Route', () => {
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -127,12 +131,11 @@ describe('Function Execute API Route', () => {
code: 'return "test"',
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
// The logger now logs execution success, not the request details
expect(mockLogger.info).toHaveBeenCalled()
expect(data.success).toBe(true)
})
})
@@ -145,11 +148,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// The code should be resolved to: return "secret-key-123"
})
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
@@ -160,11 +161,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// The code should be resolved with the email object
})
it.concurrent('should NOT treat email addresses as template variables', async () => {
@@ -178,11 +177,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should not try to replace <waleed@sim.ai> as a template variable
})
it.concurrent('should only match valid variable names in angle brackets', async () => {
@@ -194,11 +191,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should replace <validVar> and <another_valid> but not <invalid@email.com>
})
})
@@ -230,7 +225,6 @@ describe('Function Execute API Route', () => {
params: gmailData,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -255,7 +249,6 @@ describe('Function Execute API Route', () => {
params: complexEmailData,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -273,11 +266,9 @@ describe('Function Execute API Route', () => {
isCustomTool: true,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// For custom tools, parameters should be directly accessible as variables
})
})
@@ -289,7 +280,6 @@ describe('Function Execute API Route', () => {
headers: { 'Content-Type': 'application/json' },
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(500)
@@ -301,15 +291,11 @@ describe('Function Execute API Route', () => {
timeout: 10000,
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
const response = await POST(req)
const data = await response.json()
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringMatching(/\[.*\] Function execution request/),
expect.objectContaining({
timeout: 10000,
})
)
expect(response.status).toBe(200)
expect(data.success).toBe(true)
})
it.concurrent('should handle empty parameters object', async () => {
@@ -318,7 +304,6 @@ describe('Function Execute API Route', () => {
params: {},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -327,31 +312,25 @@ describe('Function Execute API Route', () => {
describe('Enhanced Error Handling', () => {
it('should provide detailed syntax error with line content', async () => {
// Mock VM Script to throw a syntax error
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Invalid or unexpected token')
error.name = 'SyntaxError'
error.stack = `user-function.js:5
const syntaxError = new Error('Invalid or unexpected token')
syntaxError.name = 'SyntaxError'
syntaxError.stack = `user-function.js:5
description: "This has a missing closing quote
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Invalid or unexpected token
at new Script (node:vm:117:7)
at POST (/path/to/route.ts:123:24)`
throw error
})
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -363,7 +342,6 @@ SyntaxError: Invalid or unexpected token
expect(data.error).toContain('Invalid or unexpected token')
expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)')
// Check debug information
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(3)
expect(data.debug.errorType).toBe('SyntaxError')
@@ -371,7 +349,6 @@ SyntaxError: Invalid or unexpected token
})
it('should provide detailed runtime error with line and column', async () => {
// Create the error object first
const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')")
runtimeError.name = 'TypeError'
runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod')
@@ -379,22 +356,13 @@ SyntaxError: Invalid or unexpected token
at user-function.js:9:3
at Script.runInContext (node:vm:147:14)`
// Mock successful script creation but runtime error
const mockScript = vi.fn().mockImplementation(() => ({
runInContext: vi.fn().mockRejectedValue(runtimeError),
}))
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockRunInContext.mockRejectedValueOnce(runtimeError)
const req = createMockRequest('POST', {
code: 'const obj = null;\nreturn obj.someMethod();',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -405,7 +373,6 @@ SyntaxError: Invalid or unexpected token
expect(data.error).toContain('return obj.someMethod();')
expect(data.error).toContain('Cannot read properties of null')
// Check debug information
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(2)
expect(data.debug.column).toBe(16)
@@ -414,28 +381,19 @@ SyntaxError: Invalid or unexpected token
})
it('should handle ReferenceError with enhanced details', async () => {
// Create the error object first
const referenceError = new Error('undefinedVariable is not defined')
referenceError.name = 'ReferenceError'
referenceError.stack = `ReferenceError: undefinedVariable is not defined
at user-function.js:4:8
at Script.runInContext (node:vm:147:14)`
const mockScript = vi.fn().mockImplementation(() => ({
runInContext: vi.fn().mockRejectedValue(referenceError),
}))
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockRunInContext.mockRejectedValueOnce(referenceError)
const req = createMockRequest('POST', {
code: 'const x = 42;\nreturn undefinedVariable + x;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -448,24 +406,18 @@ SyntaxError: Invalid or unexpected token
})
it('should handle errors without line content gracefully', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Generic error without stack trace')
error.name = 'Error'
// No stack trace
throw error
})
const genericError = new Error('Generic error without stack trace')
genericError.name = 'Error'
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw genericError
})
const req = createMockRequest('POST', {
code: 'return "test";',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -473,7 +425,6 @@ SyntaxError: Invalid or unexpected token
expect(data.success).toBe(false)
expect(data.error).toBe('Generic error without stack trace')
// Should still have debug info, but without line details
expect(data.debug).toBeDefined()
expect(data.debug.errorType).toBe('Error')
expect(data.debug.line).toBeUndefined()
@@ -481,58 +432,47 @@ SyntaxError: Invalid or unexpected token
})
it('should extract line numbers from different stack trace formats', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Test error')
error.name = 'Error'
error.stack = `Error: Test error
const testError = new Error('Test error')
testError.name = 'Error'
testError.stack = `Error: Test error
at user-function.js:7:25
at async function
at Script.runInContext (node:vm:147:14)`
throw error
})
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw testError
})
const req = createMockRequest('POST', {
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
// Line 7 in VM should map to line 5 in user code (7 - 3 + 1 = 5)
expect(data.debug.line).toBe(5)
expect(data.debug.column).toBe(25)
expect(data.debug.lineContent).toBe('return a + b + c + d;')
})
it.concurrent('should provide helpful suggestions for common syntax errors', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Unexpected end of input')
error.name = 'SyntaxError'
error.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
throw error
})
const syntaxError = new Error('Unexpected end of input')
syntaxError.name = 'SyntaxError'
syntaxError.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test"\n// Missing closing brace',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -546,7 +486,6 @@ SyntaxError: Invalid or unexpected token
describe('Utility Functions', () => {
it.concurrent('should properly escape regex special characters', async () => {
// This tests the escapeRegExp function indirectly
const req = createMockRequest('POST', {
code: 'return {{special.chars+*?}}',
envVars: {
@@ -554,15 +493,12 @@ SyntaxError: Invalid or unexpected token
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should handle special regex characters in variable names
})
it.concurrent('should handle JSON serialization edge cases', async () => {
// Test with complex but not circular data first
const req = createMockRequest('POST', {
code: 'return <complexData>',
params: {
@@ -578,7 +514,6 @@ SyntaxError: Invalid or unexpected token
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)

View File

@@ -4,148 +4,207 @@
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createMockRequest,
mockExecutionDependencies,
sampleWorkflowState,
} from '@/app/api/__test-utils__/utils'
import { createMockRequest, mockExecutionDependencies } from '@/app/api/__test-utils__/utils'
const {
mockGetSession,
mockGetUserEntityPermissions,
mockSelectLimit,
mockInsertValues,
mockOnConflictDoUpdate,
mockInsert,
mockUpdate,
mockDelete,
mockTransaction,
mockRandomUUID,
mockGetScheduleTimeValues,
mockGetSubBlockValue,
mockGenerateCronExpression,
mockCalculateNextRunTime,
mockValidateCronExpression,
} = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockGetUserEntityPermissions: vi.fn(),
mockSelectLimit: vi.fn(),
mockInsertValues: vi.fn(),
mockOnConflictDoUpdate: vi.fn(),
mockInsert: vi.fn(),
mockUpdate: vi.fn(),
mockDelete: vi.fn(),
mockTransaction: vi.fn(),
mockRandomUUID: vi.fn(),
mockGetScheduleTimeValues: vi.fn(),
mockGetSubBlockValue: vi.fn(),
mockGenerateCronExpression: vi.fn(),
mockCalculateNextRunTime: vi.fn(),
mockValidateCronExpression: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: mockGetUserEntityPermissions,
}))
vi.mock('@sim/db', () => ({
db: {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: mockSelectLimit,
}),
}),
}),
insert: mockInsert,
update: mockUpdate,
delete: mockDelete,
},
}))
vi.mock('@sim/db/schema', () => ({
workflow: {
id: 'workflow_id',
userId: 'user_id',
workspaceId: 'workspace_id',
},
workflowSchedule: {
id: 'schedule_id',
workflowId: 'workflow_id',
blockId: 'block_id',
cronExpression: 'cron_expression',
nextRunAt: 'next_run_at',
status: 'status',
},
}))
vi.mock('drizzle-orm', () => ({
eq: vi.fn((...args) => ({ type: 'eq', args })),
and: vi.fn((...args) => ({ type: 'and', args })),
}))
vi.mock('crypto', () => ({
randomUUID: mockRandomUUID,
default: {
randomUUID: mockRandomUUID,
},
}))
vi.mock('@/lib/workflows/schedules/utils', () => ({
getScheduleTimeValues: mockGetScheduleTimeValues,
getSubBlockValue: mockGetSubBlockValue,
generateCronExpression: mockGenerateCronExpression,
calculateNextRunTime: mockCalculateNextRunTime,
validateCronExpression: mockValidateCronExpression,
BlockState: {},
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
})),
}))
vi.mock('@/lib/core/telemetry', () => ({
trackPlatformEvent: vi.fn(),
}))
import { db } from '@sim/db'
import { POST } from '@/app/api/schedules/route'
describe('Schedule Configuration API Route', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
;(db as any).transaction = mockTransaction
mockExecutionDependencies()
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: {
id: 'user-id',
email: 'test@example.com',
},
}),
}))
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'), // User has admin permissions
}))
const _workflowStateWithSchedule = {
...sampleWorkflowState,
blocks: {
...sampleWorkflowState.blocks,
'starter-id': {
...sampleWorkflowState.blocks['starter-id'],
subBlocks: {
...sampleWorkflowState.blocks['starter-id'].subBlocks,
startWorkflow: { id: 'startWorkflow', type: 'dropdown', value: 'schedule' },
scheduleType: { id: 'scheduleType', type: 'dropdown', value: 'daily' },
scheduleTime: { id: 'scheduleTime', type: 'time-input', value: '09:30' },
dailyTime: { id: 'dailyTime', type: 'time-input', value: '09:30' },
},
},
mockGetSession.mockResolvedValue({
user: {
id: 'user-id',
email: 'test@example.com',
},
}
vi.doMock('@sim/db', () => {
let callCount = 0
const mockInsert = {
values: vi.fn().mockImplementation(() => ({
onConflictDoUpdate: vi.fn().mockResolvedValue({}),
})),
}
const mockDb = {
select: vi.fn().mockImplementation(() => ({
from: vi.fn().mockImplementation(() => ({
where: vi.fn().mockImplementation(() => ({
limit: vi.fn().mockImplementation(() => {
callCount++
// First call: workflow lookup for authorization
if (callCount === 1) {
return [
{
id: 'workflow-id',
userId: 'user-id',
workspaceId: null, // User owns the workflow directly
},
]
}
// Second call: existing schedule lookup - return existing schedule for update test
return [
{
id: 'existing-schedule-id',
workflowId: 'workflow-id',
blockId: 'starter-id',
cronExpression: '0 9 * * *',
nextRunAt: new Date(),
status: 'active',
},
]
}),
})),
})),
})),
insert: vi.fn().mockReturnValue(mockInsert),
update: vi.fn().mockImplementation(() => ({
set: vi.fn().mockImplementation(() => ({
where: vi.fn().mockResolvedValue([]),
})),
})),
delete: vi.fn().mockImplementation(() => ({
where: vi.fn().mockResolvedValue([]),
})),
transaction: vi.fn().mockImplementation(async (callback) => {
const tx = {
insert: vi.fn().mockReturnValue(mockInsert),
}
return callback(tx)
}),
}
return { db: mockDb }
})
vi.doMock('crypto', () => ({
randomUUID: vi.fn(() => 'test-uuid'),
default: {
randomUUID: vi.fn(() => 'test-uuid'),
mockGetUserEntityPermissions.mockResolvedValue('admin')
mockSelectLimit.mockReturnValue([
{
id: 'workflow-id',
userId: 'user-id',
workspaceId: null,
},
])
mockInsertValues.mockImplementation(() => ({
onConflictDoUpdate: mockOnConflictDoUpdate,
}))
mockOnConflictDoUpdate.mockResolvedValue({})
mockInsert.mockReturnValue({
values: mockInsertValues,
})
mockUpdate.mockImplementation(() => ({
set: vi.fn().mockImplementation(() => ({
where: vi.fn().mockResolvedValue([]),
})),
}))
vi.doMock('@/lib/workflows/schedules/utils', () => ({
getScheduleTimeValues: vi.fn().mockReturnValue({
scheduleTime: '09:30',
minutesInterval: 15,
hourlyMinute: 0,
dailyTime: [9, 30],
weeklyDay: 1,
weeklyTime: [9, 30],
monthlyDay: 1,
monthlyTime: [9, 30],
}),
getSubBlockValue: vi.fn().mockImplementation((block: any, id: string) => {
const subBlocks = {
startWorkflow: 'schedule',
scheduleType: 'daily',
scheduleTime: '09:30',
dailyTime: '09:30',
}
return subBlocks[id as keyof typeof subBlocks] || ''
}),
generateCronExpression: vi.fn().mockReturnValue('0 9 * * *'),
calculateNextRunTime: vi.fn().mockReturnValue(new Date()),
validateCronExpression: vi.fn().mockReturnValue({ isValid: true }),
BlockState: {},
mockDelete.mockImplementation(() => ({
where: vi.fn().mockResolvedValue([]),
}))
mockTransaction.mockImplementation(async (callback) => {
const tx = {
insert: vi.fn().mockReturnValue({
values: mockInsertValues,
}),
}
return callback(tx)
})
mockRandomUUID.mockReturnValue('test-uuid')
mockGetScheduleTimeValues.mockReturnValue({
scheduleTime: '09:30',
minutesInterval: 15,
hourlyMinute: 0,
dailyTime: [9, 30],
weeklyDay: 1,
weeklyTime: [9, 30],
monthlyDay: 1,
monthlyTime: [9, 30],
})
mockGetSubBlockValue.mockImplementation((block: any, id: string) => {
const subBlocks = {
startWorkflow: 'schedule',
scheduleType: 'daily',
scheduleTime: '09:30',
dailyTime: '09:30',
}
return subBlocks[id as keyof typeof subBlocks] || ''
})
mockGenerateCronExpression.mockReturnValue('0 9 * * *')
mockCalculateNextRunTime.mockReturnValue(new Date())
mockValidateCronExpression.mockReturnValue({ isValid: true })
})
afterEach(() => {
vi.clearAllMocks()
})
/**
* Test creating a new schedule
*/
it('should create a new schedule successfully', async () => {
const req = createMockRequest('POST', {
workflowId: 'workflow-id',
@@ -166,8 +225,6 @@ describe('Schedule Configuration API Route', () => {
},
})
const { POST } = await import('@/app/api/schedules/route')
const response = await POST(req)
expect(response).toBeDefined()
@@ -177,38 +234,16 @@ describe('Schedule Configuration API Route', () => {
expect(responseData).toHaveProperty('message', 'Schedule updated')
expect(responseData).toHaveProperty('cronExpression', '0 9 * * *')
expect(responseData).toHaveProperty('nextRunAt')
// We can't verify the utility functions were called directly
// since we're mocking them at the module level
// Instead, we just verify that the response has the expected properties
})
/**
* Test error handling
*/
it('should handle errors gracefully', async () => {
vi.doMock('@sim/db', () => ({
db: {
select: vi.fn().mockImplementation(() => ({
from: vi.fn().mockImplementation(() => ({
where: vi.fn().mockImplementation(() => ({
limit: vi.fn().mockImplementation(() => []),
})),
})),
})),
insert: vi.fn().mockImplementation(() => {
throw new Error('Database error')
}),
},
}))
mockSelectLimit.mockReturnValue([])
const req = createMockRequest('POST', {
workflowId: 'workflow-id',
state: { blocks: {}, edges: [], loops: {} },
})
const { POST } = await import('@/app/api/schedules/route')
const response = await POST(req)
expect(response.status).toBeGreaterThanOrEqual(400)
@@ -216,21 +251,14 @@ describe('Schedule Configuration API Route', () => {
expect(data).toHaveProperty('error')
})
/**
* Test authentication requirement
*/
it('should require authentication', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
mockGetSession.mockResolvedValue(null)
const req = createMockRequest('POST', {
workflowId: 'workflow-id',
state: { blocks: {}, edges: [], loops: {} },
})
const { POST } = await import('@/app/api/schedules/route')
const response = await POST(req)
expect(response.status).toBe(401)
@@ -238,16 +266,11 @@ describe('Schedule Configuration API Route', () => {
expect(data).toHaveProperty('error', 'Unauthorized')
})
/**
* Test invalid data handling
*/
it('should validate input data', async () => {
const req = createMockRequest('POST', {
workflowId: 'workflow-id',
})
const { POST } = await import('@/app/api/schedules/route')
const response = await POST(req)
expect(response.status).toBe(400)

View File

@@ -8,45 +8,62 @@
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('Workflow By ID API Route', () => {
const mockLogger = {
const mockGetSession = vi.fn()
const mockLoadWorkflowFromNormalizedTables = vi.fn()
const mockGetWorkflowById = vi.fn()
const mockGetWorkflowAccessContext = vi.fn()
const mockDbDelete = vi.fn()
const mockDbUpdate = vi.fn()
vi.mock('@/lib/auth', () => ({
getSession: () => mockGetSession(),
}))
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
}))
vi.mock('@/lib/workflows/persistence/utils', () => ({
loadWorkflowFromNormalizedTables: (workflowId: string) =>
mockLoadWorkflowFromNormalizedTables(workflowId),
}))
vi.mock('@/lib/workflows/utils', async () => {
const actual =
await vi.importActual<typeof import('@/lib/workflows/utils')>('@/lib/workflows/utils')
return {
...actual,
getWorkflowById: (workflowId: string) => mockGetWorkflowById(workflowId),
getWorkflowAccessContext: (workflowId: string, userId?: string) =>
mockGetWorkflowAccessContext(workflowId, userId),
}
})
const mockGetWorkflowById = vi.fn()
const mockGetWorkflowAccessContext = vi.fn()
vi.mock('@sim/db', () => ({
db: {
delete: () => mockDbDelete(),
update: () => mockDbUpdate(),
},
workflow: {},
}))
import { DELETE, GET, PUT } from './route'
describe('Workflow By ID API Route', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('mock-request-id-12345678'),
})
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/workflows/persistence/utils', () => ({
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(null),
}))
mockGetWorkflowById.mockReset()
mockGetWorkflowAccessContext.mockReset()
vi.doMock('@/lib/workflows/utils', async () => {
const actual =
await vi.importActual<typeof import('@/lib/workflows/utils')>('@/lib/workflows/utils')
return {
...actual,
getWorkflowById: mockGetWorkflowById,
getWorkflowAccessContext: mockGetWorkflowAccessContext,
}
})
mockLoadWorkflowFromNormalizedTables.mockResolvedValue(null)
})
afterEach(() => {
@@ -55,14 +72,11 @@ describe('Workflow By ID API Route', () => {
describe('GET /api/workflows/[id]', () => {
it('should return 401 when user is not authenticated', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(401)
@@ -71,14 +85,12 @@ describe('Workflow By ID API Route', () => {
})
it('should return 404 when workflow does not exist', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(null)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(null)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: null,
workspaceOwnerId: null,
workspacePermission: null,
@@ -89,7 +101,6 @@ describe('Workflow By ID API Route', () => {
const req = new NextRequest('http://localhost:3000/api/workflows/nonexistent')
const params = Promise.resolve({ id: 'nonexistent' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(404)
@@ -113,14 +124,12 @@ describe('Workflow By ID API Route', () => {
isFromNormalizedTables: true,
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
@@ -128,23 +137,11 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@/lib/workflows/persistence/utils', () => ({
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData),
}))
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
isOwner: true,
isWorkspaceOwner: false,
})
mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData)
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(200)
@@ -168,27 +165,12 @@ describe('Workflow By ID API Route', () => {
isFromNormalizedTables: true,
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: 'admin',
isOwner: false,
isWorkspaceOwner: false,
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
vi.doMock('@/lib/workflows/persistence/utils', () => ({
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData),
}))
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: 'read',
@@ -196,15 +178,11 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: vi.fn().mockResolvedValue('read'),
hasAdminPermission: vi.fn().mockResolvedValue(false),
}))
mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData)
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(200)
@@ -220,14 +198,12 @@ describe('Workflow By ID API Route', () => {
workspaceId: 'workspace-456',
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: null,
@@ -238,7 +214,6 @@ describe('Workflow By ID API Route', () => {
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(403)
@@ -262,14 +237,12 @@ describe('Workflow By ID API Route', () => {
isFromNormalizedTables: true,
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
@@ -277,14 +250,11 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@/lib/workflows/persistence/utils', () => ({
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData),
}))
mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData)
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(200)
@@ -303,14 +273,12 @@ describe('Workflow By ID API Route', () => {
workspaceId: null,
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
@@ -318,14 +286,9 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@sim/db', () => ({
db: {
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]),
}),
},
workflow: {},
}))
mockDbDelete.mockReturnValue({
where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]),
})
global.fetch = vi.fn().mockResolvedValue({
ok: true,
@@ -336,7 +299,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { DELETE } = await import('@/app/api/workflows/[id]/route')
const response = await DELETE(req, { params })
expect(response.status).toBe(200)
@@ -352,14 +314,12 @@ describe('Workflow By ID API Route', () => {
workspaceId: 'workspace-456',
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: 'admin',
@@ -367,14 +327,9 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@sim/db', () => ({
db: {
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]),
}),
},
workflow: {},
}))
mockDbDelete.mockReturnValue({
where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]),
})
global.fetch = vi.fn().mockResolvedValue({
ok: true,
@@ -385,7 +340,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { DELETE } = await import('@/app/api/workflows/[id]/route')
const response = await DELETE(req, { params })
expect(response.status).toBe(200)
@@ -401,14 +355,12 @@ describe('Workflow By ID API Route', () => {
workspaceId: 'workspace-456',
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: null,
@@ -421,7 +373,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { DELETE } = await import('@/app/api/workflows/[id]/route')
const response = await DELETE(req, { params })
expect(response.status).toBe(403)
@@ -442,14 +393,12 @@ describe('Workflow By ID API Route', () => {
const updateData = { name: 'Updated Workflow' }
const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() }
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
@@ -457,18 +406,13 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@sim/db', () => ({
db: {
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
}),
}),
mockDbUpdate.mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
}),
},
workflow: {},
}))
}),
})
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
method: 'PUT',
@@ -476,7 +420,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { PUT } = await import('@/app/api/workflows/[id]/route')
const response = await PUT(req, { params })
expect(response.status).toBe(200)
@@ -495,14 +438,12 @@ describe('Workflow By ID API Route', () => {
const updateData = { name: 'Updated Workflow' }
const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() }
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: 'write',
@@ -510,18 +451,13 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
vi.doMock('@sim/db', () => ({
db: {
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
}),
}),
mockDbUpdate.mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
}),
},
workflow: {},
}))
}),
})
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
method: 'PUT',
@@ -529,7 +465,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { PUT } = await import('@/app/api/workflows/[id]/route')
const response = await PUT(req, { params })
expect(response.status).toBe(200)
@@ -547,14 +482,12 @@ describe('Workflow By ID API Route', () => {
const updateData = { name: 'Updated Workflow' }
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: 'workspace-456',
workspacePermission: 'read',
@@ -568,7 +501,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { PUT } = await import('@/app/api/workflows/[id]/route')
const response = await PUT(req, { params })
expect(response.status).toBe(403)
@@ -584,14 +516,12 @@ describe('Workflow By ID API Route', () => {
workspaceId: null,
}
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValueOnce({
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
mockGetWorkflowAccessContext.mockResolvedValue({
workflow: mockWorkflow,
workspaceOwnerId: null,
workspacePermission: null,
@@ -599,7 +529,6 @@ describe('Workflow By ID API Route', () => {
isWorkspaceOwner: false,
})
// Invalid data - empty name
const invalidData = { name: '' }
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
@@ -608,7 +537,6 @@ describe('Workflow By ID API Route', () => {
})
const params = Promise.resolve({ id: 'workflow-123' })
const { PUT } = await import('@/app/api/workflows/[id]/route')
const response = await PUT(req, { params })
expect(response.status).toBe(400)
@@ -619,24 +547,20 @@ describe('Workflow By ID API Route', () => {
describe('Error handling', () => {
it.concurrent('should handle database errors gracefully', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-123' },
}),
}))
mockGetSession.mockResolvedValue({
user: { id: 'user-123' },
})
mockGetWorkflowById.mockRejectedValueOnce(new Error('Database connection timeout'))
mockGetWorkflowById.mockRejectedValue(new Error('Database connection timeout'))
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
const params = Promise.resolve({ id: 'workflow-123' })
const { GET } = await import('@/app/api/workflows/[id]/route')
const response = await GET(req, { params })
expect(response.status).toBe(500)
const data = await response.json()
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})
})

View File

@@ -1,6 +1,5 @@
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils'
/**
* Tests for workspace invitation by ID API route
@@ -9,137 +8,154 @@ import { mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils'
* @vitest-environment node
*/
describe('Workspace Invitation [invitationId] API Route', () => {
const mockUser = {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
}
const mockGetSession = vi.fn()
const mockHasWorkspaceAdminAccess = vi.fn()
const mockWorkspace = {
id: 'workspace-456',
name: 'Test Workspace',
}
let dbSelectResults: any[] = []
let dbSelectCallIndex = 0
const mockInvitation = {
id: 'invitation-789',
workspaceId: 'workspace-456',
email: 'invited@example.com',
inviterId: 'inviter-321',
status: 'pending',
token: 'token-abc123',
permissions: 'read',
expiresAt: new Date(Date.now() + 86400000), // 1 day from now
createdAt: new Date(),
updatedAt: new Date(),
}
const mockDbSelect = vi.fn().mockImplementation(() => ({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
then: vi.fn().mockImplementation((callback: (rows: any[]) => any) => {
const result = dbSelectResults[dbSelectCallIndex] || []
dbSelectCallIndex++
return Promise.resolve(callback ? callback(result) : result)
}),
}))
let mockDbResults: any[] = []
let mockGetSession: any
let mockHasWorkspaceAdminAccess: any
let mockTransaction: any
const mockDbInsert = vi.fn().mockImplementation(() => ({
values: vi.fn().mockResolvedValue(undefined),
}))
beforeEach(async () => {
vi.resetModules()
vi.resetAllMocks()
const mockDbUpdate = vi.fn().mockImplementation(() => ({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}))
mockDbResults = []
mockConsoleLogger()
mockAuth(mockUser)
const mockDbDelete = vi.fn().mockImplementation(() => ({
where: vi.fn().mockResolvedValue(undefined),
}))
vi.doMock('crypto', () => ({
randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'),
}))
mockGetSession = vi.fn()
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
mockHasWorkspaceAdminAccess = vi.fn()
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
hasWorkspaceAdminAccess: mockHasWorkspaceAdminAccess,
}))
vi.doMock('@/lib/core/config/env', () => {
const mockEnv = {
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
BILLING_ENABLED: false,
}
return {
env: mockEnv,
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) =>
mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable],
}
})
mockTransaction = vi.fn()
const mockDbChain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
then: vi.fn().mockImplementation((callback: any) => {
const result = mockDbResults.shift() || []
return callback ? callback(result) : Promise.resolve(result)
}),
insert: vi.fn().mockReturnThis(),
const mockDbTransaction = vi.fn().mockImplementation(async (callback: any) => {
await callback({
insert: vi.fn().mockReturnValue({
values: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
transaction: mockTransaction,
}
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
}),
})
})
vi.doMock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@/lib/auth', () => ({
getSession: () => mockGetSession(),
}))
vi.doMock('@sim/db/schema', () => ({
workspaceInvitation: {
id: 'id',
workspaceId: 'workspaceId',
email: 'email',
inviterId: 'inviterId',
status: 'status',
token: 'token',
permissions: 'permissions',
expiresAt: 'expiresAt',
},
workspace: {
id: 'id',
name: 'name',
},
user: {
id: 'id',
email: 'email',
},
permissions: {
id: 'id',
entityType: 'entityType',
entityId: 'entityId',
userId: 'userId',
permissionType: 'permissionType',
},
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
hasWorkspaceAdminAccess: (userId: string, workspaceId: string) =>
mockHasWorkspaceAdminAccess(userId, workspaceId),
}))
vi.doMock('drizzle-orm', () => ({
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
and: vi.fn((...args) => ({ type: 'and', args })),
}))
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}))
vi.mock('@/lib/core/utils/urls', () => ({
getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'),
}))
vi.mock('@sim/db', () => ({
db: {
select: () => mockDbSelect(),
insert: (table: any) => mockDbInsert(table),
update: (table: any) => mockDbUpdate(table),
delete: (table: any) => mockDbDelete(table),
transaction: (callback: any) => mockDbTransaction(callback),
},
}))
vi.mock('@sim/db/schema', () => ({
workspaceInvitation: {
id: 'id',
workspaceId: 'workspaceId',
email: 'email',
inviterId: 'inviterId',
status: 'status',
token: 'token',
permissions: 'permissions',
expiresAt: 'expiresAt',
},
workspace: {
id: 'id',
name: 'name',
},
user: {
id: 'id',
email: 'email',
},
permissions: {
id: 'id',
entityType: 'entityType',
entityId: 'entityId',
userId: 'userId',
permissionType: 'permissionType',
},
}))
vi.mock('drizzle-orm', () => ({
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
and: vi.fn((...args) => ({ type: 'and', args })),
}))
vi.mock('crypto', () => ({
randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'),
}))
import { DELETE, GET } from './route'
const mockUser = {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
}
const mockWorkspace = {
id: 'workspace-456',
name: 'Test Workspace',
}
const mockInvitation = {
id: 'invitation-789',
workspaceId: 'workspace-456',
email: 'invited@example.com',
inviterId: 'inviter-321',
status: 'pending',
token: 'token-abc123',
permissions: 'read',
expiresAt: new Date(Date.now() + 86400000), // 1 day from now
createdAt: new Date(),
updatedAt: new Date(),
}
describe('Workspace Invitation [invitationId] API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
dbSelectResults = []
dbSelectCallIndex = 0
})
describe('GET /api/workspaces/invitations/[invitationId]', () => {
it('should return invitation details when called without token', async () => {
const { GET } = await import('./route')
mockGetSession.mockResolvedValue({ user: mockUser })
mockDbResults.push([mockInvitation])
mockDbResults.push([mockWorkspace])
dbSelectResults = [[mockInvitation], [mockWorkspace]]
const request = new NextRequest('http://localhost/api/workspaces/invitations/invitation-789')
const params = Promise.resolve({ invitationId: 'invitation-789' })
@@ -157,8 +173,6 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should redirect to login when unauthenticated with token', async () => {
const { GET } = await import('./route')
mockGetSession.mockResolvedValue(null)
const request = new NextRequest(
@@ -174,27 +188,30 @@ describe('Workspace Invitation [invitationId] API Route', () => {
)
})
it('should accept invitation when called with valid token', async () => {
const { GET } = await import('./route')
it('should return 401 when unauthenticated without token', async () => {
mockGetSession.mockResolvedValue(null)
const request = new NextRequest('http://localhost/api/workspaces/invitations/invitation-789')
const params = Promise.resolve({ invitationId: 'invitation-789' })
const response = await GET(request, { params })
const data = await response.json()
expect(response.status).toBe(401)
expect(data).toEqual({ error: 'Unauthorized' })
})
it('should accept invitation when called with valid token', async () => {
mockGetSession.mockResolvedValue({
user: { ...mockUser, email: 'invited@example.com' },
})
mockDbResults.push([mockInvitation])
mockDbResults.push([mockWorkspace])
mockDbResults.push([{ ...mockUser, email: 'invited@example.com' }])
mockDbResults.push([])
mockTransaction.mockImplementation(async (callback: any) => {
await callback({
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
})
})
dbSelectResults = [
[mockInvitation], // invitation lookup
[mockWorkspace], // workspace lookup
[{ ...mockUser, email: 'invited@example.com' }], // user lookup
[], // existing permission check (empty = no existing)
]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
@@ -208,8 +225,6 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should redirect to error page when invitation expired', async () => {
const { GET } = await import('./route')
mockGetSession.mockResolvedValue({
user: { ...mockUser, email: 'invited@example.com' },
})
@@ -219,8 +234,7 @@ describe('Workspace Invitation [invitationId] API Route', () => {
expiresAt: new Date(Date.now() - 86400000), // 1 day ago
}
mockDbResults.push([expiredInvitation])
mockDbResults.push([mockWorkspace])
dbSelectResults = [[expiredInvitation], [mockWorkspace]]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
@@ -236,15 +250,15 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should redirect to error page when email mismatch', async () => {
const { GET } = await import('./route')
mockGetSession.mockResolvedValue({
user: { ...mockUser, email: 'wrong@example.com' },
})
mockDbResults.push([mockInvitation])
mockDbResults.push([mockWorkspace])
mockDbResults.push([{ ...mockUser, email: 'wrong@example.com' }])
dbSelectResults = [
[mockInvitation],
[mockWorkspace],
[{ ...mockUser, email: 'wrong@example.com' }],
]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123'
@@ -258,19 +272,29 @@ describe('Workspace Invitation [invitationId] API Route', () => {
'https://test.sim.ai/invite/invitation-789?error=email-mismatch'
)
})
it('should return 404 when invitation not found', async () => {
mockGetSession.mockResolvedValue({ user: mockUser })
dbSelectResults = [[]] // Empty result
const request = new NextRequest('http://localhost/api/workspaces/invitations/non-existent')
const params = Promise.resolve({ invitationId: 'non-existent' })
const response = await GET(request, { params })
const data = await response.json()
expect(response.status).toBe(404)
expect(data).toEqual({ error: 'Invitation not found or has expired' })
})
})
describe('DELETE /api/workspaces/invitations/[invitationId]', () => {
it('should return 401 when user is not authenticated', async () => {
const { DELETE } = await import('./route')
mockGetSession.mockResolvedValue(null)
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/invitation-789',
{
method: 'DELETE',
}
{ method: 'DELETE' }
)
const params = Promise.resolve({ invitationId: 'invitation-789' })
@@ -282,11 +306,8 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should return 404 when invitation does not exist', async () => {
const { DELETE } = await import('./route')
mockGetSession.mockResolvedValue({ user: mockUser })
mockDbResults.push([])
dbSelectResults = [[]]
const request = new NextRequest('http://localhost/api/workspaces/invitations/non-existent', {
method: 'DELETE',
@@ -301,18 +322,13 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should return 403 when user lacks admin access', async () => {
const { DELETE } = await import('./route')
mockGetSession.mockResolvedValue({ user: mockUser })
mockHasWorkspaceAdminAccess.mockResolvedValue(false)
mockDbResults.push([mockInvitation])
dbSelectResults = [[mockInvitation]]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/invitation-789',
{
method: 'DELETE',
}
{ method: 'DELETE' }
)
const params = Promise.resolve({ invitationId: 'invitation-789' })
@@ -325,19 +341,15 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should return 400 when trying to delete non-pending invitation', async () => {
const { DELETE } = await import('./route')
mockGetSession.mockResolvedValue({ user: mockUser })
mockHasWorkspaceAdminAccess.mockResolvedValue(true)
const acceptedInvitation = { ...mockInvitation, status: 'accepted' }
mockDbResults.push([acceptedInvitation])
dbSelectResults = [[acceptedInvitation]]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/invitation-789',
{
method: 'DELETE',
}
{ method: 'DELETE' }
)
const params = Promise.resolve({ invitationId: 'invitation-789' })
@@ -349,18 +361,13 @@ describe('Workspace Invitation [invitationId] API Route', () => {
})
it('should successfully delete pending invitation when user has admin access', async () => {
const { DELETE } = await import('./route')
mockGetSession.mockResolvedValue({ user: mockUser })
mockHasWorkspaceAdminAccess.mockResolvedValue(true)
mockDbResults.push([mockInvitation])
dbSelectResults = [[mockInvitation]]
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/invitation-789',
{
method: 'DELETE',
}
{ method: 'DELETE' }
)
const params = Promise.resolve({ invitationId: 'invitation-789' })
@@ -370,61 +377,5 @@ describe('Workspace Invitation [invitationId] API Route', () => {
expect(response.status).toBe(200)
expect(data).toEqual({ success: true })
})
it('should return 500 when database error occurs', async () => {
vi.resetModules()
const mockErrorDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
then: vi.fn().mockRejectedValue(new Error('Database connection failed')),
}
vi.doMock('@sim/db', () => ({ db: mockErrorDb }))
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({ user: mockUser }),
}))
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
hasWorkspaceAdminAccess: vi.fn(),
}))
vi.doMock('@/lib/core/config/env', () => {
const mockEnv = {
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
BILLING_ENABLED: false,
}
return {
env: mockEnv,
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) =>
mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable],
}
})
vi.doMock('@sim/db/schema', () => ({
workspaceInvitation: { id: 'id' },
}))
vi.doMock('drizzle-orm', () => ({
eq: vi.fn(),
}))
const { DELETE } = await import('./route')
const request = new NextRequest(
'http://localhost/api/workspaces/invitations/invitation-789',
{
method: 'DELETE',
}
)
const params = Promise.resolve({ invitationId: 'invitation-789' })
const response = await DELETE(request, { params })
const data = await response.json()
expect(response.status).toBe(500)
expect(data).toEqual({ error: 'Failed to delete invitation' })
})
})
})

View File

@@ -1,6 +1,6 @@
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
return (
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
<div>{children}</div>
</main>
)

View File

@@ -2,7 +2,7 @@
import type { ReactNode } from 'react'
import { splitReferenceSegment } from '@/lib/workflows/sanitization/references'
import { REFERENCE } from '@/executor/consts'
import { REFERENCE } from '@/executor/constants'
import { createCombinedPattern } from '@/executor/utils/reference-validation'
import { normalizeBlockName } from '@/stores/workflows/utils'
@@ -67,7 +67,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
if (matchText.startsWith(REFERENCE.ENV_VAR_START)) {
nodes.push(
<span key={key++} className='text-[#34B5FF] dark:text-[#34B5FF]'>
<span key={key++} className='text-[#34B5FF]'>
{matchText}
</span>
)
@@ -77,7 +77,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
if (split && shouldHighlightReference(split.reference)) {
pushPlainText(split.leading)
nodes.push(
<span key={key++} className='text-[#34B5FF] dark:text-[#34B5FF]'>
<span key={key++} className='text-[#34B5FF]'>
{split.reference}
</span>
)

View File

@@ -16,7 +16,7 @@ import { useTriggerConfigAggregation } from '@/hooks/use-trigger-config-aggregat
import { useWebhookManagement } from '@/hooks/use-webhook-management'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getTrigger, isTriggerValid } from '@/triggers'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
import { ShortInput } from '../short-input/short-input'
const logger = createLogger('TriggerSave')

View File

@@ -5,6 +5,7 @@ import { Badge } from '@/components/emcn/components/badge/badge'
import { Tooltip } from '@/components/emcn/components/tooltip/tooltip'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { createMcpToolId } from '@/lib/mcp/utils'
import { getProviderIdFromServiceId } from '@/lib/oauth'
@@ -242,6 +243,7 @@ const SubBlockRow = ({
rawValue,
workspaceId,
workflowId,
blockId,
allSubBlockValues,
}: {
title: string
@@ -250,6 +252,7 @@ const SubBlockRow = ({
rawValue?: unknown
workspaceId?: string
workflowId?: string
blockId?: string
allSubBlockValues?: Record<string, { value: unknown }>
}) => {
const getStringValue = useCallback(
@@ -353,6 +356,17 @@ const SubBlockRow = ({
return tool?.name ?? null
}, [subBlock?.type, rawValue, mcpToolsData])
const webhookUrlDisplayValue = useMemo(() => {
if (subBlock?.id !== 'webhookUrlDisplay' || !blockId) {
return null
}
const baseUrl = getBaseUrl()
const triggerPath = allSubBlockValues?.triggerPath?.value as string | undefined
return triggerPath
? `${baseUrl}/api/webhooks/trigger/${triggerPath}`
: `${baseUrl}/api/webhooks/trigger/${blockId}`
}, [subBlock?.id, blockId, allSubBlockValues])
const allVariables = useVariablesStore((state) => state.variables)
const variablesDisplayValue = useMemo(() => {
@@ -393,6 +407,7 @@ const SubBlockRow = ({
workflowSelectionName ||
mcpServerDisplayName ||
mcpToolDisplayName ||
webhookUrlDisplayValue ||
selectorDisplayName
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)
@@ -1002,6 +1017,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
rawValue={rawValue}
workspaceId={workspaceId}
workflowId={currentWorkflowId}
blockId={id}
allSubBlockValues={subBlockState}
/>
)

View File

@@ -2,7 +2,7 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
export default function WorkflowLayout({ children }: { children: React.ReactNode }) {
return (
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
<ErrorBoundary>{children}</ErrorBoundary>
</main>
)

View File

@@ -40,7 +40,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useSocket } from '@/app/workspace/providers/socket-provider'
import { getBlock } from '@/blocks'
import { isAnnotationOnlyBlock } from '@/executor/consts'
import { isAnnotationOnlyBlock } from '@/executor/constants'
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'

View File

@@ -46,7 +46,7 @@ export default function WorkflowsPage() {
// Always show loading state until redirect happens
// There should always be a default workflow, so we never show "no workflows found"
return (
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
<div className='flex h-full items-center justify-center'>
<div className='text-center'>
<div className='mx-auto mb-4'>

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { DAGBuilder } from '@/executor/dag/builder'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { EDGE, isConditionBlockType, isRouterBlockType } from '@/executor/consts'
import { EDGE, isConditionBlockType, isRouterBlockType } from '@/executor/constants'
import type { DAG } from '@/executor/dag/builder'
import {
buildBranchNodeId,

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType, LOOP, type SentinelType } from '@/executor/consts'
import { BlockType, LOOP, type SentinelType } from '@/executor/constants'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import { buildSentinelEndId, buildSentinelStartId } from '@/executor/utils/subflow-utils'

View File

@@ -1,4 +1,4 @@
import { BlockType, isMetadataOnlyBlockType } from '@/executor/consts'
import { BlockType, isMetadataOnlyBlockType } from '@/executor/constants'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import {
buildBranchNodeId,

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { isMetadataOnlyBlockType, isTriggerBlockType } from '@/executor/consts'
import { isMetadataOnlyBlockType, isTriggerBlockType } from '@/executor/constants'
import { extractBaseBlockId } from '@/executor/utils/subflow-utils'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'

View File

@@ -7,7 +7,7 @@ import {
DEFAULTS,
EDGE,
isSentinelBlockType,
} from '@/executor/consts'
} from '@/executor/constants'
import type { DAGNode } from '@/executor/dag/builder'
import type { BlockStateWriter, ContextExtensions } from '@/executor/execution/types'
import {

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { EDGE } from '@/executor/consts'
import { EDGE } from '@/executor/constants'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import type { DAGEdge } from '@/executor/dag/types'
import type { NormalizedBlockOutput } from '@/executor/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import type { DAG } from '@/executor/dag/builder'
import type { EdgeManager } from '@/executor/execution/edge-manager'
import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer'

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { isHosted } from '@/lib/core/config/environment'
import { getAllBlocks } from '@/blocks'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { createMcpToolId } from '@/lib/mcp/utils'
import { getAllBlocks } from '@/blocks'
import type { BlockOutput } from '@/blocks/types'
import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/consts'
import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/constants'
import { memoryService } from '@/executor/handlers/agent/memory'
import type {
AgentInputs,

View File

@@ -1,7 +1,7 @@
import '@/executor/__test-utils__/mock-dependencies'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { ApiBlockHandler } from '@/executor/handlers/api/api-handler'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType, HTTP } from '@/executor/consts'
import { BlockType, HTTP } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'

View File

@@ -1,7 +1,7 @@
import '@/executor/__test-utils__/mock-dependencies'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
import type { BlockState, ExecutionContext } from '@/executor/types'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/consts'
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,7 +1,7 @@
import '@/executor/__test-utils__/mock-dependencies'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
import type { ExecutionContext } from '@/executor/types'
import { getProviderFromModel } from '@/providers/utils'

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/consts'
import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { buildAPIUrl, extractAPIErrorMessage } from '@/executor/utils/http'
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,6 +1,6 @@
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { collectBlockData } from '@/executor/utils/block-data'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,7 +1,7 @@
import '@/executor/__test-utils__/mock-dependencies'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -8,7 +8,7 @@ import {
type FieldType,
HTTP,
PAUSE_RESUME,
} from '@/executor/consts'
} from '@/executor/constants'
import {
generatePauseContextId,
mapNodeMetadataToPauseScopes,

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, HTTP } from '@/executor/consts'
import { BlockType, HTTP } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -2,7 +2,7 @@ import '@/executor/__test-utils__/mock-dependencies'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { generateRouterPrompt } from '@/blocks/blocks/router'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { RouterBlockHandler } from '@/executor/handlers/router/router-handler'
import type { ExecutionContext } from '@/executor/types'
import { getProviderFromModel } from '@/providers/utils'

View File

@@ -2,7 +2,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRouterPrompt } from '@/blocks/blocks/router'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, DEFAULTS, HTTP, isAgentBlockType, ROUTER } from '@/executor/consts'
import { BlockType, DEFAULTS, HTTP, isAgentBlockType, ROUTER } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { calculateCost, getProviderFromModel } from '@/providers/utils'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { BlockType } from '@/executor/consts'
import { BlockType } from '@/executor/constants'
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

View File

@@ -3,7 +3,7 @@ import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import type { TraceSpan } from '@/lib/logs/types'
import type { BlockOutput } from '@/blocks/types'
import { Executor } from '@/executor'
import { BlockType, DEFAULTS, HTTP } from '@/executor/consts'
import { BlockType, DEFAULTS, HTTP } from '@/executor/constants'
import type {
BlockHandler,
ExecutionContext,

View File

@@ -1,4 +1,4 @@
import { PARALLEL } from '@/executor/consts'
import { PARALLEL } from '@/executor/constants'
import type { ExecutionContext, LoopPauseScope, ParallelPauseScope } from '@/executor/types'
interface NodeMetadataLike {

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/consts'
import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/constants'
import type { DAG } from '@/executor/dag/builder'
import type { LoopScope } from '@/executor/execution/state'
import type { BlockStateController } from '@/executor/execution/types'

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { EDGE } from '@/executor/consts'
import { EDGE } from '@/executor/constants'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import type { BlockExecutor } from '@/executor/execution/block-executor'
import type { BlockStateController } from '@/executor/execution/types'

View File

@@ -1,6 +1,6 @@
import { generateInternalToken } from '@/lib/auth/internal'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { HTTP } from '@/executor/consts'
import { HTTP } from '@/executor/constants'
export async function buildAuthHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { EVALUATOR } from '@/executor/consts'
import { EVALUATOR } from '@/executor/constants'
const logger = createLogger('JSONUtils')

View File

@@ -1,5 +1,5 @@
import { isLikelyReferenceSegment } from '@/lib/workflows/sanitization/references'
import { REFERENCE } from '@/executor/consts'
import { REFERENCE } from '@/executor/constants'
/**
* Creates a regex pattern for matching variable references.

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/consts'
import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/constants'
import type { SerializedParallel } from '@/serializer/types'
const logger = createLogger('SubflowUtils')
@@ -10,14 +10,8 @@ const SENTINEL_START_PATTERN = new RegExp(
`${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.START_SUFFIX}`
)
const SENTINEL_END_PATTERN = new RegExp(`${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.END_SUFFIX}`)
/**
* ==================
* LOOP UTILITIES
* ==================
*/
/**
* Build sentinel start node ID
*/
/** Build sentinel start node ID */
export function buildSentinelStartId(loopId: string): string {
return `${LOOP.SENTINEL.PREFIX}${loopId}${LOOP.SENTINEL.START_SUFFIX}`
}
@@ -41,11 +35,7 @@ export function extractLoopIdFromSentinel(sentinelId: string): string | null {
if (endMatch) return endMatch[1]
return null
}
/**
* ==================
* PARALLEL UTILITIES
* ==================
*/
/**
* Parse distribution items from parallel config
* Handles: arrays, JSON strings, and references

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { BlockType, REFERENCE } from '@/executor/consts'
import { BlockType, REFERENCE } from '@/executor/constants'
import type { ExecutionState, LoopScope } from '@/executor/execution/state'
import type { ExecutionContext } from '@/executor/types'
import { replaceValidReferences } from '@/executor/utils/reference-validation'

View File

@@ -1,4 +1,4 @@
import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/consts'
import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/constants'
import {
navigatePath,
type ResolutionContext,

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { extractEnvVarName, isEnvVarReference } from '@/executor/consts'
import { extractEnvVarName, isEnvVarReference } from '@/executor/constants'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
const logger = createLogger('EnvResolver')

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants'
import { extractBaseBlockId } from '@/executor/utils/subflow-utils'
import {
navigatePath,

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants'
import { extractBaseBlockId, extractBranchIndex } from '@/executor/utils/subflow-utils'
import {
navigatePath,

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import { VariableManager } from '@/lib/workflows/variables/variable-manager'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts'
import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants'
import {
navigatePath,
type ResolutionContext,

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@/lib/logs/console/logger'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getTrigger, isTriggerValid } from '@/triggers'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
const logger = createLogger('useTriggerConfigAggregation')

View File

@@ -9,7 +9,7 @@ import {
DEFAULT_FREE_STORAGE_LIMIT_GB,
DEFAULT_PRO_STORAGE_LIMIT_GB,
DEFAULT_TEAM_STORAGE_LIMIT_GB,
} from '@sim/db/consts'
} from '@sim/db/constants'
import { organization, subscription, userStats } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { getEnv } from '@/lib/core/config/env'

View File

@@ -12,7 +12,7 @@ import { AuthMode } from '@/blocks/types'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
import { tools as toolsRegistry } from '@/tools/registry'
import { getTrigger, isTriggerValid } from '@/triggers'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
export interface CopilotSubblockMetadata {
id: string

View File

@@ -12,7 +12,7 @@ import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
import { getAllBlocks, getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/consts'
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
/** Selector subblock types that can be validated */
const SELECTOR_TYPES = new Set([

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolCall, TraceSpan } from '@/lib/logs/types'
import { isWorkflowBlockType } from '@/executor/consts'
import { isWorkflowBlockType } from '@/executor/constants'
import type { ExecutionResult } from '@/executor/types'
const logger = createLogger('TraceSpans')

View File

@@ -1,6 +1,6 @@
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { createLogger } from '@/lib/logs/console/logger'
import { extractEnvVarName, isEnvVarReference } from '@/executor/consts'
import { extractEnvVarName, isEnvVarReference } from '@/executor/constants'
const logger = createLogger('EnvResolver')

View File

@@ -2,7 +2,7 @@ import type { Edge } from 'reactflow'
import { sanitizeWorkflowForSharing } from '@/lib/workflows/credentials/credential-extractor'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/consts'
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/constants'
/**
* Sanitized workflow state for copilot (removes all UI-specific data)

View File

@@ -1,5 +1,5 @@
import type { CopilotWorkflowState } from '@/lib/workflows/sanitization/json-sanitizer'
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/consts'
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
export interface EditOperation {
operation_type: 'add' | 'edit' | 'delete' | 'insert_into_subflow' | 'extract_from_subflow'

View File

@@ -41,7 +41,6 @@ describe('getApiKey', () => {
beforeEach(() => {
vi.clearAllMocks()
// @ts-expect-error - mocking boolean with different value
isHostedSpy.mockReturnValue(false)
module.require = vi.fn(() => ({
@@ -55,7 +54,6 @@ describe('getApiKey', () => {
})
it('should return user-provided key when not in hosted environment', () => {
// @ts-expect-error - mocking boolean with different value
isHostedSpy.mockReturnValue(false)
// For OpenAI
@@ -68,7 +66,6 @@ describe('getApiKey', () => {
})
it('should throw error if no key provided in non-hosted environment', () => {
// @ts-expect-error - mocking boolean with different value
isHostedSpy.mockReturnValue(false)
expect(() => getApiKey('openai', 'gpt-4')).toThrow('API key is required for openai gpt-4')

View File

@@ -3,7 +3,7 @@
*
* @vitest-environment node
*/
import { createServer } from 'http'
import { createServer, request as httpRequest } from 'http'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { createLogger } from '@/lib/logs/console/logger'
import { createSocketIOServer } from '@/socket-server/config/socket'
@@ -68,23 +68,17 @@ describe('Socket Server Index Integration', () => {
})
beforeEach(async () => {
// Use a random port for each test to avoid conflicts
PORT = 3333 + Math.floor(Math.random() * 1000)
// Create HTTP server
httpServer = createServer()
// Create Socket.IO server using extracted config
io = createSocketIOServer(httpServer)
// Initialize room manager after io is created
roomManager = new RoomManager(io)
// Configure HTTP request handler
const httpHandler = createHttpHandler(roomManager, logger)
httpServer.on('request', httpHandler)
// Start server with timeout handling
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Server failed to start on port ${PORT} within 15 seconds`))
@@ -98,7 +92,6 @@ describe('Socket Server Index Integration', () => {
httpServer.on('error', (err: any) => {
clearTimeout(timeout)
if (err.code === 'EADDRINUSE') {
// Try a different port
PORT = 3333 + Math.floor(Math.random() * 1000)
httpServer.listen(PORT, '0.0.0.0', () => {
resolve()
@@ -111,7 +104,6 @@ describe('Socket Server Index Integration', () => {
}, 20000)
afterEach(async () => {
// Properly close servers and wait for them to fully close
if (io) {
await new Promise<void>((resolve) => {
io.close(() => resolve())
@@ -132,18 +124,40 @@ describe('Socket Server Index Integration', () => {
})
it('should handle health check endpoint', async () => {
try {
const response = await fetch(`http://localhost:${PORT}/health`)
expect(response.status).toBe(200)
const data = await new Promise<{ status: string; timestamp: number; connections: number }>(
(resolve, reject) => {
const req = httpRequest(
{
hostname: 'localhost',
port: PORT,
path: '/health',
method: 'GET',
},
(res) => {
expect(res.statusCode).toBe(200)
const data = await response.json()
expect(data).toHaveProperty('status', 'ok')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('connections')
} catch (error) {
// Skip this test if fetch fails (likely due to test environment)
console.warn('Health check test skipped due to fetch error:', error)
}
let body = ''
res.on('data', (chunk) => {
body += chunk
})
res.on('end', () => {
try {
resolve(JSON.parse(body))
} catch (e) {
reject(e)
}
})
}
)
req.on('error', reject)
req.end()
}
)
expect(data).toHaveProperty('status', 'ok')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('connections')
})
})
@@ -228,7 +242,6 @@ describe('Socket Server Index Integration', () => {
describe('Module Integration', () => {
it.concurrent('should properly import all extracted modules', async () => {
// Test that all modules can be imported without errors
const { createSocketIOServer } = await import('@/socket-server/config/socket')
const { createHttpHandler } = await import('@/socket-server/routes/http')
const { RoomManager } = await import('@/socket-server/rooms/manager')
@@ -247,12 +260,10 @@ describe('Socket Server Index Integration', () => {
})
it.concurrent('should maintain all original functionality after refactoring', () => {
// Verify that the main components are properly instantiated
expect(httpServer).toBeDefined()
expect(io).toBeDefined()
expect(roomManager).toBeDefined()
// Verify core methods exist and are callable
expect(typeof roomManager.createWorkflowRoom).toBe('function')
expect(typeof roomManager.cleanupUserFromRoom).toBe('function')
expect(typeof roomManager.handleWorkflowDeletion).toBe('function')

View File

@@ -1,7 +1,7 @@
import { createServer } from 'http'
import { Server } from 'socket.io'
import { io, type Socket } from 'socket.io-client'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
describe('Socket Server Integration Tests', () => {
let httpServer: any
@@ -10,7 +10,6 @@ describe('Socket Server Integration Tests', () => {
let serverPort: number
beforeAll(async () => {
// Create a test server instance
httpServer = createServer()
socketServer = new Server(httpServer, {
cors: {
@@ -19,7 +18,6 @@ describe('Socket Server Integration Tests', () => {
},
})
// Start server on random port
await new Promise<void>((resolve) => {
httpServer.listen(() => {
serverPort = httpServer.address()?.port
@@ -27,7 +25,6 @@ describe('Socket Server Integration Tests', () => {
})
})
// Basic socket handlers for testing
socketServer.on('connection', (socket) => {
socket.on('join-workflow', ({ workflowId }) => {
socket.join(workflowId)
@@ -41,19 +38,7 @@ describe('Socket Server Integration Tests', () => {
})
})
})
})
afterAll(async () => {
if (socketServer) {
socketServer.close()
}
if (httpServer) {
httpServer.close()
}
})
beforeEach(async () => {
// Create client socket for each test
clientSocket = io(`http://localhost:${serverPort}`, {
transports: ['polling', 'websocket'],
})
@@ -65,10 +50,16 @@ describe('Socket Server Integration Tests', () => {
})
})
afterEach(() => {
afterAll(async () => {
if (clientSocket) {
clientSocket.close()
}
if (socketServer) {
socketServer.close()
}
if (httpServer) {
httpServer.close()
}
})
it('should connect to socket server', () => {
@@ -79,7 +70,7 @@ describe('Socket Server Integration Tests', () => {
const workflowId = 'test-workflow-123'
const joinedPromise = new Promise<void>((resolve) => {
clientSocket.on('joined-workflow', (data) => {
clientSocket.once('joined-workflow', (data) => {
expect(data.workflowId).toBe(workflowId)
resolve()
})
@@ -92,39 +83,45 @@ describe('Socket Server Integration Tests', () => {
it('should broadcast workflow operations', async () => {
const workflowId = 'test-workflow-456'
// Create second client
const client2 = io(`http://localhost:${serverPort}`)
await new Promise<void>((resolve) => {
client2.on('connect', resolve)
client2.once('connect', resolve)
})
// Both clients join the same workflow
clientSocket.emit('join-workflow', { workflowId })
client2.emit('join-workflow', { workflowId })
// Wait for joins to complete
await new Promise((resolve) => setTimeout(resolve, 100))
const operationPromise = new Promise<void>((resolve) => {
client2.on('workflow-operation', (data) => {
expect(data.operation).toBe('add')
expect(data.target).toBe('block')
expect(data.payload.id).toBe('block-123')
resolve()
try {
const join1Promise = new Promise<void>((resolve) => {
clientSocket.once('joined-workflow', () => resolve())
})
const join2Promise = new Promise<void>((resolve) => {
client2.once('joined-workflow', () => resolve())
})
})
// Client 1 sends operation
clientSocket.emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: 'block-123', type: 'action', name: 'Test Block' },
timestamp: Date.now(),
})
clientSocket.emit('join-workflow', { workflowId })
client2.emit('join-workflow', { workflowId })
await operationPromise
client2.close()
await Promise.all([join1Promise, join2Promise])
const operationPromise = new Promise<void>((resolve) => {
client2.once('workflow-operation', (data) => {
expect(data.operation).toBe('add')
expect(data.target).toBe('block')
expect(data.payload.id).toBe('block-123')
resolve()
})
})
clientSocket.emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: 'block-123', type: 'action', name: 'Test Block' },
timestamp: Date.now(),
})
await operationPromise
} finally {
client2.close()
}
})
it('should handle multiple concurrent connections', async () => {
@@ -132,51 +129,58 @@ describe('Socket Server Integration Tests', () => {
const clients: Socket[] = []
const workflowId = 'stress-test-workflow'
// Create multiple clients
for (let i = 0; i < numClients; i++) {
const client = io(`http://localhost:${serverPort}`)
clients.push(client)
await new Promise<void>((resolve) => {
client.on('connect', resolve)
})
client.emit('join-workflow', { workflowId })
}
// Wait for all joins
await new Promise((resolve) => setTimeout(resolve, 200))
let receivedCount = 0
const expectedCount = numClients - 1 // All except sender
const operationPromise = new Promise<void>((resolve) => {
clients.forEach((client, index) => {
if (index === 0) return // Skip sender
client.on('workflow-operation', () => {
receivedCount++
if (receivedCount === expectedCount) {
resolve()
}
try {
const connectPromises = Array.from({ length: numClients }, () => {
const client = io(`http://localhost:${serverPort}`)
clients.push(client)
return new Promise<void>((resolve) => {
client.once('connect', resolve)
})
})
})
// First client sends operation
clients[0].emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: 'stress-block', type: 'action' },
timestamp: Date.now(),
})
await Promise.all(connectPromises)
await operationPromise
expect(receivedCount).toBe(expectedCount)
const joinPromises = clients.map((client) => {
return new Promise<void>((resolve) => {
client.once('joined-workflow', () => resolve())
})
})
// Clean up
clients.forEach((client) => client.close())
clients.forEach((client) => {
client.emit('join-workflow', { workflowId })
})
await Promise.all(joinPromises)
let receivedCount = 0
const expectedCount = numClients - 1
const operationPromise = new Promise<void>((resolve) => {
clients.forEach((client, index) => {
if (index === 0) return
client.once('workflow-operation', () => {
receivedCount++
if (receivedCount === expectedCount) {
resolve()
}
})
})
})
clients[0].emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: 'stress-block', type: 'action' },
timestamp: Date.now(),
})
await operationPromise
expect(receivedCount).toBe(expectedCount)
} finally {
clients.forEach((client) => client.close())
}
})
it('should handle rapid operations without loss', async () => {
@@ -185,43 +189,51 @@ describe('Socket Server Integration Tests', () => {
const client2 = io(`http://localhost:${serverPort}`)
await new Promise<void>((resolve) => {
client2.on('connect', resolve)
client2.once('connect', resolve)
})
clientSocket.emit('join-workflow', { workflowId })
client2.emit('join-workflow', { workflowId })
await new Promise((resolve) => setTimeout(resolve, 100))
let receivedCount = 0
const receivedOperations = new Set<string>()
const operationsPromise = new Promise<void>((resolve) => {
client2.on('workflow-operation', (data) => {
receivedCount++
receivedOperations.add(data.payload.id)
if (receivedCount === numOperations) {
resolve()
}
try {
const join1Promise = new Promise<void>((resolve) => {
clientSocket.once('joined-workflow', () => resolve())
})
})
// Send rapid operations
for (let i = 0; i < numOperations; i++) {
clientSocket.emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: `rapid-block-${i}`, type: 'action' },
timestamp: Date.now(),
const join2Promise = new Promise<void>((resolve) => {
client2.once('joined-workflow', () => resolve())
})
clientSocket.emit('join-workflow', { workflowId })
client2.emit('join-workflow', { workflowId })
await Promise.all([join1Promise, join2Promise])
let receivedCount = 0
const receivedOperations = new Set<string>()
const operationsPromise = new Promise<void>((resolve) => {
client2.on('workflow-operation', (data) => {
receivedCount++
receivedOperations.add(data.payload.id)
if (receivedCount === numOperations) {
resolve()
}
})
})
for (let i = 0; i < numOperations; i++) {
clientSocket.emit('workflow-operation', {
workflowId,
operation: 'add',
target: 'block',
payload: { id: `rapid-block-${i}`, type: 'action' },
timestamp: Date.now(),
})
}
await operationsPromise
expect(receivedCount).toBe(numOperations)
expect(receivedOperations.size).toBe(numOperations)
} finally {
client2.close()
}
await operationsPromise
expect(receivedCount).toBe(numOperations)
expect(receivedOperations.size).toBe(numOperations)
client2.close()
})
})

View File

@@ -1,5 +1,6 @@
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
import type { WorkflowState } from '../workflow/types'
const logger = createLogger('WorkflowJsonImporter')
@@ -60,10 +61,18 @@ function regenerateIds(workflowState: WorkflowState): WorkflowState {
})
}
// Fifth pass: update any block references in subblock values
// Fifth pass: update any block references in subblock values and clear runtime trigger values
Object.entries(newBlocks).forEach(([blockId, block]) => {
if (block.subBlocks) {
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => {
if (TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subBlockId)) {
block.subBlocks[subBlockId] = {
...subBlock,
value: null,
}
return
}
if (subBlock.value && typeof subBlock.value === 'string') {
// Replace any block references in the value
let updatedValue = subBlock.value

View File

@@ -6,7 +6,7 @@ import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { isAnnotationOnlyBlock } from '@/executor/consts'
import { isAnnotationOnlyBlock } from '@/executor/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import {

View File

@@ -1,4 +1,4 @@
import path, { resolve } from 'path'
import path from 'path'
/// <reference types="vitest" />
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
@@ -18,8 +18,23 @@ export default defineConfig({
include: ['**/*.test.{ts,tsx}'],
exclude: [...configDefaults.exclude, '**/node_modules/**', '**/dist/**'],
setupFiles: ['./vitest.setup.ts'],
alias: {
'@sim/db': resolve(__dirname, '../../packages/db'),
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
useAtomics: true,
isolate: true,
},
},
fileParallelism: true,
maxConcurrency: 20,
testTimeout: 10000,
deps: {
optimizer: {
web: {
enabled: true,
},
},
},
},
resolve: {

View File

@@ -17,7 +17,7 @@ import {
uuid,
vector,
} from 'drizzle-orm/pg-core'
import { DEFAULT_FREE_CREDITS, TAG_SLOTS } from './consts'
import { DEFAULT_FREE_CREDITS, TAG_SLOTS } from './constants'
// Custom tsvector type for full-text search
export const tsvector = customType<{