first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { test, expect, chatHubTestConfig } from './fixtures';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('File attachment @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
let tmpDir: string;
|
||||
let testImagePath: string;
|
||||
let testTextPath: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-hub-test-'));
|
||||
testImagePath = path.join(tmpDir, 'test-image.png');
|
||||
testTextPath = path.join(tmpDir, 'test-file.txt');
|
||||
|
||||
// 100x100 solid red square PNG
|
||||
const pngBuffer = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAkElEQVR42u3QMQ0AAAjAsPk3DRb4eJpUQZviSIEsWbJkyUKBLFmyZMlCgSxZsmTJQoEsWbJkyUKBLFmyZMlCgSxZsmTJQoEsWbJkyUKBLFmyZMlCgSxZsmTJQoEsWbJkyUKBLFmyZMlCgSxZsmTJQoEsWbJkyUKBLFmyZMlCgSxZsmTJQoEsWbJkyUKBLFnvFp4t6yugc3LNAAAAAElFTkSuQmCC',
|
||||
'base64',
|
||||
);
|
||||
await fs.writeFile(testImagePath, pngBuffer);
|
||||
|
||||
// Simple text file
|
||||
await fs.writeFile(testTextPath, 'I am a file');
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (tmpDir) {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('image attachment', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i); // auto-select a model
|
||||
|
||||
await page.getFileInput().setInputFiles(testImagePath);
|
||||
await page.getChatInput().fill('What color is this image? Reply with just the color name.');
|
||||
await page.getSendButton().click();
|
||||
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('What color is this image?');
|
||||
await expect(page.getAttachmentsAt(0)).toHaveCount(1);
|
||||
await expect(page.getAttachmentsAt(0).nth(0)).toBeVisible();
|
||||
await expect(page.getChatMessages().nth(1)).toContainText(/red/i);
|
||||
|
||||
// Verify image is persisted and can be opened in new tab
|
||||
await n8n.page.reload();
|
||||
const newPage = await page.openAttachmentAt(0, 0);
|
||||
|
||||
await expect(newPage.locator('img')).toBeVisible();
|
||||
await expect(newPage.locator('img')).toHaveJSProperty('naturalWidth', 100);
|
||||
await expect(newPage.locator('img')).toHaveJSProperty('naturalHeight', 100);
|
||||
await newPage.close();
|
||||
});
|
||||
|
||||
test('text file attachment', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i);
|
||||
|
||||
await page.getFileInput().setInputFiles(testTextPath);
|
||||
await page.getChatInput().fill('What is the exact content of this file?');
|
||||
await page.getSendButton().click();
|
||||
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('What is the exact content');
|
||||
await expect(page.getAttachmentsAt(0)).toHaveCount(1);
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('I am a file');
|
||||
});
|
||||
|
||||
test('reference attachment in subsequent message', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i);
|
||||
|
||||
// Send initial message with attachment
|
||||
await page.getFileInput().setInputFiles(testImagePath);
|
||||
await page.getChatInput().fill('What color is this image? Reply with just the color name.');
|
||||
await page.getSendButton().click();
|
||||
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('What color is this image?');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText(/red/i);
|
||||
|
||||
// Send follow-up question referencing the image
|
||||
await page.getChatInput().fill('Does the image include text? Reply just yes or no.');
|
||||
await page.getSendButton().click();
|
||||
|
||||
await expect(page.getChatMessages().nth(2)).toContainText('Does the image include text?');
|
||||
await expect(page.getChatMessages().nth(3)).toContainText(/no/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { test, expect, chatHubTestConfig } from './fixtures';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
import { CredentialModal } from '../../../pages/components/CredentialModal';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Basic conversation @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('new chat with pre-configured credentials', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
|
||||
await expect(page.getGreetingMessage()).toContainText('Start a chat with');
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i); // pre-selected
|
||||
|
||||
await page.getChatInput().fill('Hello');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('Hello');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hello! How can I help you today?');
|
||||
await expect(page.sidebar.getConversations().first()).toHaveAccessibleName(/greeting/i); // verify auto-generated title
|
||||
});
|
||||
|
||||
// Test with a different user to avoid race condition on credentials
|
||||
test('new chat without pre-configured credentials @auth:member', async ({
|
||||
n8n,
|
||||
anthropicApiKey,
|
||||
}) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
const credModal = new CredentialModal(n8n.page.getByTestId('editCredential-modal'));
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
|
||||
await expect(page.getGreetingMessage()).toContainText('Select a model to start chatting');
|
||||
|
||||
await page.getModelSelectorButton().click();
|
||||
await n8n.page.waitForTimeout(500); // to reliably hover intended menu item
|
||||
await page.getVisiblePopoverMenuItem('Anthropic').hover({ force: true });
|
||||
await page.getVisiblePopoverMenuItem('Configure credentials', { exact: true }).click();
|
||||
|
||||
await credModal.fillField('apiKey', anthropicApiKey);
|
||||
await credModal.save();
|
||||
await credModal.close();
|
||||
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i); // auto-select a model
|
||||
|
||||
await page.getChatInput().fill('Hello from e2e');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('Hello from e2e');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hello!');
|
||||
await expect(page.sidebar.getConversations().first()).toHaveAccessibleName(/greeting/i); // verify auto-generated title
|
||||
});
|
||||
|
||||
test('conversation flow', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i); // auto-select a model
|
||||
|
||||
// STEP: send first prompt
|
||||
await page.getChatInput().fill('Hi');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toHaveText('Hi');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hi there!');
|
||||
await expect(page.getChatMessages()).toHaveCount(2);
|
||||
|
||||
// STEP: send 2nd prompt
|
||||
await page.getChatInput().fill('How are you?');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('Hi');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hi there!');
|
||||
await expect(page.getChatMessages().nth(2)).toContainText('How are you?');
|
||||
await expect(page.getChatMessages().nth(3)).toContainText("I'm doing well");
|
||||
await expect(page.getChatMessages()).toHaveCount(4);
|
||||
|
||||
// STEP: regenerate response to first prompt
|
||||
await page.clickRegenerateButtonAt(1);
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hello!');
|
||||
await expect(page.getChatMessages()).toHaveCount(2);
|
||||
|
||||
// STEP: switch to previous alternative
|
||||
await page.clickPrevAlternativeButtonAt(1);
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hi there!');
|
||||
await expect(page.getChatMessages()).toHaveCount(4);
|
||||
|
||||
// STEP: edit 2nd prompt
|
||||
await page.clickEditButtonAt(2);
|
||||
await page.getEditorAt(2).fill('Hola');
|
||||
await page.getSendButtonAt(2).click();
|
||||
await expect(page.getChatMessages().nth(3)).toContainText('¡Hola!');
|
||||
await expect(page.getChatMessages()).toHaveCount(4);
|
||||
|
||||
// STEP: reload page and verify persistence
|
||||
await n8n.page.reload();
|
||||
await expect(page.getChatMessages()).toHaveCount(4);
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('Hi');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hi there!');
|
||||
await expect(page.getChatMessages().nth(2)).toContainText('Hola');
|
||||
await expect(page.getChatMessages().nth(3)).toContainText('¡Hola!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { chatHubTestConfig, expect, test } from './fixtures';
|
||||
import { INSTANCE_OWNER_CREDENTIALS } from '../../../config/test-users';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Chat user role @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('use chat as chat user @auth:chat', async ({ n8n, anthropicApiKey }) => {
|
||||
const ownerN8n = await n8n.start.withUser(INSTANCE_OWNER_CREDENTIALS);
|
||||
|
||||
// Create global credential as owner
|
||||
const cred = await ownerN8n.api.credentials.createCredential({
|
||||
name: 'Global Anthropic API key',
|
||||
type: 'anthropicApi',
|
||||
isGlobal: true,
|
||||
data: {
|
||||
apiKey: anthropicApiKey,
|
||||
},
|
||||
});
|
||||
|
||||
await n8n.goHome();
|
||||
await n8n.page.waitForURL('/home/chat'); // home is chat UI for chat users
|
||||
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
// Verify global credential is available and pre-selected
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i); // pre-selected
|
||||
await expect(page.getSelectedCredentialName()).toHaveText('Global Anthropic API key');
|
||||
|
||||
await page.getModelSelectorButton().click();
|
||||
await page.getVisiblePopoverMenuItem('Anthropic').click();
|
||||
await page.getVisiblePopoverMenuItem('Configure credentials', { exact: true }).click();
|
||||
|
||||
// Verify other credentials are not available and cannot be created
|
||||
await page.credModal.getCredentialSelector().click();
|
||||
await expect(page.credModal.getCreateButton()).toBeDisabled();
|
||||
await expect(page.credModal.getVisiblePopoverOption()).toHaveCount(1);
|
||||
await expect(page.credModal.getVisiblePopoverOption().nth(0)).toContainText(
|
||||
'Global Anthropic API key',
|
||||
);
|
||||
await page.credModal.getCloseButton().click();
|
||||
|
||||
// Send chat message
|
||||
await page.getChatInput().fill('Hi');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toHaveText('Hi');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText('Hi there!');
|
||||
await expect(page.getChatMessages()).toHaveCount(2);
|
||||
|
||||
await ownerN8n.api.credentials.deleteCredential(cred.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { test, expect, chatHubTestConfig } from './fixtures';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
import { ChatHubPersonalAgentsPage } from '../../../pages/ChatHubPersonalAgentsPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Personal agent @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('create personal agent and start conversation @auth:owner', async ({
|
||||
n8n,
|
||||
anthropicCredential: _,
|
||||
}) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
|
||||
await page.getModelSelectorButton().click();
|
||||
await n8n.page.waitForTimeout(500); // to reliably hover intended menu item
|
||||
await page.getVisiblePopoverMenuItem('Personal agents').hover({ force: true });
|
||||
await page.getVisiblePopoverMenuItem('New Agent', { exact: true }).click();
|
||||
|
||||
await page.personalAgentModal.getNameField().fill('e2e agent');
|
||||
await page.personalAgentModal.getDescriptionField().fill('just for testing');
|
||||
await page.personalAgentModal.getSystemPromptField().fill('reply in Chinese');
|
||||
|
||||
await page.personalAgentModal.getModelSelectorButton().click();
|
||||
await n8n.page.waitForTimeout(500); // to reliably hover intended menu item
|
||||
await page.getVisiblePopoverMenuItem('Anthropic').hover({ force: true });
|
||||
await page.getVisiblePopoverMenuItem('Claude Opus 4.5', { exact: true }).click();
|
||||
|
||||
await page.personalAgentModal.getSaveButton().click();
|
||||
await expect(page.personalAgentModal.getRoot()).not.toBeInViewport(); // wait for modal to close
|
||||
|
||||
await expect(page.getModelSelectorButton()).toContainText('e2e agent');
|
||||
|
||||
await page.getChatInput().fill('Hello');
|
||||
await page.getSendButton().click();
|
||||
|
||||
await expect(page.getChatMessages().last()).toContainText('你好');
|
||||
});
|
||||
|
||||
test('manage personal agents @auth:admin', async ({ n8n, anthropicCredential: _ }) => {
|
||||
const page = new ChatHubPersonalAgentsPage(n8n.page);
|
||||
const chatPage = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHubPersonalAgents();
|
||||
await page.getNewAgentButton().click();
|
||||
|
||||
// STEP: create an agent
|
||||
await page.editModal.getNameField().fill('e2e agent');
|
||||
await page.editModal.getDescriptionField().fill('just for testing');
|
||||
await page.editModal.getSystemPromptField().fill('reply in Chinese');
|
||||
|
||||
await page.editModal.getModelSelectorButton().click();
|
||||
await n8n.page.waitForTimeout(500); // to reliably hover intended menu item
|
||||
await page.editModal.getVisiblePopoverMenuItem('Anthropic').hover({ force: true });
|
||||
await page.editModal.getVisiblePopoverMenuItem('Claude Opus 4.5', { exact: true }).click();
|
||||
|
||||
await page.editModal.getSaveButton().click();
|
||||
await expect(page.editModal.getRoot()).not.toBeInViewport(); // wait for modal to close
|
||||
await expect(page.getAgentCards()).toHaveCount(1);
|
||||
await expect(page.getAgentCards().nth(0)).toContainText('e2e agent');
|
||||
|
||||
// STEP: send message to the created agent
|
||||
await page.getAgentCards().nth(0).click();
|
||||
await chatPage.dismissWelcomeScreen();
|
||||
await chatPage.getChatInput().fill('Hello');
|
||||
await chatPage.getSendButton().click();
|
||||
await expect(chatPage.getChatMessages().last()).toContainText('你好');
|
||||
|
||||
// STEP: update instructions for the agent
|
||||
await chatPage.sidebar.getPersonalAgentButton().click();
|
||||
await page.getEditButtonAt(0).click();
|
||||
await page.editModal.getSystemPromptField().fill('reply in Japanese');
|
||||
await page.editModal.getSaveButton().click();
|
||||
await expect(page.editModal.getRoot()).not.toBeInViewport(); // wait for modal to close
|
||||
|
||||
// STEP: send message to the updated agent
|
||||
await page.getAgentCards().nth(0).click();
|
||||
await chatPage.getChatInput().fill('Hello');
|
||||
await chatPage.getSendButton().click();
|
||||
await expect(chatPage.getChatMessages().last()).toContainText('こんにちは');
|
||||
|
||||
// STEP: delete the agent
|
||||
await chatPage.sidebar.getPersonalAgentButton().click();
|
||||
await page.getMenuAt(0).click();
|
||||
await page.getVisiblePopoverMenuItem('Delete').click();
|
||||
await n8n.page.getByRole('dialog').getByText('Delete', { exact: true }).click(); // confirmation dialog
|
||||
await expect(page.getAgentCards()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { chatHubTestConfig, expect, test } from './fixtures';
|
||||
import { INSTANCE_MEMBER_CREDENTIALS } from '../../../config/test-users';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
import { ChatHubSettingsPage } from '../../../pages/ChatHubSettingsPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Settings @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('set global credentials for a provider', async ({ n8n, anthropicCredential }) => {
|
||||
const page = new ChatHubSettingsPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHubSettings();
|
||||
|
||||
// Open Anthropic settings
|
||||
await page.getProviderActionToggle('Anthropic').click();
|
||||
await page.getVisiblePopoverMenuItem('Edit provider').click();
|
||||
|
||||
// Configure default credential
|
||||
await page.providerModal.getCredentialPicker().click();
|
||||
await page.providerModal.getVisiblePopoverOption(anthropicCredential.name).click();
|
||||
|
||||
// Open credential modal and make it globally shared
|
||||
await page.providerModal.getEditCredentialButton().click();
|
||||
await page.credentialModal.changeTab('Sharing');
|
||||
await page.credentialModal.getUsersSelect().click();
|
||||
await page.credentialModal.getVisiblePopoverOption('All users and projects').click();
|
||||
await page.credentialModal.save();
|
||||
await page.credentialModal.close();
|
||||
|
||||
// Save settings
|
||||
await page.providerModal.getConfirmButton().click();
|
||||
await expect(n8n.notifications.getSuccessNotifications()).toHaveCount(1);
|
||||
|
||||
const memberN8n = await n8n.start.withUser(INSTANCE_MEMBER_CREDENTIALS[0]);
|
||||
const chatPage = new ChatHubChatPage(memberN8n.page);
|
||||
|
||||
await memberN8n.navigate.toChatHub();
|
||||
await chatPage.dismissWelcomeScreen();
|
||||
|
||||
await expect(chatPage.getSelectedCredentialName()).toHaveText(anthropicCredential.name);
|
||||
|
||||
await chatPage.getChatInput().fill('Hello');
|
||||
await chatPage.getSendButton().click();
|
||||
|
||||
await expect(chatPage.getChatMessages().nth(0)).toContainText('Hello');
|
||||
await expect(chatPage.getChatMessages().nth(1)).toContainText(
|
||||
'Hello! How can I help you today?',
|
||||
);
|
||||
await memberN8n.page.close();
|
||||
});
|
||||
|
||||
test('restrict available LLM providers and models', async ({
|
||||
n8n,
|
||||
anthropicCredential,
|
||||
anthropicApiKey,
|
||||
}) => {
|
||||
const page = new ChatHubSettingsPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHubSettings();
|
||||
|
||||
// Open Anthropic settings
|
||||
await page.getProviderActionToggle('Anthropic').click();
|
||||
await page.getVisiblePopoverMenuItem('Edit provider').click();
|
||||
|
||||
// Anthropic: configure default credential
|
||||
await page.providerModal.getCredentialPicker().click();
|
||||
await page.providerModal.getVisiblePopoverOption(anthropicCredential.name).click();
|
||||
|
||||
// Anthropic: enable limit models toggle
|
||||
await page.providerModal.getLimitModelsToggle().click();
|
||||
|
||||
// Anthropic: select only Claude Opus 4.5
|
||||
await page.providerModal.getModelSelector().click();
|
||||
await page.providerModal.getVisiblePopoverOption('Claude Opus 4.5').click();
|
||||
|
||||
// Anthropic: save settings
|
||||
await page.providerModal.getConfirmButton().click();
|
||||
await expect(page.providerModal.getRoot()).toBeHidden();
|
||||
|
||||
// Open OpenAI settings
|
||||
await page.getProviderActionToggle('OpenAI').click();
|
||||
await page.getVisiblePopoverMenuItem('Edit provider').click();
|
||||
|
||||
// OpenAI: disable provider and save
|
||||
await expect(page.providerModal.getEnabledToggle()).toBeChecked();
|
||||
await page.providerModal.getEnabledToggle().click();
|
||||
await page.providerModal.getConfirmButton().click();
|
||||
await expect(page.providerModal.getRoot()).toBeHidden();
|
||||
|
||||
await n8n.page.close();
|
||||
|
||||
// Log in as member and verify only selected model is available
|
||||
const memberN8n = await n8n.start.withUser(INSTANCE_MEMBER_CREDENTIALS[0]);
|
||||
const chatPage = new ChatHubChatPage(memberN8n.page);
|
||||
|
||||
const cred = await memberN8n.api.credentials.createCredential({
|
||||
name: 'Member API key',
|
||||
type: 'anthropicApi',
|
||||
data: {
|
||||
apiKey: anthropicApiKey,
|
||||
},
|
||||
});
|
||||
|
||||
await memberN8n.navigate.toChatHub();
|
||||
await chatPage.dismissWelcomeScreen();
|
||||
|
||||
await chatPage.getModelSelectorButton().click();
|
||||
await expect(chatPage.getVisiblePopoverMenuItem('Anthropic')).toBeVisible();
|
||||
await expect(chatPage.getVisiblePopoverMenuItem('OpenAI')).toBeHidden();
|
||||
await chatPage.getVisiblePopoverMenuItem('Anthropic').hover({ force: true });
|
||||
|
||||
const anthropicModels = chatPage.getVisiblePopoverMenuItem(/^Claude/);
|
||||
await expect(anthropicModels).toHaveText(['Claude Opus 4.5']);
|
||||
|
||||
await memberN8n.api.credentials.deleteCredential(cred.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { test, expect, chatHubTestConfig } from './fixtures';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Tools usage @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('use web search tool in conversation', async ({
|
||||
n8n,
|
||||
anthropicCredential: _,
|
||||
jinaCredential,
|
||||
}) => {
|
||||
const page = new ChatHubChatPage(n8n.page);
|
||||
|
||||
await n8n.navigate.toChatHub();
|
||||
await page.dismissWelcomeScreen();
|
||||
await expect(page.getModelSelectorButton()).toContainText(/claude/i);
|
||||
|
||||
// Open tools manager modal
|
||||
await page.getToolsButton().click();
|
||||
await expect(page.toolsModal.getRoot()).toBeVisible();
|
||||
|
||||
// Add Jina AI tool from available tools list
|
||||
await page.toolsModal.getAddButton('Jina AI').click();
|
||||
|
||||
// Select credential in settings view
|
||||
await page.toolsModal.getCredentialSelect().click();
|
||||
await page.getVisiblePopoverOption(jinaCredential.name).click();
|
||||
|
||||
// Change Operation from "Read" to "Search"
|
||||
await page.toolsModal.getParameterInput('operation').click();
|
||||
await page.getVisiblePopoverOption('Search').click();
|
||||
|
||||
// Set search query and simplify to "defined automatically by the model"
|
||||
await page.toolsModal.getFromAiOverrideButton('searchQuery').click();
|
||||
await page.toolsModal.getFromAiOverrideButton('simplify').click();
|
||||
|
||||
// Save and close
|
||||
await page.toolsModal.getSaveButton().click();
|
||||
await page.toolsModal.getCloseButton().click();
|
||||
await expect(page.toolsModal.getRoot()).toBeHidden();
|
||||
|
||||
// Verify tool is shown
|
||||
await expect(page.getToolsButton()).toContainText('Search web in Jina AI');
|
||||
|
||||
// Send message and check response
|
||||
await page.getChatInput().fill('What is n8n?');
|
||||
await page.getSendButton().click();
|
||||
await expect(page.getChatMessages().nth(0)).toContainText('What is n8n?');
|
||||
await expect(page.getChatMessages().nth(1)).toContainText(/automation/i, {
|
||||
timeout: 60000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { expect, test, chatHubTestConfig } from './fixtures';
|
||||
import { INSTANCE_MEMBER_CREDENTIALS } from '../../../config/test-users';
|
||||
import { CanvasPage } from '../../../pages/CanvasPage';
|
||||
import { ChatHubChatPage } from '../../../pages/ChatHubChatPage';
|
||||
import { ChatHubWorkflowAgentsPage } from '../../../pages/ChatHubWorkflowAgentsPage';
|
||||
import { NodeDetailsViewPage } from '../../../pages/NodeDetailsViewPage';
|
||||
|
||||
test.use(chatHubTestConfig);
|
||||
|
||||
test.describe('Workflow agent @capability:proxy', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Chat' },
|
||||
],
|
||||
}, () => {
|
||||
test('manage workflow agents @auth:admin', async ({ n8n, agentWorkflow }) => {
|
||||
const agentsPage = new ChatHubWorkflowAgentsPage(n8n.page);
|
||||
const chatPage = new ChatHubChatPage(n8n.page);
|
||||
|
||||
// STEP: Navigate to workflow agents page and verify agent is listed
|
||||
await n8n.navigate.toChatHubWorkflowAgents();
|
||||
await expect(agentsPage.getAgentCards()).toHaveCount(1);
|
||||
await expect(agentsPage.getAgentCards().nth(0)).toContainText(agentWorkflow.name);
|
||||
|
||||
// STEP: Click agent card to start conversation
|
||||
await agentsPage.getAgentCards().nth(0).click();
|
||||
await chatPage.dismissWelcomeScreen();
|
||||
await expect(chatPage.getModelSelectorButton()).toContainText(agentWorkflow.name);
|
||||
|
||||
await chatPage.getChatInput().fill('Hello');
|
||||
await chatPage.getSendButton().click();
|
||||
await expect(chatPage.getChatMessages().last()).toContainText(/Bonjour/i);
|
||||
|
||||
// STEP: Open workflow in new tab and update system prompt
|
||||
const tab1 = await chatPage.clickOpenWorkflowButton();
|
||||
const canvas = new CanvasPage(tab1);
|
||||
const ndv = new NodeDetailsViewPage(tab1);
|
||||
|
||||
await canvas.openNode('AI Agent');
|
||||
await ndv.fillParameterInput('System Message', 'Reply in Finnish');
|
||||
await ndv.close();
|
||||
await canvas.publishWorkflow();
|
||||
await tab1.close();
|
||||
|
||||
// STEP: Select the workflow agent for new conversation
|
||||
await n8n.navigate.toChatHub();
|
||||
await chatPage.getModelSelectorButton().click();
|
||||
await n8n.page.waitForTimeout(500);
|
||||
await chatPage.getVisiblePopoverMenuItem('Workflow agents').hover({ force: true });
|
||||
await chatPage.getVisiblePopoverMenuItem(agentWorkflow.name, { exact: true }).click();
|
||||
|
||||
// STEP: Send message again
|
||||
await chatPage.getChatInput().fill('Hello');
|
||||
await chatPage.getSendButton().click();
|
||||
await expect(chatPage.getChatMessages().last()).toContainText(/Hei|Moi/i);
|
||||
|
||||
// STEP: Open workflow in new tab and disable ChatHub
|
||||
const tab2 = await chatPage.clickOpenWorkflowButton();
|
||||
const canvas2 = new CanvasPage(tab2);
|
||||
const ndv2 = new NodeDetailsViewPage(tab2);
|
||||
|
||||
await canvas2.openNode('When chat message received');
|
||||
await ndv2.getParameterSwitch('availableInChat').click();
|
||||
await ndv2.close();
|
||||
await canvas2.publishWorkflow();
|
||||
await tab2.close();
|
||||
await n8n.navigate.toChatHubWorkflowAgents();
|
||||
await expect(agentsPage.getEmptyText()).toBeVisible();
|
||||
});
|
||||
|
||||
test('sharing workflow agent with project chat user', async ({
|
||||
n8n,
|
||||
anthropicCredential,
|
||||
agentWorkflow,
|
||||
project,
|
||||
}) => {
|
||||
const memberN8n = await n8n.start.withUser(INSTANCE_MEMBER_CREDENTIALS[0]);
|
||||
const memberEmail = INSTANCE_MEMBER_CREDENTIALS[0].email;
|
||||
|
||||
// Transfer workflow and credential to it
|
||||
await n8n.api.credentials.transferCredential(anthropicCredential.id, project.id);
|
||||
await n8n.api.workflows.transfer(agentWorkflow.id, project.id);
|
||||
|
||||
// Verify that the agent is visible to owner
|
||||
const ownerWorkflowAgentsPage = new ChatHubWorkflowAgentsPage(n8n.page);
|
||||
await n8n.navigate.toChatHubWorkflowAgents();
|
||||
await expect(ownerWorkflowAgentsPage.getAgentCards()).toContainText([agentWorkflow.name]);
|
||||
|
||||
// Verify that the agent is not visible to member before sharing
|
||||
const memberWorkflowAgentsPage = new ChatHubWorkflowAgentsPage(memberN8n.page);
|
||||
await memberN8n.navigate.toChatHubWorkflowAgents();
|
||||
await expect(memberWorkflowAgentsPage.getEmptyText()).toBeVisible();
|
||||
|
||||
// Add member user to the project with project chat user role
|
||||
await n8n.navigate.toProjectSettings(project.id);
|
||||
await n8n.projectSettings.getMembersSearchInput().click();
|
||||
await n8n.projectSettings.getVisiblePopoverOption(memberEmail).click();
|
||||
await expect(n8n.projectSettings.getMembersTable()).toContainText(memberEmail);
|
||||
await n8n.projectSettings.getRoleDropdownFor(memberEmail).click();
|
||||
await n8n.projectSettings.getVisiblePopoverOption('Project Chat User').click();
|
||||
await expect(n8n.notifications.getSuccessNotifications().first()).toBeVisible();
|
||||
|
||||
// Verify that the agent is visible and usable to member after sharing
|
||||
await memberN8n.page.reload();
|
||||
await expect(memberWorkflowAgentsPage.getAgentCards()).toContainText([agentWorkflow.name]);
|
||||
await memberWorkflowAgentsPage.getAgentCards().nth(0).click();
|
||||
|
||||
const memberChatPage = new ChatHubChatPage(memberN8n.page);
|
||||
await memberChatPage.dismissWelcomeScreen();
|
||||
await memberChatPage.getChatInput().fill('Hello');
|
||||
await memberChatPage.getSendButton().click();
|
||||
await expect(memberChatPage.getChatMessages().last()).toContainText(/Bonjour/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Project } from '@n8n/db';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
|
||||
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';
|
||||
const JINA_API_KEY = process.env.JINA_API_KEY ?? 'mock-jina-api-key';
|
||||
|
||||
type ChatHubFixtures = {
|
||||
project: Project;
|
||||
anthropicCredential: CredentialResponse;
|
||||
anthropicApiKey: string;
|
||||
jinaCredential: CredentialResponse;
|
||||
jinaApiKey: string;
|
||||
chatHubProxySetup: undefined;
|
||||
agentWorkflow: IWorkflowBase;
|
||||
};
|
||||
|
||||
export const chatHubTestConfig = {
|
||||
timezoneId: 'America/New_York',
|
||||
capability: {
|
||||
services: ['proxy'],
|
||||
env: {
|
||||
N8N_COMMUNITY_PACKAGES_ENABLED: 'false',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const test = base.extend<ChatHubFixtures>({
|
||||
anthropicApiKey: async ({}, use) => {
|
||||
await use(ANTHROPIC_API_KEY);
|
||||
},
|
||||
|
||||
jinaApiKey: async ({}, use) => {
|
||||
await use(JINA_API_KEY);
|
||||
},
|
||||
|
||||
chatHubProxySetup: [
|
||||
async ({ services }, use) => {
|
||||
// Setup
|
||||
await services.proxy.clearAllExpectations();
|
||||
await services.proxy.loadExpectations('chat-hub', { strictBodyMatching: true });
|
||||
|
||||
await use(undefined);
|
||||
|
||||
// Teardown
|
||||
if (!process.env.CI) {
|
||||
await services.proxy.recordExpectations('chat-hub', {
|
||||
dedupe: true,
|
||||
transform: (expectation) => {
|
||||
const response = expectation.httpResponse as {
|
||||
headers?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
if (response?.headers) {
|
||||
delete response.headers['anthropic-organization-id'];
|
||||
}
|
||||
|
||||
return expectation;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
|
||||
project: async ({ n8n }, use) => {
|
||||
const project = await n8n.api.projects.createProject('ChatHub test project');
|
||||
|
||||
await use(project);
|
||||
|
||||
await n8n.api.projects.deleteProject(project.id);
|
||||
},
|
||||
|
||||
agentWorkflow: async ({ n8n, anthropicCredential, project: _ }, use) => {
|
||||
const res = await n8n.api.workflows.importWorkflowFromFile('chat-hub-workflow-agent.json', {
|
||||
transform: (workflow) => {
|
||||
const anthropicNode = workflow.nodes?.find((n) => n.type.includes('lmChatAnthropic'));
|
||||
|
||||
anthropicNode!.credentials!.anthropicApi = anthropicCredential;
|
||||
|
||||
return workflow;
|
||||
},
|
||||
});
|
||||
|
||||
await use(res.createdWorkflow);
|
||||
|
||||
await n8n.api.workflows.archive(res.workflowId);
|
||||
await n8n.api.workflows.delete(res.workflowId);
|
||||
},
|
||||
|
||||
anthropicCredential: async ({ n8n, anthropicApiKey, project: _ }, 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);
|
||||
},
|
||||
|
||||
jinaCredential: async ({ n8n, jinaApiKey }, use) => {
|
||||
const res = await n8n.api.credentials.createCredential({
|
||||
name: `Jina AI cred ${crypto.randomUUID().slice(0, 8)}`,
|
||||
type: 'jinaAiApi',
|
||||
data: {
|
||||
apiKey: jinaApiKey,
|
||||
},
|
||||
});
|
||||
|
||||
await use(res);
|
||||
|
||||
await n8n.api.credentials.deleteCredential(res.id);
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = baseExpect;
|
||||
Reference in New Issue
Block a user