diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 9c9f00984a..3c70935669 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4330,7 +4330,7 @@ export function PylonIcon(props: SVGProps) { viewBox='0 0 26 26' fill='none' > - + { const mockCreateErrorResponse = vi.fn() const mockEncryptSecret = vi.fn() const mockCheckChatAccess = vi.fn() - const mockGetSession = vi.fn() + const mockDeployWorkflow = vi.fn() beforeEach(() => { vi.resetModules() + // Set default return values + mockLimit.mockResolvedValue([]) mockSelect.mockReturnValue({ from: mockFrom }) mockFrom.mockReturnValue({ where: mockWhere }) mockWhere.mockReturnValue({ limit: mockLimit }) @@ -43,10 +45,6 @@ describe('Chat Edit API Route', () => { chat: { id: 'id', identifier: 'identifier', userId: 'userId' }, })) - vi.doMock('@/lib/auth', () => ({ - getSession: mockGetSession, - })) - vi.doMock('@/lib/logs/console/logger', () => ({ createLogger: vi.fn().mockReturnValue({ info: vi.fn(), @@ -86,6 +84,15 @@ describe('Chat Edit API Route', () => { vi.doMock('@/app/api/chat/utils', () => ({ checkChatAccess: mockCheckChatAccess, })) + + mockDeployWorkflow.mockResolvedValue({ success: true, version: 1 }) + vi.doMock('@/lib/workflows/db-helpers', () => ({ + deployWorkflow: mockDeployWorkflow, + })) + + vi.doMock('drizzle-orm', () => ({ + eq: vi.fn((field, value) => ({ field, value, type: 'eq' })), + })) }) afterEach(() => { @@ -94,20 +101,25 @@ describe('Chat Edit API Route', () => { describe('GET', () => { it('should return 401 when user is not authenticated', async () => { - mockGetSession.mockResolvedValueOnce(null) + vi.doMock('@/lib/auth', () => ({ + getSession: vi.fn().mockResolvedValue(null), + })) const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123') const { GET } = await import('@/app/api/chat/manage/[id]/route') const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(401) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401) + const data = await response.json() + expect(data.error).toBe('Unauthorized') }) it('should return 404 when chat not found or access denied', async () => { - mockGetSession.mockResolvedValueOnce({ - user: { id: 'user-id' }, - }) + vi.doMock('@/lib/auth', () => ({ + getSession: vi.fn().mockResolvedValue({ + user: { id: 'user-id' }, + }), + })) mockCheckChatAccess.mockResolvedValue({ hasAccess: false }) @@ -116,7 +128,8 @@ describe('Chat Edit API Route', () => { const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(404) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404) + const data = await response.json() + expect(data.error).toBe('Chat not found or access denied') expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id') }) @@ -143,15 +156,12 @@ describe('Chat Edit API Route', () => { const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(200) - expect(mockCreateSuccessResponse).toHaveBeenCalledWith({ - id: 'chat-123', - identifier: 'test-chat', - title: 'Test Chat', - description: 'A test chat', - customizations: { primaryColor: '#000000' }, - chatUrl: 'http://localhost:3000/chat/test-chat', - hasPassword: true, - }) + const data = await response.json() + expect(data.id).toBe('chat-123') + expect(data.identifier).toBe('test-chat') + expect(data.title).toBe('Test Chat') + expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat') + expect(data.hasPassword).toBe(true) }) }) @@ -169,7 +179,8 @@ describe('Chat Edit API Route', () => { const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(401) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401) + const data = await response.json() + expect(data.error).toBe('Unauthorized') }) it('should return 404 when chat not found or access denied', async () => { @@ -189,7 +200,8 @@ describe('Chat Edit API Route', () => { const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(404) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404) + const data = await response.json() + expect(data.error).toBe('Chat not found or access denied') expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id') }) @@ -205,9 +217,11 @@ describe('Chat Edit API Route', () => { identifier: 'test-chat', title: 'Test Chat', authType: 'public', + workflowId: 'workflow-123', } mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat }) + mockLimit.mockResolvedValueOnce([]) // No identifier conflict const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', { method: 'PATCH', @@ -218,11 +232,10 @@ describe('Chat Edit API Route', () => { expect(response.status).toBe(200) expect(mockUpdate).toHaveBeenCalled() - expect(mockCreateSuccessResponse).toHaveBeenCalledWith({ - id: 'chat-123', - chatUrl: 'http://localhost:3000/chat/test-chat', - message: 'Chat deployment updated successfully', - }) + const data = await response.json() + expect(data.id).toBe('chat-123') + expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat') + expect(data.message).toBe('Chat deployment updated successfully') }) it('should handle identifier conflicts', async () => { @@ -236,11 +249,15 @@ describe('Chat Edit API Route', () => { id: 'chat-123', identifier: 'test-chat', title: 'Test Chat', + workflowId: 'workflow-123', } mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat }) - // Mock identifier conflict - mockLimit.mockResolvedValueOnce([{ id: 'other-chat-id', identifier: 'new-identifier' }]) + + // Reset and reconfigure mockLimit to return the conflict + mockLimit.mockReset() + mockLimit.mockResolvedValue([{ id: 'other-chat-id', identifier: 'new-identifier' }]) + mockWhere.mockReturnValue({ limit: mockLimit }) const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', { method: 'PATCH', @@ -250,7 +267,8 @@ describe('Chat Edit API Route', () => { const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(400) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Identifier already in use', 400) + const data = await response.json() + expect(data.error).toBe('Identifier already in use') }) it('should validate password requirement for password auth', async () => { @@ -266,6 +284,7 @@ describe('Chat Edit API Route', () => { title: 'Test Chat', authType: 'public', password: null, + workflowId: 'workflow-123', } mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat }) @@ -278,10 +297,8 @@ describe('Chat Edit API Route', () => { const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(400) - expect(mockCreateErrorResponse).toHaveBeenCalledWith( - 'Password is required when using password protection', - 400 - ) + const data = await response.json() + expect(data.error).toBe('Password is required when using password protection') }) it('should allow access when user has workspace admin permission', async () => { @@ -296,10 +313,12 @@ describe('Chat Edit API Route', () => { identifier: 'test-chat', title: 'Test Chat', authType: 'public', + workflowId: 'workflow-123', } // User doesn't own chat but has workspace admin access mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat }) + mockLimit.mockResolvedValueOnce([]) // No identifier conflict const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', { method: 'PATCH', @@ -326,7 +345,8 @@ describe('Chat Edit API Route', () => { const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(401) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401) + const data = await response.json() + expect(data.error).toBe('Unauthorized') }) it('should return 404 when chat not found or access denied', async () => { @@ -345,7 +365,8 @@ describe('Chat Edit API Route', () => { const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(404) - expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404) + const data = await response.json() + expect(data.error).toBe('Chat not found or access denied') expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id') }) @@ -367,9 +388,8 @@ describe('Chat Edit API Route', () => { expect(response.status).toBe(200) expect(mockDelete).toHaveBeenCalled() - expect(mockCreateSuccessResponse).toHaveBeenCalledWith({ - message: 'Chat deployment deleted successfully', - }) + const data = await response.json() + expect(data.message).toBe('Chat deployment deleted successfully') }) it('should allow deletion when user has workspace admin permission', async () => { diff --git a/apps/sim/app/api/tools/custom/route.ts b/apps/sim/app/api/tools/custom/route.ts index b76d85896e..933d56dbc5 100644 --- a/apps/sim/app/api/tools/custom/route.ts +++ b/apps/sim/app/api/tools/custom/route.ts @@ -175,7 +175,8 @@ export async function POST(req: NextRequest) { } } catch (error) { logger.error(`[${requestId}] Error updating custom tools`, error) - return NextResponse.json({ error: 'Failed to update custom tools' }, { status: 500 }) + const errorMessage = error instanceof Error ? error.message : 'Failed to update custom tools' + return NextResponse.json({ error: errorMessage }, { status: 500 }) } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx index 432fcc7b83..d6e4e771f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx @@ -97,11 +97,9 @@ export function CodeEditor({ return () => resizeObserver.disconnect() }, [code]) - // Calculate the number of lines to determine gutter width const lineCount = code.split('\n').length const gutterWidth = calculateGutterWidth(lineCount) - // Render helpers const renderLineNumbers = () => { const numbers: ReactElement[] = [] let lineNumber = 1 @@ -127,88 +125,41 @@ export function CodeEditor({ return numbers } - // Custom highlighter that highlights environment variables and tags const customHighlight = (code: string) => { if (!highlightVariables || language !== 'javascript') { - // Use default Prism highlighting for non-JS or when variable highlighting is off return highlight(code, languages[language], language) } - // First, get the default Prism highlighting - let highlighted = highlight(code, languages[language], language) + const placeholders: Array<{ placeholder: string; original: string; type: 'env' | 'param' }> = [] + let processedCode = code - // Collect all syntax highlights to apply in a single pass - type SyntaxHighlight = { - start: number - end: number - replacement: string - } - const highlights: SyntaxHighlight[] = [] - - // Find environment variables with {{var_name}} syntax - let match - const envVarRegex = /\{\{([^}]+)\}\}/g - while ((match = envVarRegex.exec(highlighted)) !== null) { - highlights.push({ - start: match.index, - end: match.index + match[0].length, - replacement: `${match[0]}`, - }) - } - - // Find tags with syntax (not in HTML context) - if (!language.includes('html')) { - const tagRegex = /<([^>\s/]+)>/g - while ((match = tagRegex.exec(highlighted)) !== null) { - // Skip HTML comments and closing tags - if (!match[0].startsWith('