diff --git a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue index 30a070ed04d..2582e11fc64 100644 --- a/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue +++ b/packages/frontend/editor-ui/src/features/execution/logs/components/LogsPanel.vue @@ -57,7 +57,7 @@ const { onOverviewPanelResizeEnd, } = useLogsPanelLayout(workflowName, popOutContainer, popOutContent, container, logsContainer); -const { currentSessionId, messages, refreshSession, displayExecution } = useChatState( +const { currentSessionId, chatOptions, refreshSession, displayExecution } = useChatState( props.isReadOnly, ); @@ -168,7 +168,7 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) { >
; - messages: ComputedRef; previousChatMessages: ComputedRef; refreshSession: () => void; displayExecution: (executionId: string) => void; @@ -261,7 +260,7 @@ export function useChatState( messageComponents: { [MessageComponentKey.WITH_BUTTONS]: MessageWithButtons, }, - messageHistory: messages.value, + messageHistory: isReadOnly ? restoredChatMessages.value : messages.value, disabled: ref(isReadOnly), i18n: { en: { @@ -366,9 +365,6 @@ export function useChatState( return { currentSessionId: computed(() => logsStore.chatSessionId), - messages: computed(() => - isReadOnly ? restoredChatMessages.value : logsStore.chatSessionMessages, - ), previousChatMessages, refreshSession, displayExecution, diff --git a/packages/testing/playwright/expectations/execution.logs/1777281716392-unknown-host-POST-_v1_messages-3002af3d.json b/packages/testing/playwright/expectations/execution.logs/1777281716392-unknown-host-POST-_v1_messages-3002af3d.json new file mode 100644 index 00000000000..4628ff0196a --- /dev/null +++ b/packages/testing/playwright/expectations/execution.logs/1777281716392-unknown-host-POST-_v1_messages-3002af3d.json @@ -0,0 +1,120 @@ +{ + "httpRequest": { + "method": "POST", + "path": "/v1/messages", + "body": { + "contentType": "application/json", + "type": "JSON", + "json": { + "model": "claude-sonnet-4-5-20250929", + "stream": true, + "max_tokens": 8192, + "thinking": { + "type": "disabled" + }, + "messages": [ + { + "role": "user", + "content": "Hi!" + } + ] + }, + "rawBytes": "eyJtb2RlbCI6ImNsYXVkZS1zb25uZXQtNC01LTIwMjUwOTI5Iiwic3RyZWFtIjp0cnVlLCJtYXhfdG9rZW5zIjo4MTkyLCJ0aGlua2luZyI6eyJ0eXBlIjoiZGlzYWJsZWQifSwibWVzc2FnZXMiOlt7InJvbGUiOiJ1c2VyIiwiY29udGVudCI6IkhpISJ9XX0=" + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-envoy-upstream-service-time": [ + "1494" + ], + "strict-transport-security": [ + "max-age=31536000; includeSubDomains; preload" + ], + "set-cookie": [ + "_cfuvid=JjjjTwlTrrDFqgfRsM9he9mrHuj9XhEgxXbsiBhNeiE-1777281698.7332523-1.0.1.1-OyArYqG84afkWTE3QLvX8_xQHOn90JMyDIjk0jbx7qQ; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com" + ], + "request-id": [ + "req_011CaU2iSzpUtRdjtvzUYN8p" + ], + "cf-cache-status": [ + "DYNAMIC" + ], + "anthropic-ratelimit-tokens-reset": [ + "2026-04-27T09:21:38Z" + ], + "anthropic-ratelimit-tokens-remaining": [ + "27000000" + ], + "anthropic-ratelimit-tokens-limit": [ + "27000000" + ], + "anthropic-ratelimit-requests-reset": [ + "2026-04-27T09:21:38Z" + ], + "anthropic-ratelimit-requests-remaining": [ + "19998" + ], + "anthropic-ratelimit-requests-limit": [ + "20000" + ], + "anthropic-ratelimit-output-tokens-reset": [ + "2026-04-27T09:21:38Z" + ], + "anthropic-ratelimit-output-tokens-remaining": [ + "4500000" + ], + "anthropic-ratelimit-output-tokens-limit": [ + "4500000" + ], + "anthropic-ratelimit-input-tokens-reset": [ + "2026-04-27T09:21:38Z" + ], + "anthropic-ratelimit-input-tokens-remaining": [ + "22500000" + ], + "anthropic-ratelimit-input-tokens-limit": [ + "22500000" + ], + "X-Robots-Tag": [ + "none" + ], + "Server": [ + "cloudflare" + ], + "Date": [ + "Mon, 27 Apr 2026 09:21:40 GMT" + ], + "Content-Type": [ + "text/event-stream; charset=utf-8" + ], + "Content-Security-Policy": [ + "default-src 'none'; frame-ancestors 'none'" + ], + "Cache-Control": [ + "no-cache" + ], + "CF-RAY": [ + "9f2cc2191dd9e527-TXL" + ] + }, + "cookies": { + "_cfuvid": "JjjjTwlTrrDFqgfRsM9he9mrHuj9XhEgxXbsiBhNeiE-1777281698.7332523-1.0.1.1-OyArYqG84afkWTE3QLvX8_xQHOn90JMyDIjk0jbx7qQ" + }, + "body": { + "type": "STRING", + "string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01Np2KsYNwWYH8AoYtUy4UZA\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":9,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":8,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello! How can I help you today\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"?\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":9,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":12} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n", + "rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOSIsImlkIjoibXNnXzAxTnAyS3NZTndXWUg4QW9ZdFV5NFVaQSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo4LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19ICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiSGVsbG8hIEhvdyBjYW4gSSBoZWxwIHlvdSB0b2RheSJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ij8ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjEyfSAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgfQoK", + "contentType": "text/event-stream; charset=utf-8" + } + }, + "id": "1777281716392-unknown-host-POST-_v1_messages-3002af3d.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} \ No newline at end of file diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/fixtures.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/fixtures.ts new file mode 100644 index 00000000000..5bf31fee0b1 --- /dev/null +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/fixtures.ts @@ -0,0 +1,70 @@ +import { test as base, expect as baseExpect } from '../../../../../fixtures/base'; +import type { CredentialResponse } from '../../../../../services/credential-api-helper'; + +const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY ?? 'mock-anthropic-api-key'; + +type ExecutionLogFixtures = { + anthropicCredential: CredentialResponse; + anthropicApiKey: string; + proxySetup: undefined; +}; + +export const executionLogsTestConfig = { + capability: { + services: ['proxy'], + env: { + N8N_COMMUNITY_PACKAGES_ENABLED: 'false', // suppress extraneous HTTP requests + }, + }, +} as const; + +export const test = base.extend({ + anthropicApiKey: async ({}, use) => { + await use(ANTHROPIC_API_KEY); + }, + + proxySetup: [ + async ({ services }, use) => { + // Setup + await services.proxy.clearAllExpectations(); + await services.proxy.loadExpectations('execution.logs', { strictBodyMatching: true }); + + await use(undefined); + + // Teardown + if (!process.env.CI) { + await services.proxy.recordExpectations('execution.logs', { + dedupe: true, + transform: (expectation) => { + const response = expectation.httpResponse as { + headers?: Record; + }; + + if (response?.headers) { + delete response.headers['anthropic-organization-id']; + } + + return expectation; + }, + }); + } + }, + { auto: true }, + ], + + anthropicCredential: async ({ n8n, anthropicApiKey }, use) => { + const res = await n8n.api.credentials.createCredential({ + name: `Anthropic cred ${crypto.randomUUID().slice(0, 8)}`, + type: 'anthropicApi', + data: { + apiKey: anthropicApiKey, + }, + }); + + await use(res); + + await n8n.api.credentials.deleteCredential(res.id); + }, +}); + +export const expect = baseExpect; diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts index 59101e13c21..fae48a32936 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts @@ -1,4 +1,6 @@ -import { test, expect } from '../../../../../fixtures/base'; +import { test, expect, executionLogsTestConfig } from './fixtures'; + +test.use(executionLogsTestConfig); // Node name constants const NODES = { @@ -101,58 +103,50 @@ test.describe( await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toContainText(NODES.IF); }); - // TODO: make it possible to test workflows with AI model end-to-end - test.fixme( - 'should show input and output data in the selected display mode', - async ({ n8n, setupRequirements }) => { - await setupRequirements({ workflow: 'Workflow_ai_agent.json' }); + test('should show input and output data in the selected display mode', async ({ + n8n, + anthropicCredential, + setupRequirements, + }) => { + await setupRequirements({ workflow: 'Workflow_ai_agent.json' }); - await n8n.canvas.clickZoomToFitButton(); - await n8n.canvas.logsPanel.open(); - await n8n.canvas.logsPanel.sendManualChatMessage('Hi!'); - await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful'); - await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!'); - await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(1)).toContainText( - 'Hello from e2e model!!!', - ); - await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toHaveText('E2E Chat Model'); - await n8n.canvas.logsPanel.getLogEntries().nth(2).click(); + await n8n.canvas.clickZoomToFitButton(); + await n8n.canvas.openNode('E2E Chat Model'); + await expect(n8n.ndv.getCredentialSelect()).toHaveValue(anthropicCredential.name); + await n8n.ndv.close(); + await n8n.canvas.logsPanel.open(); + await n8n.canvas.logsPanel.sendManualChatMessage('Hi!'); + await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!'); + await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(1)).toContainText('Hello'); + await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toHaveText('E2E Chat Model'); - await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText( - 'Hello from e2e model!!!', - ); - await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('table'); - await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 0)).toContainText( - 'text:Hello from **e2e** model!!!', - ); - await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText( - 'completionTokens:20', - ); - await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('schema'); - await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('generations[0]'); - await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText( - 'Hello from **e2e** model!!!', - ); - await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('json'); - await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText( - '[{"response": {"generations": [', - ); + await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('Hello'); + await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('table'); + await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 0)).toContainText('text:Hello'); + await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText( + /completionTokens:\d+promptTokens:\d+totalTokens:\d+/, + ); + await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('schema'); + await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('generations[0]'); + await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('Hello'); + await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('json'); + await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText( + /\[\s*{\s*"response":\s*{\s*"generations":\s*\[/, + ); - await n8n.canvas.logsPanel.toggleInputPanel(); - await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!'); - await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('table'); - await expect(n8n.canvas.logsPanel.inputPanel.getTbodyCell(0, 0)).toContainText( - '0:Human: Hi!', - ); - await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('schema'); - await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('messages[0]'); - await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!'); - await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('json'); - await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText( - '[{"messages": ["Human: Hi!"],', - ); - }, - ); + await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!'); + await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('table'); + await expect(n8n.canvas.logsPanel.inputPanel.getTbodyCell(0, 0)).toContainText( + '0:Human: Hi!', + ); + await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('schema'); + await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('messages[0]'); + await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!'); + await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('json'); + await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText( + /\[\s*{\s*"messages":\s*\[\s*"Human: Hi!"\s*\],/, + ); + }); test('should show input and output data of correct run index and branch', async ({ n8n, @@ -223,21 +217,25 @@ test.describe( await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(6); }); - // TODO: make it possible to test workflows with AI model end-to-end - test.fixme('should show logs for a past execution', async ({ n8n, setupRequirements }) => { + test('should show logs for a past execution', async ({ + n8n, + anthropicCredential, + setupRequirements, + }) => { await setupRequirements({ workflow: 'Workflow_ai_agent.json' }); await n8n.canvas.clickZoomToFitButton(); + await n8n.canvas.openNode('E2E Chat Model'); + await expect(n8n.ndv.getCredentialSelect()).toHaveValue(anthropicCredential.name); + await n8n.ndv.close(); await n8n.canvas.logsPanel.open(); await n8n.canvas.logsPanel.sendManualChatMessage('Hi!'); - await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful'); + await n8n.notifications.waitForNotificationAndClose('Successful'); await n8n.canvas.openExecutions(); await n8n.executions.getAutoRefreshButton().click(); await expect(n8n.executions.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!'); - await expect(n8n.executions.logsPanel.getManualChatMessages().nth(1)).toContainText( - 'Hello from e2e model!!!', - ); + await expect(n8n.executions.logsPanel.getManualChatMessages().nth(1)).toContainText('Hello'); await expect( n8n.executions.logsPanel.getOverviewStatus().filter({ hasText: /Success in [\d.]+m?s/ }), ).toBeVisible(); diff --git a/packages/testing/playwright/workflows/Workflow_ai_agent.json b/packages/testing/playwright/workflows/Workflow_ai_agent.json index 30e4cb34786..f281d2d68f0 100644 --- a/packages/testing/playwright/workflows/Workflow_ai_agent.json +++ b/packages/testing/playwright/workflows/Workflow_ai_agent.json @@ -23,10 +23,16 @@ }, { "parameters": { - "response": "Hello from **e2e** model!!!" + "model": { + "__rl": true, + "mode": "list", + "value": "claude-sonnet-4-5-20250929", + "cachedResultName": "Claude Sonnet 4.5" + }, + "options": {} }, - "type": "@n8n/n8n-nodes-langchain.lmChatE2eTest", - "typeVersion": 1, + "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic", + "typeVersion": 1.3, "position": [308, 220], "id": "2f239d5b-95ef-4949-92b6-5a7541e1029f", "name": "E2E Chat Model"