feat: Add model selector node (#16371)

This commit is contained in:
Benjamin Schroth
2025-06-20 15:30:33 +02:00
committed by GitHub
parent a9688b101f
commit 79650ea55a
20 changed files with 1321 additions and 113 deletions

View File

@@ -5,6 +5,7 @@ import { createTestingPinia } from '@pinia/testing';
import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription, WorkflowParameters } from 'n8n-workflow';
import { NodeConnectionTypes, Workflow } from 'n8n-workflow';
import { nextTick } from 'vue';
const nodeType: INodeTypeDescription = {
displayName: 'OpenAI',
@@ -57,6 +58,8 @@ const workflow: WorkflowParameters = {
};
const getNodeType = vi.fn();
let mockWorkflowData = workflow;
let mockGetNodeByName = vi.fn(() => node);
vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({
@@ -66,8 +69,8 @@ vi.mock('@/stores/nodeTypes.store', () => ({
vi.mock('@/stores/workflows.store', () => ({
useWorkflowsStore: vi.fn(() => ({
getCurrentWorkflow: vi.fn(() => new Workflow(workflow)),
getNodeByName: vi.fn(() => node),
getCurrentWorkflow: vi.fn(() => new Workflow(mockWorkflowData)),
getNodeByName: mockGetNodeByName,
})),
}));
@@ -88,17 +91,17 @@ describe('NDVSubConnections', () => {
vi.advanceTimersByTime(1000); // Event debounce time
await waitFor(() => {});
expect(getByTestId('subnode-connection-group-ai_tool')).toBeVisible();
expect(getByTestId('subnode-connection-group-ai_tool-0')).toBeVisible();
expect(html()).toEqual(
`<div class="container">
<div class="connections" style="--possible-connections: 1;">
<div data-test-id="subnode-connection-group-ai_tool">
<div data-test-id="subnode-connection-group-ai_tool-0">
<div class="connectionType"><span class="connectionLabel">Tools</span>
<div>
<div class="connectedNodesWrapper" style="--nodes-length: 0;">
<div class="plusButton">
<n8n-tooltip placement="top" teleported="true" offset="10" show-after="300" disabled="false">
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool"></n8n-icon-button>
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool-0"></n8n-icon-button>
</n8n-tooltip>
</div>
<!--v-if-->
@@ -123,4 +126,101 @@ describe('NDVSubConnections', () => {
await waitFor(() => {});
expect(component.html()).toEqual('<!--v-if-->');
});
it('should render multiple connections of the same type separately', async () => {
// Mock a ModelSelector-like node with multiple ai_languageModel connections
const multiConnectionNodeType: INodeTypeDescription = {
displayName: 'Model Selector',
name: 'modelSelector',
version: [1],
inputs: [
{ type: NodeConnectionTypes.Main },
{
type: NodeConnectionTypes.AiLanguageModel,
displayName: 'Model 1',
required: true,
maxConnections: 1,
},
{
type: NodeConnectionTypes.AiLanguageModel,
displayName: 'Model 2',
required: true,
maxConnections: 1,
},
{
type: NodeConnectionTypes.AiLanguageModel,
displayName: 'Model 3',
required: true,
maxConnections: 1,
},
],
outputs: [NodeConnectionTypes.AiLanguageModel],
properties: [],
defaults: { color: '', name: '' },
group: [],
description: '',
};
const multiConnectionNode: INodeUi = {
...node,
name: 'ModelSelector',
type: 'modelSelector',
};
// Mock connected nodes
const mockWorkflow = {
...workflow,
nodes: [multiConnectionNode],
connectionsByDestinationNode: {
ModelSelector: {
[NodeConnectionTypes.AiLanguageModel]: [
null, // Main input (index 0) - no ai_languageModel connection
[{ node: 'OpenAI1', type: NodeConnectionTypes.AiLanguageModel, index: 0 }], // Model 1 (index 1)
[{ node: 'Claude', type: NodeConnectionTypes.AiLanguageModel, index: 0 }], // Model 2 (index 2)
[], // Model 3 (index 3) - no connection
],
},
},
};
// Mock additional nodes
const openAI1Node: INodeUi = {
...node,
name: 'OpenAI1',
type: '@n8n/n8n-nodes-langchain.openAi',
};
const claudeNode: INodeUi = {
...node,
name: 'Claude',
type: '@n8n/n8n-nodes-langchain.claude',
};
getNodeType.mockReturnValue(multiConnectionNodeType);
// Update mock data for this test
mockWorkflowData = mockWorkflow;
mockGetNodeByName = vi.fn((name: string) => {
if (name === 'ModelSelector') return multiConnectionNode;
if (name === 'OpenAI1') return openAI1Node;
if (name === 'Claude') return claudeNode;
return null;
});
const { getByTestId } = render(NDVSubConnections, {
props: {
rootNode: multiConnectionNode,
},
});
vi.advanceTimersByTime(1);
await nextTick();
expect(getByTestId('subnode-connection-group-ai_languageModel-0')).toBeVisible(); // Model 1
expect(getByTestId('subnode-connection-group-ai_languageModel-1')).toBeVisible(); // Model 2
expect(getByTestId('subnode-connection-group-ai_languageModel-2')).toBeVisible(); // Model 3
expect(getByTestId('add-subnode-ai_languageModel-0')).toBeVisible();
expect(getByTestId('add-subnode-ai_languageModel-1')).toBeVisible();
expect(getByTestId('add-subnode-ai_languageModel-2')).toBeVisible();
});
});