feat: add comprehensive performance benchmark tracking system

- Create benchmark test suites for critical operations:
  - Node loading performance
  - Database query performance
  - Search operations performance
  - Validation performance
  - MCP tool execution performance

- Add GitHub Actions workflow for benchmark tracking:
  - Runs on push to main and PRs
  - Uses github-action-benchmark for historical tracking
  - Comments on PRs with performance results
  - Alerts on >10% performance regressions
  - Stores results in GitHub Pages

- Create benchmark infrastructure:
  - Custom Vitest benchmark configuration
  - JSON reporter for CI results
  - Result formatter for github-action-benchmark
  - Performance threshold documentation

- Add supporting utilities:
  - SQLiteStorageService for benchmark database setup
  - MCPEngine wrapper for testing MCP tools
  - Test factories for generating benchmark data
  - Enhanced NodeRepository with benchmark methods

- Document benchmark system:
  - Comprehensive benchmark guide in docs/BENCHMARKS.md
  - Performance thresholds in .github/BENCHMARK_THRESHOLDS.md
  - README for benchmarks directory
  - Integration with existing test suite

The benchmark system will help monitor performance over time and catch regressions before they reach production.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-28 22:45:09 +02:00
parent 0252788dd6
commit b5210e5963
52 changed files with 6843 additions and 16 deletions

View File

@@ -0,0 +1,153 @@
# n8n-nodes-base Mock
This directory contains comprehensive mocks for n8n packages used in unit tests.
## n8n-nodes-base Mock
The `n8n-nodes-base.ts` mock provides a complete testing infrastructure for code that depends on n8n nodes.
### Features
1. **Pre-configured Node Types**
- `webhook` - Trigger node with webhook functionality
- `httpRequest` - HTTP request node with mock responses
- `slack` - Slack integration with all resources and operations
- `function` - JavaScript code execution node
- `noOp` - Pass-through utility node
- `merge` - Data stream merging node
- `if` - Conditional branching node
- `switch` - Multi-output routing node
2. **Flexible Mock Behavior**
- Override node execution logic
- Customize node descriptions
- Add custom nodes dynamically
- Reset all mocks between tests
### Basic Usage
```typescript
import { vi } from 'vitest';
// Mock the module
vi.mock('n8n-nodes-base', () => import('../__mocks__/n8n-nodes-base'));
// In your test
import { getNodeTypes, mockNodeBehavior, resetAllMocks } from '../__mocks__/n8n-nodes-base';
describe('Your test', () => {
beforeEach(() => {
resetAllMocks();
});
it('should get node description', () => {
const registry = getNodeTypes();
const slackNode = registry.getByName('slack');
expect(slackNode?.description.name).toBe('slack');
});
});
```
### Advanced Usage
#### Override Node Behavior
```typescript
mockNodeBehavior('httpRequest', {
execute: async function(this: IExecuteFunctions) {
return [[{ json: { custom: 'response' } }]];
}
});
```
#### Add Custom Nodes
```typescript
import { registerMockNode } from '../__mocks__/n8n-nodes-base';
const customNode = {
description: {
displayName: 'Custom Node',
name: 'customNode',
group: ['transform'],
version: 1,
description: 'A custom test node',
defaults: { name: 'Custom' },
inputs: ['main'],
outputs: ['main'],
properties: []
},
execute: async function() {
return [[{ json: { result: 'custom' } }]];
}
};
registerMockNode('customNode', customNode);
```
#### Mock Execution Context
```typescript
const mockContext = {
getInputData: vi.fn(() => [{ json: { test: 'data' } }]),
getNodeParameter: vi.fn((name: string) => {
const params = {
method: 'POST',
url: 'https://api.example.com'
};
return params[name];
}),
getCredentials: vi.fn(async () => ({ apiKey: 'test-key' })),
helpers: {
returnJsonArray: vi.fn(),
httpRequest: vi.fn()
}
};
const result = await node.execute.call(mockContext);
```
### Mock Structure
Each mock node implements the `INodeType` interface with:
- `description`: Complete node metadata including properties, inputs/outputs, credentials
- `execute`: Mock implementation for regular nodes (returns `INodeExecutionData[][]`)
- `webhook`: Mock implementation for trigger nodes (returns webhook data)
### Testing Patterns
1. **Unit Testing Node Logic**
```typescript
const node = registry.getByName('slack');
const result = await node.execute.call(mockContext);
expect(result[0][0].json.ok).toBe(true);
```
2. **Testing Node Properties**
```typescript
const node = registry.getByName('httpRequest');
const methodProp = node.description.properties.find(p => p.name === 'method');
expect(methodProp.options).toHaveLength(6);
```
3. **Testing Conditional Nodes**
```typescript
const ifNode = registry.getByName('if');
const [trueOutput, falseOutput] = await ifNode.execute.call(mockContext);
expect(trueOutput).toHaveLength(2);
expect(falseOutput).toHaveLength(1);
```
### Utilities
- `resetAllMocks()` - Clear all mock function calls
- `mockNodeBehavior(name, overrides)` - Override specific node behavior
- `registerMockNode(name, node)` - Add new mock nodes
- `getNodeTypes()` - Get the node registry with `getByName` and `getByNameAndVersion`
### See Also
- `tests/unit/examples/using-n8n-nodes-base-mock.test.ts` - Complete usage examples
- `tests/unit/__mocks__/n8n-nodes-base.test.ts` - Mock test coverage

View File

@@ -0,0 +1,224 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { getNodeTypes, mockNodeBehavior, resetAllMocks, registerMockNode } from './n8n-nodes-base';
describe('n8n-nodes-base mock', () => {
beforeEach(() => {
resetAllMocks();
});
describe('getNodeTypes', () => {
it('should return node types registry', () => {
const registry = getNodeTypes();
expect(registry).toBeDefined();
expect(registry.getByName).toBeDefined();
expect(registry.getByNameAndVersion).toBeDefined();
});
it('should retrieve webhook node', () => {
const registry = getNodeTypes();
const webhookNode = registry.getByName('webhook');
expect(webhookNode).toBeDefined();
expect(webhookNode?.description.name).toBe('webhook');
expect(webhookNode?.description.group).toContain('trigger');
expect(webhookNode?.webhook).toBeDefined();
});
it('should retrieve httpRequest node', () => {
const registry = getNodeTypes();
const httpNode = registry.getByName('httpRequest');
expect(httpNode).toBeDefined();
expect(httpNode?.description.name).toBe('httpRequest');
expect(httpNode?.description.version).toBe(3);
expect(httpNode?.execute).toBeDefined();
});
it('should retrieve slack node', () => {
const registry = getNodeTypes();
const slackNode = registry.getByName('slack');
expect(slackNode).toBeDefined();
expect(slackNode?.description.credentials).toHaveLength(1);
expect(slackNode?.description.credentials?.[0].name).toBe('slackApi');
});
});
describe('node execution', () => {
it('should execute webhook node', async () => {
const registry = getNodeTypes();
const webhookNode = registry.getByName('webhook');
const mockContext = {
getWebhookName: vi.fn(() => 'default'),
getBodyData: vi.fn(() => ({ test: 'data' })),
getHeaderData: vi.fn(() => ({ 'content-type': 'application/json' })),
getQueryData: vi.fn(() => ({ query: 'param' })),
getRequestObject: vi.fn(),
getResponseObject: vi.fn(),
helpers: {
returnJsonArray: vi.fn((data) => [{ json: data }]),
},
};
const result = await webhookNode?.webhook?.call(mockContext);
expect(result).toBeDefined();
expect(result.workflowData).toBeDefined();
expect(result.workflowData[0]).toHaveLength(1);
expect(result.workflowData[0][0].json).toMatchObject({
headers: { 'content-type': 'application/json' },
params: { query: 'param' },
body: { test: 'data' },
});
});
it('should execute httpRequest node', async () => {
const registry = getNodeTypes();
const httpNode = registry.getByName('httpRequest');
const mockContext = {
getInputData: vi.fn(() => [{ json: { test: 'input' } }]),
getNodeParameter: vi.fn((name: string) => {
if (name === 'method') return 'POST';
if (name === 'url') return 'https://api.example.com';
return '';
}),
helpers: {
returnJsonArray: vi.fn((data) => [{ json: data }]),
httpRequest: vi.fn(),
},
};
const result = await httpNode?.execute?.call(mockContext);
expect(result).toBeDefined();
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
expect(result[0][0].json).toMatchObject({
statusCode: 200,
body: {
success: true,
method: 'POST',
url: 'https://api.example.com',
},
});
});
});
describe('mockNodeBehavior', () => {
it('should override node execution behavior', async () => {
const customExecute = vi.fn(async function() {
return [[{ json: { custom: 'response' } }]];
});
mockNodeBehavior('httpRequest', {
execute: customExecute,
});
const registry = getNodeTypes();
const httpNode = registry.getByName('httpRequest');
const mockContext = {
getInputData: vi.fn(() => []),
getNodeParameter: vi.fn(),
};
const result = await httpNode?.execute?.call(mockContext);
expect(customExecute).toHaveBeenCalled();
expect(result).toEqual([[{ json: { custom: 'response' } }]]);
});
it('should override node description', () => {
mockNodeBehavior('slack', {
description: {
displayName: 'Custom Slack',
version: 3,
},
});
const registry = getNodeTypes();
const slackNode = registry.getByName('slack');
expect(slackNode?.description.displayName).toBe('Custom Slack');
expect(slackNode?.description.version).toBe(3);
expect(slackNode?.description.name).toBe('slack'); // Original preserved
});
});
describe('registerMockNode', () => {
it('should register custom node', () => {
const customNode = {
description: {
displayName: 'Custom Node',
name: 'customNode',
group: ['transform'],
version: 1,
description: 'A custom test node',
defaults: { name: 'Custom' },
inputs: ['main'],
outputs: ['main'],
properties: [],
},
execute: vi.fn(async function() {
return [[{ json: { custom: true } }]];
}),
};
registerMockNode('customNode', customNode);
const registry = getNodeTypes();
const retrievedNode = registry.getByName('customNode');
expect(retrievedNode).toBe(customNode);
expect(retrievedNode?.description.name).toBe('customNode');
});
});
describe('conditional nodes', () => {
it('should execute if node with two outputs', async () => {
const registry = getNodeTypes();
const ifNode = registry.getByName('if');
const mockContext = {
getInputData: vi.fn(() => [
{ json: { value: 1 } },
{ json: { value: 2 } },
{ json: { value: 3 } },
{ json: { value: 4 } },
]),
getNodeParameter: vi.fn(),
};
const result = await ifNode?.execute?.call(mockContext);
expect(result).toHaveLength(2); // true and false outputs
expect(result[0]).toHaveLength(2); // even indices
expect(result[1]).toHaveLength(2); // odd indices
});
it('should execute switch node with multiple outputs', async () => {
const registry = getNodeTypes();
const switchNode = registry.getByName('switch');
const mockContext = {
getInputData: vi.fn(() => [
{ json: { value: 1 } },
{ json: { value: 2 } },
{ json: { value: 3 } },
{ json: { value: 4 } },
]),
getNodeParameter: vi.fn(),
};
const result = await switchNode?.execute?.call(mockContext);
expect(result).toHaveLength(4); // 4 outputs
expect(result[0]).toHaveLength(1); // item 0
expect(result[1]).toHaveLength(1); // item 1
expect(result[2]).toHaveLength(1); // item 2
expect(result[3]).toHaveLength(1); // item 3
});
});
});

View File

@@ -0,0 +1,655 @@
import { vi } from 'vitest';
// Mock types that match n8n-workflow
interface INodeExecutionData {
json: any;
binary?: any;
pairedItem?: any;
}
interface IExecuteFunctions {
getInputData(): INodeExecutionData[];
getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any;
getCredentials(type: string): Promise<any>;
helpers: {
returnJsonArray(data: any): INodeExecutionData[];
httpRequest(options: any): Promise<any>;
webhook(): any;
};
}
interface IWebhookFunctions {
getWebhookName(): string;
getBodyData(): any;
getHeaderData(): any;
getQueryData(): any;
getRequestObject(): any;
getResponseObject(): any;
helpers: {
returnJsonArray(data: any): INodeExecutionData[];
};
}
interface INodeTypeDescription {
displayName: string;
name: string;
group: string[];
version: number;
description: string;
defaults: { name: string };
inputs: string[];
outputs: string[];
credentials?: any[];
webhooks?: any[];
properties: any[];
icon?: string;
subtitle?: string;
}
interface INodeType {
description: INodeTypeDescription;
execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
webhook?(this: IWebhookFunctions): Promise<any>;
trigger?(this: any): Promise<void>;
poll?(this: any): Promise<INodeExecutionData[][] | null>;
}
// Base mock node implementation
class BaseMockNode implements INodeType {
description: INodeTypeDescription;
execute: any;
webhook: any;
constructor(description: INodeTypeDescription, execute?: any, webhook?: any) {
this.description = description;
this.execute = execute ? vi.fn(execute) : undefined;
this.webhook = webhook ? vi.fn(webhook) : undefined;
}
}
// Mock implementations for each node type
const mockWebhookNode = new BaseMockNode(
{
displayName: 'Webhook',
name: 'webhook',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when a webhook is called',
defaults: { name: 'Webhook' },
inputs: [],
outputs: ['main'],
webhooks: [
{
name: 'default',
httpMethod: '={{$parameter["httpMethod"]}}',
path: '={{$parameter["path"]}}',
responseMode: '={{$parameter["responseMode"]}}',
}
],
properties: [
{
displayName: 'Path',
name: 'path',
type: 'string',
default: 'webhook',
required: true,
description: 'The path to listen on',
},
{
displayName: 'HTTP Method',
name: 'httpMethod',
type: 'options',
default: 'GET',
options: [
{ name: 'GET', value: 'GET' },
{ name: 'POST', value: 'POST' },
{ name: 'PUT', value: 'PUT' },
{ name: 'DELETE', value: 'DELETE' },
{ name: 'HEAD', value: 'HEAD' },
{ name: 'PATCH', value: 'PATCH' },
],
},
{
displayName: 'Response Mode',
name: 'responseMode',
type: 'options',
default: 'onReceived',
options: [
{ name: 'On Received', value: 'onReceived' },
{ name: 'Last Node', value: 'lastNode' },
],
},
],
},
undefined,
async function webhook(this: IWebhookFunctions) {
const returnData: INodeExecutionData[] = [];
returnData.push({
json: {
headers: this.getHeaderData(),
params: this.getQueryData(),
body: this.getBodyData(),
}
});
return {
workflowData: [returnData],
};
}
);
const mockHttpRequestNode = new BaseMockNode(
{
displayName: 'HTTP Request',
name: 'httpRequest',
group: ['transform'],
version: 3,
description: 'Makes an HTTP request and returns the response',
defaults: { name: 'HTTP Request' },
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Method',
name: 'method',
type: 'options',
default: 'GET',
options: [
{ name: 'GET', value: 'GET' },
{ name: 'POST', value: 'POST' },
{ name: 'PUT', value: 'PUT' },
{ name: 'DELETE', value: 'DELETE' },
{ name: 'HEAD', value: 'HEAD' },
{ name: 'PATCH', value: 'PATCH' },
],
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
required: true,
placeholder: 'https://example.com',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
default: 'none',
options: [
{ name: 'None', value: 'none' },
{ name: 'Basic Auth', value: 'basicAuth' },
{ name: 'Digest Auth', value: 'digestAuth' },
{ name: 'Header Auth', value: 'headerAuth' },
{ name: 'OAuth1', value: 'oAuth1' },
{ name: 'OAuth2', value: 'oAuth2' },
],
},
{
displayName: 'Response Format',
name: 'responseFormat',
type: 'options',
default: 'json',
options: [
{ name: 'JSON', value: 'json' },
{ name: 'String', value: 'string' },
{ name: 'File', value: 'file' },
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Body Content Type',
name: 'bodyContentType',
type: 'options',
default: 'json',
options: [
{ name: 'JSON', value: 'json' },
{ name: 'Form Data', value: 'formData' },
{ name: 'Form URL Encoded', value: 'form-urlencoded' },
{ name: 'Raw', value: 'raw' },
],
},
{
displayName: 'Headers',
name: 'headers',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
},
{
displayName: 'Query Parameters',
name: 'queryParameters',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
},
],
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const method = this.getNodeParameter('method', i) as string;
const url = this.getNodeParameter('url', i) as string;
// Mock response
const response = {
statusCode: 200,
headers: {},
body: { success: true, method, url },
};
returnData.push({
json: response,
});
}
return [returnData];
}
);
const mockSlackNode = new BaseMockNode(
{
displayName: 'Slack',
name: 'slack',
group: ['output'],
version: 2,
description: 'Send messages to Slack',
defaults: { name: 'Slack' },
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'slackApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
default: 'message',
options: [
{ name: 'Channel', value: 'channel' },
{ name: 'Message', value: 'message' },
{ name: 'User', value: 'user' },
{ name: 'File', value: 'file' },
],
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ['message'],
},
},
default: 'post',
options: [
{ name: 'Post', value: 'post' },
{ name: 'Update', value: 'update' },
{ name: 'Delete', value: 'delete' },
],
},
{
displayName: 'Channel',
name: 'channel',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
displayOptions: {
show: {
resource: ['message'],
operation: ['post'],
},
},
default: '',
required: true,
},
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: ['message'],
operation: ['post'],
},
},
default: '',
required: true,
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter('resource', i) as string;
const operation = this.getNodeParameter('operation', i) as string;
// Mock response
const response = {
ok: true,
channel: this.getNodeParameter('channel', i, '') as string,
ts: Date.now().toString(),
message: {
text: this.getNodeParameter('text', i, '') as string,
},
};
returnData.push({
json: response,
});
}
return [returnData];
}
);
const mockFunctionNode = new BaseMockNode(
{
displayName: 'Function',
name: 'function',
group: ['transform'],
version: 1,
description: 'Execute custom JavaScript code',
defaults: { name: 'Function' },
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'JavaScript Code',
name: 'functionCode',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
codeAutocomplete: 'function',
editor: 'code',
rows: 10,
},
default: 'return items;',
description: 'JavaScript code to execute',
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const functionCode = this.getNodeParameter('functionCode', 0) as string;
// Simple mock - just return items
return [items];
}
);
const mockNoOpNode = new BaseMockNode(
{
displayName: 'No Operation',
name: 'noOp',
group: ['utility'],
version: 1,
description: 'Does nothing',
defaults: { name: 'No Op' },
inputs: ['main'],
outputs: ['main'],
properties: [],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
return [this.getInputData()];
}
);
const mockMergeNode = new BaseMockNode(
{
displayName: 'Merge',
name: 'merge',
group: ['transform'],
version: 2,
description: 'Merge multiple data streams',
defaults: { name: 'Merge' },
inputs: ['main', 'main'],
outputs: ['main'],
properties: [
{
displayName: 'Mode',
name: 'mode',
type: 'options',
default: 'append',
options: [
{ name: 'Append', value: 'append' },
{ name: 'Merge By Index', value: 'mergeByIndex' },
{ name: 'Merge By Key', value: 'mergeByKey' },
{ name: 'Multiplex', value: 'multiplex' },
],
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const mode = this.getNodeParameter('mode', 0) as string;
// Mock merge - just return first input
return [this.getInputData(0)];
}
);
const mockIfNode = new BaseMockNode(
{
displayName: 'IF',
name: 'if',
group: ['transform'],
version: 1,
description: 'Conditional logic',
defaults: { name: 'IF' },
inputs: ['main'],
outputs: ['main', 'main'],
outputNames: ['true', 'false'],
properties: [
{
displayName: 'Conditions',
name: 'conditions',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'string',
displayName: 'String',
values: [
{
displayName: 'Value 1',
name: 'value1',
type: 'string',
default: '',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'equals',
options: [
{ name: 'Equals', value: 'equals' },
{ name: 'Not Equals', value: 'notEquals' },
{ name: 'Contains', value: 'contains' },
{ name: 'Not Contains', value: 'notContains' },
],
},
{
displayName: 'Value 2',
name: 'value2',
type: 'string',
default: '',
},
],
},
],
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const trueItems: INodeExecutionData[] = [];
const falseItems: INodeExecutionData[] = [];
// Mock condition - split 50/50
items.forEach((item, index) => {
if (index % 2 === 0) {
trueItems.push(item);
} else {
falseItems.push(item);
}
});
return [trueItems, falseItems];
}
);
const mockSwitchNode = new BaseMockNode(
{
displayName: 'Switch',
name: 'switch',
group: ['transform'],
version: 1,
description: 'Route items based on conditions',
defaults: { name: 'Switch' },
inputs: ['main'],
outputs: ['main', 'main', 'main', 'main'],
properties: [
{
displayName: 'Mode',
name: 'mode',
type: 'options',
default: 'expression',
options: [
{ name: 'Expression', value: 'expression' },
{ name: 'Rules', value: 'rules' },
],
},
{
displayName: 'Output',
name: 'output',
type: 'options',
displayOptions: {
show: {
mode: ['expression'],
},
},
default: 'all',
options: [
{ name: 'All', value: 'all' },
{ name: 'First Match', value: 'firstMatch' },
],
},
],
},
async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
// Mock routing - distribute evenly across outputs
const outputs: INodeExecutionData[][] = [[], [], [], []];
items.forEach((item, index) => {
outputs[index % 4].push(item);
});
return outputs;
}
);
// Node registry
const nodeRegistry = new Map<string, INodeType>([
['webhook', mockWebhookNode],
['httpRequest', mockHttpRequestNode],
['slack', mockSlackNode],
['function', mockFunctionNode],
['noOp', mockNoOpNode],
['merge', mockMergeNode],
['if', mockIfNode],
['switch', mockSwitchNode],
]);
// Export mock functions
export const getNodeTypes = vi.fn(() => ({
getByName: vi.fn((name: string) => nodeRegistry.get(name)),
getByNameAndVersion: vi.fn((name: string, version: number) => nodeRegistry.get(name)),
}));
// Export individual node classes for direct import
export const Webhook = mockWebhookNode;
export const HttpRequest = mockHttpRequestNode;
export const Slack = mockSlackNode;
export const Function = mockFunctionNode;
export const NoOp = mockNoOpNode;
export const Merge = mockMergeNode;
export const If = mockIfNode;
export const Switch = mockSwitchNode;
// Test utility to override node behavior
export const mockNodeBehavior = (nodeName: string, overrides: Partial<INodeType>) => {
const existingNode = nodeRegistry.get(nodeName);
if (!existingNode) {
throw new Error(`Node ${nodeName} not found in registry`);
}
const updatedNode = new BaseMockNode(
{ ...existingNode.description, ...overrides.description },
overrides.execute || existingNode.execute,
overrides.webhook || existingNode.webhook
);
nodeRegistry.set(nodeName, updatedNode);
return updatedNode;
};
// Test utility to reset all mocks
export const resetAllMocks = () => {
getNodeTypes.mockClear();
nodeRegistry.forEach((node) => {
if (node.execute && vi.isMockFunction(node.execute)) {
node.execute.mockClear();
}
if (node.webhook && vi.isMockFunction(node.webhook)) {
node.webhook.mockClear();
}
});
};
// Test utility to add custom nodes
export const registerMockNode = (name: string, node: INodeType) => {
nodeRegistry.set(name, node);
};
// Export default for require() compatibility
export default {
getNodeTypes,
Webhook,
HttpRequest,
Slack,
Function,
NoOp,
Merge,
If,
Switch,
mockNodeBehavior,
resetAllMocks,
registerMockNode,
};