diff --git a/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.test.ts b/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.test.ts index a222ae049e8..ccbcf945ad5 100644 --- a/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.test.ts +++ b/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.test.ts @@ -7,6 +7,7 @@ interface VueComponentInstance { setupState?: { onUserMessage?: (message: string) => Promise; showAskOwnerTooltip?: boolean; + showExecuteMessage?: boolean; }; }; } @@ -22,6 +23,17 @@ vi.mock('@/composables/useWorkflowSaving', () => ({ }), })); +// Mock ExecuteMessage component +vi.mock('./ExecuteMessage.vue', () => ({ + default: defineComponent({ + name: 'ExecuteMessage', + emits: ['workflow-executed'], + setup() { + return () => h('div', { 'data-test-id': 'execute-message-component' }, 'Execute and refine'); + }, + }), +})); + // Mock AskAssistantChat component vi.mock('@n8n/design-system/components/AskAssistantChat/AskAssistantChat.vue', () => ({ default: defineComponent({ @@ -36,7 +48,7 @@ vi.mock('@n8n/design-system/components/AskAssistantChat/AskAssistantChat.vue', ( 'showAskOwnerTooltip', ], emits: ['message', 'feedback', 'stop', 'upgrade-click'], - setup(props, { emit, expose }) { + setup(props, { emit, expose, slots }) { const feedbackText = { value: '' }; const sendMessage = (message: string) => { @@ -44,7 +56,7 @@ vi.mock('@n8n/design-system/components/AskAssistantChat/AskAssistantChat.vue', ( }; expose({ sendMessage }); - // Create a more realistic mock that includes rating buttons when needed + // Create a more realistic mock that includes rating buttons and slots when needed return () => { const lastMessage = props.messages?.[props.messages.length - 1]; const showRating = lastMessage?.showRating; @@ -74,6 +86,8 @@ vi.mock('@n8n/design-system/components/AskAssistantChat/AskAssistantChat.vue', ( }), ] : null, + // Render messagesFooter slot if it exists + slots.messagesFooter?.(), ]); }; }, @@ -1118,6 +1132,175 @@ describe('AskAssistantBuild', () => { }); }); + describe('Execute and refine section visibility', () => { + it('should hide ExecuteMessage component when there is an error after workflow update', async () => { + // Setup: workflow with nodes + workflowsStore.$patch({ + workflow: { + nodes: [ + { + id: 'node1', + name: 'Start', + type: 'n8n-nodes-base.start', + position: [0, 0], + typeVersion: 1, + parameters: {}, + } as INodeUi, + ], + connections: {}, + }, + }); + + const { queryByTestId } = renderComponent(); + + // Simulate workflow update message followed by error + builderStore.$patch({ + streaming: false, + chatMessages: [ + { id: '1', role: 'user', type: 'text', content: 'Create a workflow' }, + { + id: '2', + role: 'assistant', + type: 'workflow-updated', + codeSnippet: JSON.stringify({ nodes: [], connections: {} }), + }, + { id: '3', role: 'assistant', type: 'error', content: 'An error occurred' }, + ], + }); + + await flushPromises(); + + // Verify the ExecuteMessage component should NOT be rendered + expect(queryByTestId('execute-message-component')).not.toBeInTheDocument(); + }); + + it('should show ExecuteMessage component when there is NO error after workflow update', async () => { + // Setup: workflow with nodes + workflowsStore.$patch({ + workflow: { + nodes: [ + { + id: 'node1', + name: 'Start', + type: 'n8n-nodes-base.start', + position: [0, 0], + typeVersion: 1, + parameters: {}, + } as INodeUi, + ], + connections: {}, + }, + }); + + const { queryByTestId } = renderComponent(); + + // Simulate workflow update message WITHOUT error + builderStore.$patch({ + streaming: false, + chatMessages: [ + { id: '1', role: 'user', type: 'text', content: 'Create a workflow' }, + { + id: '2', + role: 'assistant', + type: 'workflow-updated', + codeSnippet: JSON.stringify({ nodes: [], connections: {} }), + }, + { id: '3', role: 'assistant', type: 'text', content: 'Workflow created successfully' }, + ], + }); + + await flushPromises(); + + // Verify the ExecuteMessage component SHOULD be rendered + expect(queryByTestId('execute-message-component')).toBeInTheDocument(); + }); + + it('should show ExecuteMessage component when error occurs BEFORE workflow update', async () => { + // Setup: workflow with nodes + workflowsStore.$patch({ + workflow: { + nodes: [ + { + id: 'node1', + name: 'Start', + type: 'n8n-nodes-base.start', + position: [0, 0], + typeVersion: 1, + parameters: {}, + } as INodeUi, + ], + connections: {}, + }, + }); + + const { queryByTestId } = renderComponent(); + + // Simulate error BEFORE workflow update + builderStore.$patch({ + streaming: false, + chatMessages: [ + { id: '1', role: 'user', type: 'text', content: 'Create a workflow' }, + { id: '2', role: 'assistant', type: 'error', content: 'Initial error' }, + { + id: '3', + role: 'assistant', + type: 'workflow-updated', + codeSnippet: JSON.stringify({ nodes: [], connections: {} }), + }, + { id: '4', role: 'assistant', type: 'text', content: 'Recovered and created workflow' }, + ], + }); + + await flushPromises(); + + // Verify the ExecuteMessage component SHOULD be rendered because error was before workflow update + expect(queryByTestId('execute-message-component')).toBeInTheDocument(); + }); + + it('should hide ExecuteMessage component when using update_node_parameters tool followed by error', async () => { + // Setup: workflow with nodes + workflowsStore.$patch({ + workflow: { + nodes: [ + { + id: 'node1', + name: 'HTTP Request', + type: 'n8n-nodes-base.httpRequest', + position: [0, 0], + typeVersion: 1, + parameters: {}, + } as INodeUi, + ], + connections: {}, + }, + }); + + const { queryByTestId } = renderComponent(); + + // Simulate update_node_parameters tool call followed by error + builderStore.$patch({ + streaming: false, + chatMessages: [ + { id: '1', role: 'user', type: 'text', content: 'Update the HTTP node parameters' }, + { + id: '2', + role: 'assistant', + type: 'tool', + toolName: 'update_node_parameters', + status: 'completed', + updates: [], + }, + { id: '3', role: 'assistant', type: 'error', content: 'Failed to update parameters' }, + ], + }); + + await flushPromises(); + + // Verify the ExecuteMessage component should NOT be rendered + expect(queryByTestId('execute-message-component')).not.toBeInTheDocument(); + }); + }); + it('should handle multiple canvas generations correctly', async () => { const originalWorkflow = { nodes: [], diff --git a/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.vue b/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.vue index 0575c6f7074..e4922b69caf 100644 --- a/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.vue +++ b/packages/frontend/editor-ui/src/features/assistant/components/Agent/AskAssistantBuild.vue @@ -60,10 +60,17 @@ const showExecuteMessage = computed(() => { msg.type === 'workflow-updated' || (msg.type === 'tool' && msg.toolName === 'update_node_parameters'), ); + + // Check if there's an error message after the last workflow update + const hasErrorAfterUpdate = builderStore.chatMessages + .slice(builderUpdatedWorkflowMessageIndex + 1) + .some((msg) => msg.type === 'error'); + return ( !builderStore.streaming && workflowsStore.workflow.nodes.length > 0 && - builderUpdatedWorkflowMessageIndex > -1 + builderUpdatedWorkflowMessageIndex > -1 && + !hasErrorAfterUpdate ); }); const creditsQuota = computed(() => builderStore.creditsQuota);