- 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>
227 lines
7.2 KiB
TypeScript
227 lines
7.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { getNodeTypes, mockNodeBehavior, resetAllMocks } from '../__mocks__/n8n-nodes-base';
|
|
|
|
// Example service that uses n8n-nodes-base
|
|
class WorkflowService {
|
|
async getNodeDescription(nodeName: string) {
|
|
const nodeTypes = getNodeTypes();
|
|
const node = nodeTypes.getByName(nodeName);
|
|
return node?.description;
|
|
}
|
|
|
|
async executeNode(nodeName: string, context: any) {
|
|
const nodeTypes = getNodeTypes();
|
|
const node = nodeTypes.getByName(nodeName);
|
|
|
|
if (!node?.execute) {
|
|
throw new Error(`Node ${nodeName} does not have an execute method`);
|
|
}
|
|
|
|
return node.execute.call(context);
|
|
}
|
|
|
|
async validateSlackMessage(channel: string, text: string) {
|
|
if (!channel || !text) {
|
|
throw new Error('Channel and text are required');
|
|
}
|
|
|
|
const nodeTypes = getNodeTypes();
|
|
const slackNode = nodeTypes.getByName('slack');
|
|
|
|
if (!slackNode) {
|
|
throw new Error('Slack node not found');
|
|
}
|
|
|
|
// Check if required properties exist
|
|
const channelProp = slackNode.description.properties.find(p => p.name === 'channel');
|
|
const textProp = slackNode.description.properties.find(p => p.name === 'text');
|
|
|
|
return !!(channelProp && textProp);
|
|
}
|
|
}
|
|
|
|
// Mock the module at the top level
|
|
vi.mock('n8n-nodes-base', () => ({
|
|
getNodeTypes: vi.fn(() => {
|
|
const { getNodeTypes } = require('../__mocks__/n8n-nodes-base');
|
|
return getNodeTypes();
|
|
})
|
|
}));
|
|
|
|
describe('WorkflowService with n8n-nodes-base mock', () => {
|
|
let service: WorkflowService;
|
|
|
|
beforeEach(() => {
|
|
resetAllMocks();
|
|
service = new WorkflowService();
|
|
});
|
|
|
|
describe('getNodeDescription', () => {
|
|
it('should get webhook node description', async () => {
|
|
const description = await service.getNodeDescription('webhook');
|
|
|
|
expect(description).toBeDefined();
|
|
expect(description?.name).toBe('webhook');
|
|
expect(description?.group).toContain('trigger');
|
|
expect(description?.webhooks).toBeDefined();
|
|
});
|
|
|
|
it('should get httpRequest node description', async () => {
|
|
const description = await service.getNodeDescription('httpRequest');
|
|
|
|
expect(description).toBeDefined();
|
|
expect(description?.name).toBe('httpRequest');
|
|
expect(description?.version).toBe(3);
|
|
|
|
const methodProp = description?.properties.find(p => p.name === 'method');
|
|
expect(methodProp).toBeDefined();
|
|
expect(methodProp?.options).toHaveLength(6);
|
|
});
|
|
});
|
|
|
|
describe('executeNode', () => {
|
|
it('should execute httpRequest node with custom response', async () => {
|
|
// Override the httpRequest node behavior for this test
|
|
mockNodeBehavior('httpRequest', {
|
|
execute: vi.fn(async function(this: any) {
|
|
const url = this.getNodeParameter('url', 0);
|
|
return [[{
|
|
json: {
|
|
statusCode: 200,
|
|
url,
|
|
customData: 'mocked response'
|
|
}
|
|
}]];
|
|
})
|
|
});
|
|
|
|
const mockContext = {
|
|
getInputData: vi.fn(() => [{ json: { input: 'data' } }]),
|
|
getNodeParameter: vi.fn((name: string) => {
|
|
if (name === 'url') return 'https://test.com/api';
|
|
return '';
|
|
})
|
|
};
|
|
|
|
const result = await service.executeNode('httpRequest', mockContext);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result[0][0].json).toMatchObject({
|
|
statusCode: 200,
|
|
url: 'https://test.com/api',
|
|
customData: 'mocked response'
|
|
});
|
|
});
|
|
|
|
it('should execute slack node and track calls', async () => {
|
|
const mockContext = {
|
|
getInputData: vi.fn(() => [{ json: { message: 'test' } }]),
|
|
getNodeParameter: vi.fn((name: string, index: number) => {
|
|
const params: Record<string, string> = {
|
|
resource: 'message',
|
|
operation: 'post',
|
|
channel: '#general',
|
|
text: 'Hello from test!'
|
|
};
|
|
return params[name] || '';
|
|
}),
|
|
getCredentials: vi.fn(async () => ({ token: 'mock-token' }))
|
|
};
|
|
|
|
const result = await service.executeNode('slack', mockContext);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result[0][0].json).toMatchObject({
|
|
ok: true,
|
|
channel: '#general',
|
|
message: {
|
|
text: 'Hello from test!'
|
|
}
|
|
});
|
|
|
|
// Verify the mock was called
|
|
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('channel', 0, '');
|
|
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('text', 0, '');
|
|
});
|
|
|
|
it('should throw error for non-executable node', async () => {
|
|
// Create a trigger-only node
|
|
mockNodeBehavior('webhook', {
|
|
execute: undefined // Remove execute method
|
|
});
|
|
|
|
await expect(
|
|
service.executeNode('webhook', {})
|
|
).rejects.toThrow('Node webhook does not have an execute method');
|
|
});
|
|
});
|
|
|
|
describe('validateSlackMessage', () => {
|
|
it('should validate slack message parameters', async () => {
|
|
const isValid = await service.validateSlackMessage('#general', 'Hello');
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should throw error for missing parameters', async () => {
|
|
await expect(
|
|
service.validateSlackMessage('', 'Hello')
|
|
).rejects.toThrow('Channel and text are required');
|
|
|
|
await expect(
|
|
service.validateSlackMessage('#general', '')
|
|
).rejects.toThrow('Channel and text are required');
|
|
});
|
|
|
|
it('should handle missing slack node', async () => {
|
|
// Override getNodeTypes to return undefined for slack
|
|
const getNodeTypes = vi.fn(() => ({
|
|
getByName: vi.fn((name: string) => {
|
|
if (name === 'slack') return undefined;
|
|
return null;
|
|
}),
|
|
getByNameAndVersion: vi.fn()
|
|
}));
|
|
|
|
vi.mocked(require('n8n-nodes-base').getNodeTypes).mockImplementation(getNodeTypes);
|
|
|
|
await expect(
|
|
service.validateSlackMessage('#general', 'Hello')
|
|
).rejects.toThrow('Slack node not found');
|
|
});
|
|
});
|
|
|
|
describe('complex workflow scenarios', () => {
|
|
it('should handle if node branching', async () => {
|
|
const mockContext = {
|
|
getInputData: vi.fn(() => [
|
|
{ json: { status: 'active' } },
|
|
{ json: { status: 'inactive' } },
|
|
{ json: { status: 'active' } },
|
|
]),
|
|
getNodeParameter: vi.fn()
|
|
};
|
|
|
|
const result = await service.executeNode('if', mockContext);
|
|
|
|
expect(result).toHaveLength(2); // true and false branches
|
|
expect(result[0]).toHaveLength(2); // items at index 0 and 2
|
|
expect(result[1]).toHaveLength(1); // item at index 1
|
|
});
|
|
|
|
it('should handle merge node combining inputs', async () => {
|
|
const mockContext = {
|
|
getInputData: vi.fn((inputIndex?: number) => {
|
|
if (inputIndex === 0) return [{ json: { source: 'input1' } }];
|
|
if (inputIndex === 1) return [{ json: { source: 'input2' } }];
|
|
return [{ json: { source: 'input1' } }];
|
|
}),
|
|
getNodeParameter: vi.fn(() => 'append')
|
|
};
|
|
|
|
const result = await service.executeNode('merge', mockContext);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result[0]).toHaveLength(1);
|
|
});
|
|
});
|
|
}); |