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,278 @@
import { TaskRunnersConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INode,
INodeExecutionData,
INodeParameters,
INodeTypes,
ITaskDataConnections,
IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
createEnvProviderState,
createRunExecutionData,
NodeConnectionTypes,
Workflow,
} from 'n8n-workflow';
import { LocalTaskRequester } from '@/task-runners/task-managers/local-task-requester';
import { TaskRunnerModule } from '@/task-runners/task-runner-module';
import { PyTaskRunnerProcess } from '@/task-runners/task-runner-process-py';
jest.spyOn(PyTaskRunnerProcess, 'checkRequirements').mockResolvedValue('python');
/**
* Integration tests for the JS TaskRunner execution. Starts the TaskRunner
* as a child process and executes tasks on it via the broker.
*/
describe('JS TaskRunner execution on internal mode', () => {
const runnerConfig = Container.get(TaskRunnersConfig);
runnerConfig.mode = 'internal';
runnerConfig.port = 45678;
const taskRunnerModule = Container.get(TaskRunnerModule);
const taskRequester = Container.get(LocalTaskRequester);
/**
* Sets up task data that includes a workflow with manual trigger and a
* code node with the given JS code. The input data is a single item:
* ```json
* {
* "input": "item"
* }
* ```
*/
const newTaskData = (jsCode: string) => {
const taskSettings = {
code: jsCode,
nodeMode: 'runOnceForAllItems',
workflowMode: 'manual',
continueOnFail: false,
};
const codeNode: INode = {
parameters: {
jsCode,
},
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [200, 80],
id: 'b35fd455-32e4-4d52-b840-36aa28dd1910',
name: 'Code',
};
const workflow = new Workflow({
id: 'testWorkflow',
name: 'testWorkflow',
nodes: [
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [0, 0],
id: 'a39a566a-283a-433e-88bc-b3857aab706f',
name: 'ManualTrigger',
},
codeNode,
],
connections: {
ManualTrigger: {
main: [
[
{
node: 'Code',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
active: true,
nodeTypes: mock<INodeTypes>(),
});
const inputData: INodeExecutionData[] = [
{
json: {
input: 'item',
},
},
];
const inputConnections: ITaskDataConnections = {
main: [inputData],
};
const runExecutionData = createRunExecutionData({
startData: {},
resultData: {
runData: {
ManualTrigger: [
{
startTime: Date.now(),
executionIndex: 0,
executionTime: 0,
executionStatus: 'success',
source: [],
data: {
main: [inputData],
},
},
],
},
lastNodeExecuted: 'ManualTrigger',
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
});
return {
additionalData: mock<IWorkflowExecuteAdditionalData>(),
executeFunctions: mock<IExecuteFunctions>(),
taskSettings,
codeNode,
workflow,
inputData,
inputConnections,
runExecutionData,
envProviderState: createEnvProviderState(),
};
};
const runTaskWithCode = async (jsCode: string) => {
const {
additionalData,
taskSettings,
codeNode,
workflow,
inputData,
inputConnections,
runExecutionData,
executeFunctions,
envProviderState,
} = newTaskData(jsCode);
return await taskRequester.startTask<INodeExecutionData[], Error>(
additionalData,
'javascript',
taskSettings,
executeFunctions,
inputConnections,
codeNode,
workflow,
runExecutionData,
0,
0,
codeNode.name,
inputData,
mock<INodeParameters>(),
mock<WorkflowExecuteMode>(),
envProviderState,
);
};
describe('Basic code execution', () => {
beforeAll(async () => {
await taskRunnerModule.start();
});
afterAll(async () => {
await taskRunnerModule.stop();
});
it('should execute a simple JS task', async () => {
// Act
const result = await runTaskWithCode('return { hello: "world" }');
// Assert
expect(result).toEqual({
ok: true,
result: { hello: 'world' },
});
});
});
describe('Internal and external libs', () => {
beforeAll(async () => {
process.env.NODE_FUNCTION_ALLOW_BUILTIN = 'crypto';
process.env.NODE_FUNCTION_ALLOW_EXTERNAL = 'moment';
const { TaskBroker } = await import('@/task-runners/task-broker/task-broker.service');
Container.get(TaskBroker).stopDraining();
await taskRunnerModule.start();
});
afterAll(async () => {
await taskRunnerModule.stop();
});
it('should allow importing allowed internal module', async () => {
// Act
const result = await runTaskWithCode(`
const crypto = require("crypto");
return {
digest: crypto
.createHmac("sha256", Buffer.from("MySecretKey"))
.update("MESSAGE")
.digest("base64")
}
`);
expect(result).toEqual({
ok: true,
result: { digest: 'T09DMv7upNDKMD3Ht36FkwzrmWSgWpPiUNlcIX9/yaI=' },
});
});
it('should not allow importing disallowed internal module', async () => {
// Act
const result = await runTaskWithCode(`
const fs = require("fs");
return { file: fs.readFileSync("test.txt") }
`);
expect(result).toEqual({
ok: false,
error: expect.objectContaining({
message: "Module 'fs' is disallowed [line 2]",
}),
});
});
it('should allow importing allowed external module', async () => {
// Act
const result = await runTaskWithCode(`
const moment = require("moment");
return { time: moment("1995-12-25").format("YYYY-MM-DD") }
`);
expect(result).toEqual({
ok: true,
result: { time: '1995-12-25' },
});
});
it('should not allow importing disallowed external module', async () => {
// Act
const result = await runTaskWithCode(`
const lodash = require("lodash");
return [{ obj: lodash.cloneDeep({}) }]
`);
expect(result).toEqual({
ok: false,
error: expect.objectContaining({
message: "Module 'lodash' is disallowed [line 2]",
}),
});
});
});
});
@@ -0,0 +1,44 @@
import { setupBrokerTestServer } from '@test-integration/utils/task-broker-test-server';
describe('TaskBrokerServer', () => {
const { agent, server } = setupBrokerTestServer({
authToken: 'token',
mode: 'external',
});
beforeAll(async () => {
await server.start();
});
afterAll(async () => {
await server.stop();
});
describe('/healthz', () => {
it('should return 200', async () => {
await agent.get('/healthz').expect(200);
});
});
describe('/runners/_ws', () => {
it('should return 429 when too many requests are made', async () => {
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(429);
});
});
describe('/runners/auth', () => {
it('should return 429 when too many requests are made', async () => {
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(429);
});
});
});
@@ -0,0 +1,43 @@
import { TaskRunnersConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { DefaultTaskRunnerDisconnectAnalyzer } from '@/task-runners/default-task-runner-disconnect-analyzer';
import { MissingAuthTokenError } from '@/task-runners/errors/missing-auth-token.error';
import { TaskBrokerWsServer } from '@/task-runners/task-broker/task-broker-ws-server';
import { TaskRunnerModule } from '@/task-runners/task-runner-module';
describe('TaskRunnerModule in external mode', () => {
const runnerConfig = Container.get(TaskRunnersConfig);
runnerConfig.mode = 'external';
runnerConfig.port = 0;
runnerConfig.authToken = 'test';
const module = Container.get(TaskRunnerModule);
afterEach(async () => {
await module.stop();
});
describe('start', () => {
it('should throw if auth token is missing', async () => {
const runnerConfig = new TaskRunnersConfig();
runnerConfig.mode = 'external';
runnerConfig.authToken = '';
const module = new TaskRunnerModule(mock(), mock(), runnerConfig, mock());
await expect(module.start()).rejects.toThrowError(MissingAuthTokenError);
});
it('should start the task runner', async () => {
// Act
await module.start();
});
it('should use DefaultTaskRunnerDisconnectAnalyzer', () => {
const wsServer = Container.get(TaskBrokerWsServer);
expect(wsServer.getDisconnectAnalyzer()).toBeInstanceOf(DefaultTaskRunnerDisconnectAnalyzer);
});
});
});
@@ -0,0 +1,33 @@
import { TaskRunnersConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { InternalTaskRunnerDisconnectAnalyzer } from '@/task-runners/internal-task-runner-disconnect-analyzer';
import { TaskBrokerWsServer } from '@/task-runners/task-broker/task-broker-ws-server';
import { TaskRunnerModule } from '@/task-runners/task-runner-module';
import { PyTaskRunnerProcess } from '@/task-runners/task-runner-process-py';
jest.spyOn(PyTaskRunnerProcess, 'checkRequirements').mockResolvedValue('python');
describe('TaskRunnerModule in internal mode', () => {
const runnerConfig = Container.get(TaskRunnersConfig);
runnerConfig.port = 0; // Random port
runnerConfig.mode = 'internal';
const module = Container.get(TaskRunnerModule);
afterEach(async () => {
await module.stop();
});
describe('start', () => {
it('should start the task runner', async () => {
// Act
await module.start();
});
it('should use InternalTaskRunnerDisconnectAnalyzer', () => {
const wsServer = Container.get(TaskBrokerWsServer);
expect(wsServer.getDisconnectAnalyzer()).toBeInstanceOf(InternalTaskRunnerDisconnectAnalyzer);
});
});
});
@@ -0,0 +1,117 @@
import { Container } from '@n8n/di';
import { TaskBrokerWsServer } from '@/task-runners/task-broker/task-broker-ws-server';
import { TaskBroker } from '@/task-runners/task-broker/task-broker.service';
import { JsTaskRunnerProcess } from '@/task-runners/task-runner-process-js';
import { TaskRunnerProcessRestartLoopDetector } from '@/task-runners/task-runner-process-restart-loop-detector';
import { retryUntil } from '@test-integration/retry-until';
import { setupBrokerTestServer } from '@test-integration/utils/task-broker-test-server';
describe('TaskRunnerProcess', () => {
const { config, server: taskRunnerServer } = setupBrokerTestServer({
mode: 'internal',
});
const runnerProcess = Container.get(JsTaskRunnerProcess);
const taskBroker = Container.get(TaskBroker);
const taskRunnerService = Container.get(TaskBrokerWsServer);
beforeAll(async () => {
await taskRunnerServer.start();
// Set the port to the actually used port
config.port = taskRunnerServer.port;
});
afterAll(async () => {
await taskRunnerServer.stop();
});
afterEach(async () => {
await runnerProcess.stop();
});
const getNumConnectedRunners = () => taskRunnerService.runnerConnections.size;
const getNumRegisteredRunners = () => taskBroker.getKnownRunners().size;
it('should start and connect the task runner', async () => {
// Act
await runnerProcess.start();
// Assert
expect(runnerProcess.isRunning).toBeTruthy();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumRegisteredRunners()).toBe(1);
});
it('should stop an disconnect the task runner', async () => {
// Arrange
await runnerProcess.start();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumRegisteredRunners()).toBe(1);
// Act
await runnerProcess.stop();
// Assert
// Wait until the runner has disconnected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(0));
expect(runnerProcess.isRunning).toBeFalsy();
expect(getNumRegisteredRunners()).toBe(0);
});
it('should restart the task runner if it exits', async () => {
// Arrange
await runnerProcess.start();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
const processId = runnerProcess.pid;
// Act
// @ts-expect-error private property
runnerProcess.process?.kill('SIGKILL');
// Wait until the runner has exited
await runnerProcess.runPromise;
// Assert
// Wait until the runner has connected again
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumConnectedRunners()).toBe(1);
expect(getNumRegisteredRunners()).toBe(1);
expect(runnerProcess.pid).not.toBe(processId);
});
it('should work together with restart loop detector', async () => {
// Arrange
const restartLoopDetector = new TaskRunnerProcessRestartLoopDetector(runnerProcess);
let restartLoopDetectedEventEmitted = false;
restartLoopDetector.once('restart-loop-detected', () => {
restartLoopDetectedEventEmitted = true;
});
// Act
await runnerProcess.start();
// Simulate a restart loop
for (let i = 0; i < 5; i++) {
await retryUntil(() => {
expect(runnerProcess.pid).toBeDefined();
});
// @ts-expect-error private property
runnerProcess.process?.kill();
await new Promise((resolve) => {
runnerProcess.once('exit', resolve);
});
}
// Assert
expect(restartLoopDetectedEventEmitted).toBe(true);
});
});
@@ -0,0 +1,44 @@
import { setupBrokerTestServer } from '@test-integration/utils/task-broker-test-server';
describe('TaskBrokerServer', () => {
const { agent, server } = setupBrokerTestServer({
authToken: 'token',
mode: 'external',
});
beforeAll(async () => {
await server.start();
});
afterAll(async () => {
await server.stop();
});
describe('/healthz', () => {
it('should return 200', async () => {
await agent.get('/healthz').expect(200);
});
});
describe('/runners/_ws', () => {
it('should return 429 when too many requests are made', async () => {
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(401);
await agent.post('/runners/_ws').send({}).expect(429);
});
});
describe('/runners/auth', () => {
it('should return 429 when too many requests are made', async () => {
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(403);
await agent.post('/runners/auth').send({ token: 'invalid' }).expect(429);
});
});
});