diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx index 4eab4ada664..110e77c5ac3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx @@ -414,151 +414,26 @@ describe('useWorkflowExecution cancellation', () => { }) }) -describe('useWorkflowExecution attachment uploads', () => { - beforeEach(() => { - vi.clearAllMocks() - mockEndScopedExecution.mockReset().mockReturnValue(true) - terminalStoreState._hasHydrated = false - executionStoreState.workflowExecutions.set('workflow-1', idleExecution) - executionStoreState.getWorkflowExecution.mockReturnValue(idleExecution) - executionStoreState.getCurrentExecutionId.mockReturnValue(null) - mockAdoptScopedExecution.mockReturnValue(undefined) - mockLoadExecutionPointer.mockResolvedValue(null) - mockReconnect.mockResolvedValue(undefined) - mockResolveStartCandidates.mockReturnValue([]) - mockSelectBestTrigger.mockReturnValue([]) - vi.stubGlobal('fetch', mockFetch) - mockUploadInternalFileSession.mockRejectedValue( - new Error('Workspace file storage limit exceeded') - ) - mockFetch.mockResolvedValue( - new Response(JSON.stringify({ error: 'Workspace file storage limit exceeded' }), { - status: 413, - headers: { 'Content-Type': 'application/json' }, - }) - ) - mockExecute.mockResolvedValue(undefined) - mockExecuteFromBlock.mockResolvedValue(undefined) - workflowStoreState.edges.length = 0 - }) - - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('does not execute and reports the exact server error when an explicit attachment fails', async () => { - const { result, unmount } = renderWorkflowExecutionHook() - const contextFile = new File(['context'], 'context.txt', { type: 'text/plain' }) - const file = new File(['report'], 'report.pdf', { type: 'application/pdf' }) - let uploadError: unknown - - mockUploadInternalFileSession.mockResolvedValueOnce({ - id: 'attachment-context', - key: 'executions/context.txt', - url: '/uploads/context.txt', - name: contextFile.name, - size: contextFile.size, - type: contextFile.type, - context: 'execution', - }) - - await act(async () => { - try { - await result().handleRunWorkflow({ - input: 'Summarize this report', - conversationId: 'conversation-1', - files: [ - { - name: contextFile.name, - size: contextFile.size, - type: contextFile.type, - file: contextFile, - }, - { - name: file.name, - size: file.size, - type: file.type, - file, - }, - ], - }) - } catch (error) { - uploadError = error - } - }) - - expect(uploadError).toBeInstanceOf(WorkflowAttachmentUploadError) - expect((uploadError as Error).message).toBe( - 'Failed to upload report.pdf: Workspace file storage limit exceeded' - ) - expect(mockExecute).not.toHaveBeenCalled() - - unmount() - }) - - it('returns uploaded metadata without mutating or leaking local input into execution', async () => { - const { result, unmount } = renderWorkflowExecutionHook() - const file = new File(['diagram'], 'diagram.png', { type: 'image/png' }) - const workflowInput = { - input: 'Describe this diagram', - conversationId: 'conversation-1', - files: [ - { - name: file.name, - size: file.size, - type: file.type, - file, - }, - ], - } - let runResult: unknown - - mockUploadInternalFileSession.mockResolvedValueOnce({ - id: 'attachment-diagram', - key: 'execution/diagram.png', - url: '/api/files/serve/execution%2Fdiagram.png', - name: file.name, - size: file.size, - type: file.type, - context: 'execution', - }) - - await act(async () => { - runResult = await result().handleRunWorkflow(workflowInput) - await drainStream(runResult) - }) - - expect(isChatWorkflowRunResult(runResult)).toBe(true) - if (!isChatWorkflowRunResult(runResult)) { - throw new Error('Expected a chat workflow run result') - } - expect(runResult.uploadedAttachments).toEqual([ - expect.objectContaining({ - name: 'diagram.png', - url: '/api/files/serve/execution%2Fdiagram.png', - size: file.size, - type: 'image/png', - key: 'execution/diagram.png', - }), - ]) - expect(workflowInput.files[0].file).toBe(file) - expect(mockExecute).toHaveBeenCalledWith( - expect.objectContaining({ - input: expect.objectContaining({ - input: 'Describe this diagram', - conversationId: 'conversation-1', - files: [ - expect.objectContaining({ - name: 'diagram.png', - url: '/api/files/serve/execution%2Fdiagram.png', - }), - ], - }), - }) - ) +function resetWorkflowExecutionTestState() { + vi.clearAllMocks() + mockBeginScopedExecution.mockReset().mockReturnValue({}) + mockAdoptScopedExecution.mockReset().mockReturnValue(undefined) + mockEndScopedExecution.mockReset().mockReturnValue(true) + mockLoadExecutionPointer.mockReset().mockResolvedValue(null) + mockReconnect.mockReset().mockResolvedValue(undefined) + mockResolveStartCandidates.mockReset().mockReturnValue([]) + mockSelectBestTrigger.mockReset().mockReturnValue([]) + mockExecute.mockReset().mockResolvedValue(undefined) + mockExecuteFromBlock.mockReset().mockResolvedValue(undefined) + terminalStoreState._hasHydrated = false + executionStoreState.workflowExecutions.set('workflow-1', idleExecution) + executionStoreState.getWorkflowExecution.mockReturnValue(idleExecution) + executionStoreState.getCurrentExecutionId.mockReturnValue(null) + workflowStoreState.edges.length = 0 +} - unmount() - }) +describe('useWorkflowExecution lifecycle ownership', () => { + beforeEach(resetWorkflowExecutionTestState) it('does not let an overlapping run without lifecycle ownership end the active run', async () => { const persistenceExecution = {} @@ -776,6 +651,140 @@ describe('useWorkflowExecution attachment uploads', () => { unmount() }) +}) + +describe('useWorkflowExecution attachment uploads', () => { + beforeEach(() => { + resetWorkflowExecutionTestState() + vi.stubGlobal('fetch', mockFetch) + mockUploadInternalFileSession.mockRejectedValue( + new Error('Workspace file storage limit exceeded') + ) + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ error: 'Workspace file storage limit exceeded' }), { + status: 413, + headers: { 'Content-Type': 'application/json' }, + }) + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('does not execute and reports the exact server error when an explicit attachment fails', async () => { + const { result, unmount } = renderWorkflowExecutionHook() + const contextFile = new File(['context'], 'context.txt', { type: 'text/plain' }) + const file = new File(['report'], 'report.pdf', { type: 'application/pdf' }) + let uploadError: unknown + + mockUploadInternalFileSession.mockResolvedValueOnce({ + id: 'attachment-context', + key: 'executions/context.txt', + url: '/uploads/context.txt', + name: contextFile.name, + size: contextFile.size, + type: contextFile.type, + context: 'execution', + }) + + await act(async () => { + try { + await result().handleRunWorkflow({ + input: 'Summarize this report', + conversationId: 'conversation-1', + files: [ + { + name: contextFile.name, + size: contextFile.size, + type: contextFile.type, + file: contextFile, + }, + { + name: file.name, + size: file.size, + type: file.type, + file, + }, + ], + }) + } catch (error) { + uploadError = error + } + }) + + expect(uploadError).toBeInstanceOf(WorkflowAttachmentUploadError) + expect((uploadError as Error).message).toBe( + 'Failed to upload report.pdf: Workspace file storage limit exceeded' + ) + expect(mockExecute).not.toHaveBeenCalled() + + unmount() + }) + + it('returns uploaded metadata without mutating or leaking local input into execution', async () => { + const { result, unmount } = renderWorkflowExecutionHook() + const file = new File(['diagram'], 'diagram.png', { type: 'image/png' }) + const workflowInput = { + input: 'Describe this diagram', + conversationId: 'conversation-1', + files: [ + { + name: file.name, + size: file.size, + type: file.type, + file, + }, + ], + } + let runResult: unknown + + mockUploadInternalFileSession.mockResolvedValueOnce({ + id: 'attachment-diagram', + key: 'execution/diagram.png', + url: '/api/files/serve/execution%2Fdiagram.png', + name: file.name, + size: file.size, + type: file.type, + context: 'execution', + }) + + await act(async () => { + runResult = await result().handleRunWorkflow(workflowInput) + await drainStream(runResult) + }) + + expect(isChatWorkflowRunResult(runResult)).toBe(true) + if (!isChatWorkflowRunResult(runResult)) { + throw new Error('Expected a chat workflow run result') + } + expect(runResult.uploadedAttachments).toEqual([ + expect.objectContaining({ + name: 'diagram.png', + url: '/api/files/serve/execution%2Fdiagram.png', + size: file.size, + type: 'image/png', + key: 'execution/diagram.png', + }), + ]) + expect(workflowInput.files[0].file).toBe(file) + expect(mockExecute).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ + input: 'Describe this diagram', + conversationId: 'conversation-1', + files: [ + expect.objectContaining({ + name: 'diagram.png', + url: '/api/files/serve/execution%2Fdiagram.png', + }), + ], + }), + }) + ) + + unmount() + }) it('uses only projected live thinking without changing normal settle behavior', async () => { mockExecute.mockImplementationOnce(async (options) => { diff --git a/apps/sim/ee/sso/components/sso-settings.test.tsx b/apps/sim/ee/sso/components/sso-settings.test.tsx index 259c0dd6d66..bf290898ac1 100644 --- a/apps/sim/ee/sso/components/sso-settings.test.tsx +++ b/apps/sim/ee/sso/components/sso-settings.test.tsx @@ -3,6 +3,7 @@ */ import { act, type ChangeEventHandler, type ReactNode } from 'react' import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { getErrorMessage } from '@sim/utils/errors' import { createRoot, type Root } from 'react-dom/client' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -98,6 +99,24 @@ vi.mock('@/components/settings/save-discard-actions', () => ({ vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({ SettingsEmptyState: ({ children }: { children?: ReactNode }) =>