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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,35 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import { normalizeItems } from 'n8n-core';
import type { IExecuteFunctions, INode, IWorkflowDataProxyData } from 'n8n-workflow';
import { Code } from '../Code.node';
import { PythonTaskRunnerSandbox } from '../PythonTaskRunnerSandbox';
describe('Code Node unit test', () => {
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
it('should route legacy `python` language to native Python runner', async () => {
const node = new Code();
const pythonThisArg: MockProxy<IExecuteFunctions> = mock<IExecuteFunctions>();
pythonThisArg.helpers = { normalizeItems } as IExecuteFunctions['helpers'];
pythonThisArg.getNode.mockReturnValue(mock<INode>({ typeVersion: 2 }));
pythonThisArg.getWorkflowDataProxy.mockReturnValue(workflowDataProxy);
pythonThisArg.getMode.mockReturnValue('manual');
pythonThisArg.getRunnerStatus.mockReturnValue({ available: true });
pythonThisArg.getNodeParameter.calledWith('language', 0).mockReturnValue('python');
pythonThisArg.getNodeParameter.calledWith('mode', 0).mockReturnValue('runOnceForAllItems');
pythonThisArg.getNodeParameter.calledWith('pythonCode', 0).mockReturnValue('return []');
pythonThisArg.getInputData.mockReturnValue([{ json: {} }]);
const runSpy = jest
.spyOn(PythonTaskRunnerSandbox.prototype, 'runUsingIncomingItems')
.mockResolvedValue([]);
await node.execute.call(pythonThisArg);
expect(runSpy).toHaveBeenCalled();
runSpy.mockRestore();
});
});
@@ -0,0 +1,213 @@
{
"nodes": [
{
"parameters": {},
"id": "33eede8d-2ab0-42ab-b79a-a069d8549ab0",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-40, 580]
},
{
"parameters": {
"jsCode": "return[\n { value: 1 },\n { value: 2 },\n]"
},
"id": "a5913b52-24dc-4f81-bb7f-f90e61dad978",
"name": "Sample Data",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [200, 580]
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nlet sum = 0;\nfor (const item of $input.all()) {\n sum += item.json.value;\n}\n\nreturn [ {sum} ];"
},
"id": "c4ad4913-5af3-42bc-a784-69182f1facdd",
"name": "Run Once for All Items",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 320]
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nlet sum = 0;\nfor (const item of items) {\n sum += item.json.value;\n}\n\nreturn [ {sum} ];"
},
"id": "34cbd204-4335-4790-92cd-c3df617eee21",
"name": "Run Once for All Items (Legacy Syntax)",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 500]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Add a new field called 'myNewField' to the\n// JSON of the item\n$input.item.json.myNewField = $input.item.json.value;\n\nreturn $input.item;"
},
"id": "f67d29bf-554a-4572-8867-4456182dec24",
"name": "Run Once for Each Item",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 680]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Add a new field called 'myNewField' to the\n// JSON of the item\nitem.json.myNewField = item.json.value;\n\nreturn item;"
},
"id": "6f4bf149-e84e-4e0d-802a-7eaf7a42b18c",
"name": "Run Once for Each Item (Legacy Syntax)",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 860]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const json = $input.item.json\njson.myNewField = await (async () => json.value)();\n\nreturn $input.item;"
},
"id": "3cff4a64-c3fd-47d3-a33e-3c446846138f",
"name": "With Async Functions",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
460,
1200
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const json = $input.item.json\njson.myNewField = await new Promise((resolve) => resolve(json.value));\n\nreturn $input.item;"
},
"id": "947e4e3e-2da3-40c5-97da-830c4572fc05",
"name": "With Promises",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
460,
1380
]
}
],
"pinData": {
"Run Once for All Items": [
{
"json": {
"sum": 3
}
}
],
"Run Once for Each Item": [
{
"json": {
"value": 1,
"myNewField": 1
}
},
{
"json": {
"value": 2,
"myNewField": 2
}
}
],
"Run Once for All Items (Legacy Syntax)": [
{
"json": {
"sum": 3
}
}
],
"Run Once for Each Item (Legacy Syntax)": [
{
"json": {
"value": 1,
"myNewField": 1
}
},
{
"json": {
"value": 2,
"myNewField": 2
}
}
],
"With Async Functions": [
{
"json": {
"value": 1,
"myNewField": 1
}
},
{
"json": {
"value": 2,
"myNewField": 2
}
}
],
"With Promises": [
{
"json": {
"value": 1,
"myNewField": 1
}
},
{
"json": {
"value": 2,
"myNewField": 2
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Sample Data",
"type": "main",
"index": 0
}
]
]
},
"Sample Data": {
"main": [
[
{
"node": "Run Once for All Items",
"type": "main",
"index": 0
},
{
"node": "Run Once for Each Item",
"type": "main",
"index": 0
},
{
"node": "Run Once for All Items (Legacy Syntax)",
"type": "main",
"index": 0
},
{
"node": "Run Once for Each Item (Legacy Syntax)",
"type": "main",
"index": 0
},
{
"node": "With Async Functions",
"type": "main",
"index": 0
},
{
"node": "With Promises",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,64 @@
import { ExecutionError } from '../ExecutionError';
describe('ExecutionError', () => {
describe('constructor', () => {
it('should set message to "Unknown error" when stack is empty', () => {
const error = new Error('test');
error.stack = '';
const executionError = new ExecutionError(error);
expect(executionError.message).toBe('Unknown error');
});
it('should extract error details and type from stack', () => {
const error = new Error('ErrorType: Error Details');
error.stack = 'Error: ErrorType: Error Details\n at Code:123';
const executionError = new ExecutionError(error);
expect(executionError.message).toBe('Error Details [line 123]');
expect(executionError.description).toBe('ErrorType');
});
it('should extract error details when no error type is present', () => {
const error = new Error('Error Details');
error.stack = 'Error: Error Details\n at Code:123';
const executionError = new ExecutionError(error);
expect(executionError.message).toBe('Error Details [line 123]');
expect(executionError.description).toBe(null);
});
it('should handle stack with only "Error: " prefix', () => {
const error = new Error('Error: ');
error.stack = 'Error: Error: \n at Code:123';
const executionError = new ExecutionError(error);
expect(executionError.message).toBe('Unknown error [line 123]');
expect(executionError.description).toBe(null);
});
it('should handle stack with colon and space', () => {
const error = new Error(': ');
error.stack = 'Error: : \n at Code:123';
const executionError = new ExecutionError(error);
expect(executionError.message).toBe('Unknown error [line 123]');
expect(executionError.description).toBe(null);
});
it('should handle itemIndex', () => {
const error = new Error('ErrorType: Error Details');
error.stack = 'Error: ErrorType: Error Details\n at Code:123';
const executionError = new ExecutionError(error, 1);
expect(executionError.message).toBe('Error Details [line 123, for item 1]');
expect(executionError.description).toBe('ErrorType');
expect(executionError.itemIndex).toBe(1);
expect(executionError.context).toEqual({ itemIndex: 1 });
});
it('should handle stack without line number', () => {
const error = new Error('ErrorType: Error Details');
error.stack = 'Error: ErrorType: Error Details';
const executionError = new ExecutionError(error, 1);
expect(executionError.message).toBe('Error Details');
expect(executionError.description).toBe('ErrorType');
expect(executionError.itemIndex).toBe(1);
expect(executionError.context).toEqual({ itemIndex: 1 });
});
});
});
@@ -0,0 +1,51 @@
import { validateNoDisallowedMethodsInRunForEach } from '../JsCodeValidator';
describe('JsCodeValidator', () => {
describe('validateNoDisallowedMethodsInRunForEach', () => {
it('should not throw error if disallow method is used within single line comments', () => {
const code = [
"// Add a new field called 'myNewField' to the JSON of the item",
'$input.item.json.myNewField = 1;',
' // const xxx = $input.all()',
'return $input.item;',
].join('\n');
expect(() => validateNoDisallowedMethodsInRunForEach(code, 0)).not.toThrow();
});
it('should not throw error if disallow method is used in single multi line comments', () => {
const code = [
"// Add a new field called 'myNewField' to the JSON of the item",
'$input.item.json.myNewField = 1;',
'/** const xxx = $input.all()*/',
'return $input.item;',
].join('\n');
expect(() => validateNoDisallowedMethodsInRunForEach(code, 0)).not.toThrow();
});
it('should not throw error if disallow method is used within multi line comments', () => {
const code = [
"// Add a new field called 'myNewField' to the JSON of the item",
'$input.item.json.myNewField = 1;',
'/**',
'*const xxx = $input.all()',
'*/',
'return $input.item;',
].join('\n');
expect(() => validateNoDisallowedMethodsInRunForEach(code, 0)).not.toThrow();
});
it('should throw error if disallow method is used', () => {
const code = [
"// Add a new field called 'myNewField' to the JSON of the item",
'$input.item.json.myNewField = 1;',
'const xxx = $input.all()',
'return $input.item;',
].join('\n');
expect(() => validateNoDisallowedMethodsInRunForEach(code, 0)).toThrow();
});
});
});
@@ -0,0 +1,247 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { createResultOk, createResultError } from 'n8n-workflow';
import { JsTaskRunnerSandbox } from '../JsTaskRunnerSandbox';
describe('JsTaskRunnerSandbox', () => {
describe('runCodeForEachItem', () => {
it('should chunk the input items and execute the code for each chunk', async () => {
const jsCode = 'console.log($item);';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
executeFunctions.helpers = {
...executeFunctions.helpers,
normalizeItems: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
.mockImplementation((items: any) => (Array.isArray(items) ? items : [items])),
};
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions, 2);
let i = 1;
executeFunctions.startJob.mockResolvedValue(createResultOk([{ json: { item: i++ } }]));
const numInputItems = 5;
await sandbox.runCodeForEachItem(jsCode, numInputItems);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledTimes(3);
const calls = executeFunctions.startJob.mock.calls;
expect(calls).toEqual([
[
'javascript',
{
code: jsCode,
workflowMode,
nodeMode: 'runOnceForEachItem',
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 0, count: 2 },
additionalProperties: {},
},
0,
],
[
'javascript',
{
code: jsCode,
workflowMode,
nodeMode: 'runOnceForEachItem',
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 2, count: 2 },
additionalProperties: {},
},
0,
],
[
'javascript',
{
code: jsCode,
workflowMode,
nodeMode: 'runOnceForEachItem',
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 4, count: 1 },
additionalProperties: {},
},
0,
],
]);
});
});
describe('runCodeForTool', () => {
it('should execute code and return string result', async () => {
const jsCode = 'return "Hello World";';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
executeFunctions.helpers = {
...executeFunctions.helpers,
normalizeItems: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
.mockImplementation((items: any) => (Array.isArray(items) ? items : [items])),
};
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions);
const expectedResult = 'Hello World';
executeFunctions.startJob.mockResolvedValue(createResultOk(expectedResult));
const result = await sandbox.runCodeForTool(jsCode);
expect(result).toBe(expectedResult);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'javascript',
{
code: jsCode,
nodeMode,
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
additionalProperties: {},
},
0,
);
});
it('should handle execution errors by calling throwExecutionError', async () => {
const jsCode = 'throw new Error("execution failed");';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
executeFunctions.helpers = {
...executeFunctions.helpers,
normalizeItems: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
.mockImplementation((items: any) => (Array.isArray(items) ? items : [items])),
};
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions);
const executionError = { message: 'execution failed', stack: 'error stack' };
executeFunctions.startJob.mockResolvedValue(createResultError(executionError));
// Mock throwExecutionError to throw an error for testing
const throwExecutionErrorModule = await import('../throw-execution-error');
const throwExecutionErrorSpy = jest
.spyOn(throwExecutionErrorModule, 'throwExecutionError')
.mockImplementation(() => {
throw new Error('Execution failed');
});
await expect(sandbox.runCodeForTool(jsCode)).rejects.toThrow('Execution failed');
expect(throwExecutionErrorSpy).toHaveBeenCalledWith(executionError);
});
});
describe('runCode', () => {
it('should execute code and return typed result', async () => {
const jsCode = 'return { sorted: [3, 2, 1].sort() };';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions);
const expectedResult = { sorted: [1, 2, 3] };
executeFunctions.startJob.mockResolvedValue(createResultOk(expectedResult));
const result = await sandbox.runCode<{ sorted: number[] }>(jsCode);
expect(result).toEqual(expectedResult);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'javascript',
{
code: jsCode,
nodeMode: 'runCode',
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
additionalProperties: {},
},
0,
);
});
it('should pass additionalProperties to the job', async () => {
const jsCode = 'return items.sort();';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
const additionalProperties = { items: [3, 1, 2], customOption: true };
const sandbox = new JsTaskRunnerSandbox(
workflowMode,
executeFunctions,
1000,
additionalProperties,
);
executeFunctions.startJob.mockResolvedValue(createResultOk([1, 2, 3]));
await sandbox.runCode<number[]>(jsCode);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'javascript',
{
code: jsCode,
nodeMode: 'runCode',
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
additionalProperties,
},
0,
);
});
it('should handle execution errors by calling throwExecutionError', async () => {
const jsCode = 'throw new Error("sort failed");';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions);
const executionError = { message: 'sort failed', stack: 'error stack' };
executeFunctions.startJob.mockResolvedValue(createResultError(executionError));
// Mock throwExecutionError to throw an error for testing
const throwExecutionErrorModule = await import('../throw-execution-error');
const throwExecutionErrorSpy = jest
.spyOn(throwExecutionErrorModule, 'throwExecutionError')
.mockImplementation(() => {
throw new Error('Execution failed');
});
await expect(sandbox.runCode(jsCode)).rejects.toThrow('Execution failed');
expect(throwExecutionErrorSpy).toHaveBeenCalledWith(executionError);
});
it('should handle error result without error property', async () => {
const jsCode = 'return null;';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
const sandbox = new JsTaskRunnerSandbox(workflowMode, executeFunctions);
// Simulate an error result without the 'error' property
executeFunctions.startJob.mockResolvedValue({ ok: false } as ReturnType<
typeof createResultError
>);
// Mock throwExecutionError to throw an error for testing
const throwExecutionErrorModule = await import('../throw-execution-error');
const throwExecutionErrorSpy = jest
.spyOn(throwExecutionErrorModule, 'throwExecutionError')
.mockImplementation(() => {
throw new Error('Execution failed');
});
await expect(sandbox.runCode(jsCode)).rejects.toThrow('Execution failed');
expect(throwExecutionErrorSpy).toHaveBeenCalledWith({});
});
});
});
@@ -0,0 +1,295 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { createResultOk, createResultError, NodeOperationError } from 'n8n-workflow';
import { PythonTaskRunnerSandbox } from '../PythonTaskRunnerSandbox';
const createNormalizeItemsMock = () =>
jest.fn().mockImplementation((items: any) => {
const itemsArray = Array.isArray(items) ? items : [items];
return itemsArray.map((item: any) => {
if (item.json !== undefined) {
return item;
}
return { json: item };
});
});
const createMockExecuteFunctions = (inputData: any[] = []) => {
const executeFunctions = mock<IExecuteFunctions>();
executeFunctions.helpers = {
...executeFunctions.helpers,
normalizeItems: createNormalizeItemsMock(),
};
executeFunctions.getNode.mockReturnValue({
id: 'node-id',
name: 'Code',
type: 'n8n-nodes-base.code',
typeVersion: 1,
position: [0, 0],
parameters: {},
});
executeFunctions.getWorkflow.mockReturnValue({
id: 'workflow-id',
name: 'Test Workflow',
active: false,
});
executeFunctions.getInputData.mockReturnValue(inputData);
return executeFunctions;
};
describe('PythonTaskRunnerSandbox', () => {
describe('runUsingIncomingItems', () => {
it('should call validateRunCodeAllItems for runOnceForAllItems mode', async () => {
const pythonCode = 'return [{"foo": "bar"}]';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([{ json: { test: 'data' } }]);
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
);
const mockResult = [{ foo: 'bar' }];
executeFunctions.startJob.mockResolvedValue(createResultOk(mockResult));
const result = await sandbox.runUsingIncomingItems();
expect(executeFunctions.startJob).toHaveBeenCalledTimes(1);
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'python',
{
code: pythonCode,
nodeMode,
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
items: [{ json: { test: 'data' } }],
nodeId: 'node-id',
nodeName: 'Code',
workflowId: 'workflow-id',
workflowName: 'Test Workflow',
},
0,
);
expect(executeFunctions.helpers.normalizeItems).toHaveBeenCalledWith(mockResult);
expect(result).toEqual([{ json: { foo: 'bar' } }]);
});
it('should call validateRunCodeEachItem for runOnceForEachItem mode', async () => {
const pythonCode = 'return {"foo": "bar"}';
const nodeMode = 'runOnceForEachItem';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([
{ json: { test: 'data1' } },
{ json: { test: 'data2' } },
]);
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
);
const mockResult = [
{ json: { foo: 'bar' }, pairedItem: { item: 0 } },
{ json: { foo: 'bar' }, pairedItem: { item: 1 } },
];
executeFunctions.startJob.mockResolvedValue(createResultOk(mockResult));
const result = await sandbox.runUsingIncomingItems();
expect(executeFunctions.startJob).toHaveBeenCalledTimes(1);
expect(executeFunctions.helpers.normalizeItems).toHaveBeenCalledTimes(2);
expect(result).toHaveLength(2);
expect(result[0]).toHaveProperty('json');
expect(result[0]).toHaveProperty('pairedItem');
});
it('should handle execution errors by calling throwExecutionError', async () => {
const pythonCode = 'raise ValueError("test error")';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
);
const executionError = { message: 'test error', stack: 'error stack' };
executeFunctions.startJob.mockResolvedValue(createResultError(executionError));
const throwExecutionErrorModule = await import('../throw-execution-error');
const throwExecutionErrorSpy = jest
.spyOn(throwExecutionErrorModule, 'throwExecutionError')
.mockImplementation(() => {
throw new Error('Execution failed');
});
await expect(sandbox.runUsingIncomingItems()).rejects.toThrow('Execution failed');
expect(throwExecutionErrorSpy).toHaveBeenCalledWith(executionError);
});
it('should throw NodeOperationError when pythonCode is undefined', async () => {
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const sandbox = new PythonTaskRunnerSandbox(
undefined as unknown as string,
nodeMode,
workflowMode,
executeFunctions,
);
await expect(sandbox.runUsingIncomingItems()).rejects.toThrow(NodeOperationError);
await expect(sandbox.runUsingIncomingItems()).rejects.toThrow(
'No Python code found to execute',
);
expect(executeFunctions.startJob).not.toHaveBeenCalled();
});
});
describe('runCodeForTool', () => {
it('should pass query and empty items to the runner', async () => {
const pythonCode = 'return _query.upper()';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const query = 'hello world';
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
{ query },
);
executeFunctions.startJob.mockResolvedValue(createResultOk('HELLO WORLD'));
const result = await sandbox.runCodeForTool();
expect(executeFunctions.startJob).toHaveBeenCalledTimes(1);
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'python',
{
code: pythonCode,
nodeMode: 'runOnceForAllItems',
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
items: [],
nodeId: 'node-id',
nodeName: 'Code',
workflowId: 'workflow-id',
workflowName: 'Test Workflow',
query,
},
0,
);
expect(result).toBe('HELLO WORLD');
});
it('should pass structured query object to the runner', async () => {
const pythonCode = 'return f"{_query["name"]} is {_query["age"]}"';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const query = { name: 'Alice', age: 30 };
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
{ query },
);
executeFunctions.startJob.mockResolvedValue(createResultOk('Alice is 30'));
const result = await sandbox.runCodeForTool();
expect(executeFunctions.startJob).toHaveBeenCalledWith(
'python',
expect.objectContaining({ query, items: [] }),
0,
);
expect(result).toBe('Alice is 30');
});
it('should return result without validation', async () => {
const pythonCode = 'return 42';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
{ query: 'test' },
);
executeFunctions.startJob.mockResolvedValue(createResultOk(42));
const result = await sandbox.runCodeForTool();
// Should return raw number, not wrapped in INodeExecutionData
expect(result).toBe(42);
expect(executeFunctions.helpers.normalizeItems).not.toHaveBeenCalled();
});
it('should handle execution errors by calling throwExecutionError', async () => {
const pythonCode = 'raise ValueError("tool error")';
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const sandbox = new PythonTaskRunnerSandbox(
pythonCode,
nodeMode,
workflowMode,
executeFunctions,
{ query: 'test' },
);
const executionError = { message: 'tool error', stack: 'error stack' };
executeFunctions.startJob.mockResolvedValue(createResultError(executionError));
const throwExecutionErrorModule = await import('../throw-execution-error');
const throwExecutionErrorSpy = jest
.spyOn(throwExecutionErrorModule, 'throwExecutionError')
.mockImplementation(() => {
throw new Error('Tool execution failed');
});
await expect(sandbox.runCodeForTool()).rejects.toThrow('Tool execution failed');
expect(throwExecutionErrorSpy).toHaveBeenCalledWith(executionError);
});
it('should throw NodeOperationError when pythonCode is undefined', async () => {
const nodeMode = 'runOnceForAllItems';
const workflowMode = 'manual';
const executeFunctions = createMockExecuteFunctions([]);
const sandbox = new PythonTaskRunnerSandbox(
undefined as unknown as string,
nodeMode,
workflowMode,
executeFunctions,
{ query: 'test' },
);
await expect(sandbox.runCodeForTool()).rejects.toThrow(NodeOperationError);
await expect(sandbox.runCodeForTool()).rejects.toThrow('No Python code found to execute');
expect(executeFunctions.startJob).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,46 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { addPostExecutionWarning } from '../utils';
describe('addPostExecutionWarning', () => {
const context = mock<IExecuteFunctions>();
const inputItemsLength = 2;
beforeEach(() => jest.resetAllMocks());
it('should add execution hints when returnData length differs from inputItemsLength', () => {
const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }];
addPostExecutionWarning(context, returnData, inputItemsLength);
expect(context.addExecutionHints).toHaveBeenCalledWith({
message:
'To make sure expressions after this node work, return the input items that produced each output item. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
location: 'outputPane',
});
});
it('should add execution hints when any item has undefined pairedItem', () => {
const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }, { json: {} }];
addPostExecutionWarning(context, returnData, inputItemsLength);
expect(context.addExecutionHints).toHaveBeenCalledWith({
message:
'To make sure expressions after this node work, return the input items that produced each output item. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
location: 'outputPane',
});
});
it('should not add execution hints when all items match inputItemsLength and have defined pairedItem', () => {
const returnData: INodeExecutionData[] = [
{ json: {}, pairedItem: 0 },
{ json: {}, pairedItem: 1 },
];
addPostExecutionWarning(context, returnData, inputItemsLength);
expect(context.addExecutionHints).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,77 @@
{
"name": "errors falsely flagged as internal",
"nodes": [
{
"parameters": {},
"id": "d2ff695c-4ca0-457d-ae9e-666d2dc53a53",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [360, 420],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "return {json: \"test\"}"
},
"id": "fc540f62-d671-49a2-b35d-aead4ed8bd10",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [620, 420],
"onError": "continueErrorOutput"
},
{
"parameters": {},
"id": "d8335277-61af-42d8-9cf5-02a8b85df42b",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [900, 500]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"error": "A 'json' property isn't an object [item 0]"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[],
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "1aa448f4-ac5f-497b-ae3c-62a5e9db63d4",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "TlJvElz9tvmByCh3",
"tags": []
}