Skip to content

Commit 54dcded

Browse files
authored
feat(editor): Support partial executions of tool nodes (#14945)
1 parent 5fa41bd commit 54dcded

21 files changed

+1135
-25
lines changed

packages/cli/src/__tests__/load-nodes-and-credentials.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ describe('LoadNodesAndCredentials', () => {
318318
group: ['input'],
319319
inputs: [],
320320
outputs: ['ai_tool'],
321+
usableAsTool: true,
321322
properties: [
322323
{
323324
default: 'A test node',
@@ -370,6 +371,7 @@ describe('LoadNodesAndCredentials', () => {
370371
inputs: [],
371372
outputs: ['ai_tool'],
372373
description: 'A test node',
374+
usableAsTool: true,
373375
properties: [
374376
{
375377
displayName: 'Description',

packages/cli/src/load-nodes-and-credentials.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ export class LoadNodesAndCredentials {
317317
} as INodeTypeBaseDescription)
318318
: deepCopy(usableNode);
319319
const wrapped = this.convertNodeToAiTool({ description }).description;
320+
// TODO: Remove this when we support partial execution on all tool nodes
321+
wrapped.usableAsTool = true;
320322

321323
this.types.nodes.push(wrapped);
322324
this.known.nodes[wrapped.name] = { ...this.known.nodes[usableNode.name] };
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { createTestingPinia } from '@pinia/testing';
2+
import { createComponentRenderer } from '@/__tests__/render';
3+
import FromAiParametersModal from '@/components/FromAiParametersModal.vue';
4+
import { FROM_AI_PARAMETERS_MODAL_KEY, STORES } from '@/constants';
5+
import userEvent from '@testing-library/user-event';
6+
import { useWorkflowsStore } from '@/stores/workflows.store';
7+
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
8+
import { useRouter } from 'vue-router';
9+
import { NodeConnectionTypes } from 'n8n-workflow';
10+
11+
const ModalStub = {
12+
template: `
13+
<div>
14+
<slot name="header" />
15+
<slot name="title" />
16+
<slot name="content" />
17+
<slot name="footer" />
18+
</div>
19+
`,
20+
};
21+
22+
vi.mock('vue-router');
23+
24+
vi.mocked(useRouter);
25+
26+
const mockNode = {
27+
id: 'id1',
28+
name: 'Test Node',
29+
parameters: {
30+
testBoolean: "={{ $fromAI('testBoolean', ``, 'boolean') }}",
31+
testParam: "={{ $fromAi('testParam', ``, 'string') }}",
32+
},
33+
};
34+
35+
const mockParentNode = {
36+
name: 'Parent Node',
37+
};
38+
39+
const mockRunData = {
40+
data: {
41+
resultData: {
42+
runData: {
43+
['Test Node']: [
44+
{
45+
inputOverride: {
46+
[NodeConnectionTypes.AiTool]: [[{ json: { testParam: 'override' } }]],
47+
},
48+
},
49+
],
50+
},
51+
},
52+
},
53+
};
54+
55+
const mockWorkflow = {
56+
id: 'test-workflow',
57+
getChildNodes: () => ['Parent Node'],
58+
};
59+
60+
const renderModal = createComponentRenderer(FromAiParametersModal);
61+
let pinia: ReturnType<typeof createTestingPinia>;
62+
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
63+
let parameterOverridesStore: ReturnType<typeof useParameterOverridesStore>;
64+
describe('FromAiParametersModal', () => {
65+
beforeEach(() => {
66+
pinia = createTestingPinia({
67+
initialState: {
68+
[STORES.UI]: {
69+
modalsById: {
70+
[FROM_AI_PARAMETERS_MODAL_KEY]: {
71+
open: true,
72+
data: {
73+
nodeName: 'Test Node',
74+
},
75+
},
76+
},
77+
modalStack: [FROM_AI_PARAMETERS_MODAL_KEY],
78+
},
79+
[STORES.WORKFLOWS]: {
80+
workflow: mockWorkflow,
81+
workflowExecutionData: mockRunData,
82+
},
83+
},
84+
});
85+
workflowsStore = useWorkflowsStore();
86+
workflowsStore.getNodeByName = vi
87+
.fn()
88+
.mockImplementation((name: string) => (name === 'Test Node' ? mockNode : mockParentNode));
89+
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
90+
parameterOverridesStore = useParameterOverridesStore();
91+
parameterOverridesStore.clearParameterOverrides = vi.fn();
92+
parameterOverridesStore.addParameterOverrides = vi.fn();
93+
parameterOverridesStore.substituteParameters = vi.fn();
94+
});
95+
96+
it('renders correctly with node data', () => {
97+
const { getByTitle } = renderModal({
98+
props: {
99+
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
100+
data: {
101+
nodeName: 'Test Node',
102+
},
103+
},
104+
global: {
105+
stubs: {
106+
Modal: ModalStub,
107+
},
108+
},
109+
pinia,
110+
});
111+
112+
expect(getByTitle('Test Test Node')).toBeTruthy();
113+
});
114+
115+
it('uses run data when available as initial values', async () => {
116+
const { getByTestId } = renderModal({
117+
props: {
118+
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
119+
data: {
120+
nodeName: 'Test Node',
121+
},
122+
},
123+
global: {
124+
stubs: {
125+
Modal: ModalStub,
126+
},
127+
},
128+
pinia,
129+
});
130+
131+
await userEvent.click(getByTestId('execute-workflow-button'));
132+
133+
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
134+
'test-workflow',
135+
'id1',
136+
{
137+
testBoolean: true,
138+
testParam: 'override',
139+
},
140+
);
141+
});
142+
143+
it('clears parameter overrides when modal is executed', async () => {
144+
const { getByTestId } = renderModal({
145+
props: {
146+
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
147+
data: {
148+
nodeName: 'Test Node',
149+
},
150+
},
151+
global: {
152+
stubs: {
153+
Modal: ModalStub,
154+
},
155+
},
156+
pinia,
157+
});
158+
159+
await userEvent.click(getByTestId('execute-workflow-button'));
160+
161+
expect(parameterOverridesStore.clearParameterOverrides).toHaveBeenCalledWith(
162+
'test-workflow',
163+
'id1',
164+
);
165+
});
166+
167+
it('adds substitutes for parameters when executed', async () => {
168+
const { getByTestId } = renderModal({
169+
props: {
170+
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
171+
data: {
172+
nodeName: 'Test Node',
173+
},
174+
},
175+
global: {
176+
stubs: {
177+
Modal: ModalStub,
178+
},
179+
},
180+
pinia,
181+
});
182+
183+
const inputs = getByTestId('from-ai-parameters-modal-inputs');
184+
await userEvent.click(inputs.querySelector('input[value="testBoolean"]') as Element);
185+
await userEvent.clear(inputs.querySelector('input[name="testParam"]') as Element);
186+
await userEvent.type(inputs.querySelector('input[name="testParam"]') as Element, 'given value');
187+
await userEvent.click(getByTestId('execute-workflow-button'));
188+
189+
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
190+
'test-workflow',
191+
'id1',
192+
{
193+
testBoolean: false,
194+
testParam: 'given value',
195+
},
196+
);
197+
});
198+
});

0 commit comments

Comments
 (0)