mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 22:42:04 +00:00
Implements comprehensive execution data filtering system to enable AI agents to inspect large workflow executions without exceeding token limits. Features: - Preview mode: Shows structure, counts, and size estimates (~500 tokens) - Summary mode: Returns 2 sample items per node (~2-5K tokens) - Filtered mode: Granular control with itemsLimit and nodeNames - Full mode: Complete data retrieval (explicit opt-in) - Smart recommendations based on data size analysis - Structure-only mode (itemsLimit: 0) for schema inspection - 100% backward compatibility with legacy includeData parameter Technical improvements: - New ExecutionProcessor service with intelligent filtering logic - Type-safe implementation with Record<string, unknown> over any - Comprehensive validation and error handling - 33 unit tests with 78% coverage - Constants-based thresholds for easy tuning Bug fixes: - Fixed preview mode API data fetching to enable structure analysis - Validates and caps itemsLimit to prevent abuse Impact: - Reduces token usage by 80-95% for large datasets (50+ items) - Prevents token overflow when inspecting workflow executions - Enables recommended workflow: preview → recommendation → targeted fetch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
666 lines
18 KiB
TypeScript
666 lines
18 KiB
TypeScript
/**
|
|
* Execution Processor Service Tests
|
|
*
|
|
* Comprehensive test coverage for execution filtering and processing
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
generatePreview,
|
|
filterExecutionData,
|
|
processExecution,
|
|
} from '../../../src/services/execution-processor';
|
|
import {
|
|
Execution,
|
|
ExecutionStatus,
|
|
ExecutionFilterOptions,
|
|
} from '../../../src/types/n8n-api';
|
|
|
|
/**
|
|
* Test data factories
|
|
*/
|
|
|
|
function createMockExecution(options: {
|
|
id?: string;
|
|
status?: ExecutionStatus;
|
|
nodeData?: Record<string, any>;
|
|
hasError?: boolean;
|
|
}): Execution {
|
|
const { id = 'test-exec-1', status = ExecutionStatus.SUCCESS, nodeData = {}, hasError = false } = options;
|
|
|
|
return {
|
|
id,
|
|
workflowId: 'workflow-1',
|
|
status,
|
|
mode: 'manual',
|
|
finished: true,
|
|
startedAt: '2024-01-01T10:00:00.000Z',
|
|
stoppedAt: '2024-01-01T10:00:05.000Z',
|
|
data: {
|
|
resultData: {
|
|
runData: nodeData,
|
|
error: hasError ? { message: 'Test error' } : undefined,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function createNodeData(itemCount: number, includeError = false) {
|
|
const items = Array.from({ length: itemCount }, (_, i) => ({
|
|
json: {
|
|
id: i + 1,
|
|
name: `Item ${i + 1}`,
|
|
value: Math.random() * 100,
|
|
nested: {
|
|
field1: `value${i}`,
|
|
field2: true,
|
|
},
|
|
},
|
|
}));
|
|
|
|
return [
|
|
{
|
|
startTime: Date.now(),
|
|
executionTime: 123,
|
|
data: {
|
|
main: [items],
|
|
},
|
|
error: includeError ? { message: 'Node error' } : undefined,
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Preview Mode Tests
|
|
*/
|
|
describe('ExecutionProcessor - Preview Mode', () => {
|
|
it('should generate preview for empty execution', () => {
|
|
const execution = createMockExecution({ nodeData: {} });
|
|
const { preview, recommendation } = generatePreview(execution);
|
|
|
|
expect(preview.totalNodes).toBe(0);
|
|
expect(preview.executedNodes).toBe(0);
|
|
expect(preview.estimatedSizeKB).toBe(0);
|
|
expect(recommendation.canFetchFull).toBe(true);
|
|
expect(recommendation.suggestedMode).toBe('full'); // Empty execution is safe to fetch in full
|
|
});
|
|
|
|
it('should generate preview with accurate item counts', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
'Filter': createNodeData(12),
|
|
},
|
|
});
|
|
|
|
const { preview } = generatePreview(execution);
|
|
|
|
expect(preview.totalNodes).toBe(2);
|
|
expect(preview.executedNodes).toBe(2);
|
|
expect(preview.nodes['HTTP Request'].itemCounts.output).toBe(50);
|
|
expect(preview.nodes['Filter'].itemCounts.output).toBe(12);
|
|
});
|
|
|
|
it('should extract data structure from nodes', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
});
|
|
|
|
const { preview } = generatePreview(execution);
|
|
const structure = preview.nodes['HTTP Request'].dataStructure;
|
|
|
|
expect(structure).toHaveProperty('json');
|
|
expect(structure.json).toHaveProperty('id');
|
|
expect(structure.json).toHaveProperty('name');
|
|
expect(structure.json).toHaveProperty('nested');
|
|
expect(structure.json.id).toBe('number');
|
|
expect(structure.json.name).toBe('string');
|
|
expect(typeof structure.json.nested).toBe('object');
|
|
});
|
|
|
|
it('should estimate data size', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const { preview } = generatePreview(execution);
|
|
|
|
expect(preview.estimatedSizeKB).toBeGreaterThan(0);
|
|
expect(preview.nodes['HTTP Request'].estimatedSizeKB).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should detect error status in nodes', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5, true),
|
|
},
|
|
});
|
|
|
|
const { preview } = generatePreview(execution);
|
|
|
|
expect(preview.nodes['HTTP Request'].status).toBe('error');
|
|
expect(preview.nodes['HTTP Request'].error).toBeDefined();
|
|
});
|
|
|
|
it('should recommend full mode for small datasets', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
});
|
|
|
|
const { recommendation } = generatePreview(execution);
|
|
|
|
expect(recommendation.canFetchFull).toBe(true);
|
|
expect(recommendation.suggestedMode).toBe('full');
|
|
});
|
|
|
|
it('should recommend filtered mode for large datasets', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(100),
|
|
},
|
|
});
|
|
|
|
const { recommendation } = generatePreview(execution);
|
|
|
|
expect(recommendation.canFetchFull).toBe(false);
|
|
expect(recommendation.suggestedMode).toBe('filtered');
|
|
expect(recommendation.suggestedItemsLimit).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should recommend summary mode for moderate datasets', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(30),
|
|
},
|
|
});
|
|
|
|
const { recommendation } = generatePreview(execution);
|
|
|
|
expect(recommendation.canFetchFull).toBe(false);
|
|
expect(recommendation.suggestedMode).toBe('summary');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Filtering Mode Tests
|
|
*/
|
|
describe('ExecutionProcessor - Filtering', () => {
|
|
it('should filter by node names', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(10),
|
|
'Filter': createNodeData(5),
|
|
'Set': createNodeData(3),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
nodeNames: ['HTTP Request', 'Filter'],
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
|
|
expect(result.nodes).toHaveProperty('HTTP Request');
|
|
expect(result.nodes).toHaveProperty('Filter');
|
|
expect(result.nodes).not.toHaveProperty('Set');
|
|
expect(result.summary?.executedNodes).toBe(2);
|
|
});
|
|
|
|
it('should handle non-existent node names gracefully', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(10),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
nodeNames: ['NonExistent'],
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
|
|
expect(Object.keys(result.nodes || {})).toHaveLength(0);
|
|
expect(result.summary?.executedNodes).toBe(0);
|
|
});
|
|
|
|
it('should limit items to 0 (structure only)', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
itemsLimit: 0,
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.metadata.itemsShown).toBe(0);
|
|
expect(nodeData?.data?.metadata.truncated).toBe(true);
|
|
expect(nodeData?.data?.metadata.totalItems).toBe(50);
|
|
|
|
// Check that we have structure but no actual values
|
|
const output = nodeData?.data?.output?.[0]?.[0];
|
|
expect(output).toBeDefined();
|
|
expect(typeof output).toBe('object');
|
|
});
|
|
|
|
it('should limit items to 2 (default)', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'summary',
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.metadata.itemsShown).toBe(2);
|
|
expect(nodeData?.data?.metadata.totalItems).toBe(50);
|
|
expect(nodeData?.data?.metadata.truncated).toBe(true);
|
|
expect(nodeData?.data?.output?.[0]).toHaveLength(2);
|
|
});
|
|
|
|
it('should limit items to custom value', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
itemsLimit: 5,
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.metadata.itemsShown).toBe(5);
|
|
expect(nodeData?.data?.metadata.truncated).toBe(true);
|
|
expect(nodeData?.data?.output?.[0]).toHaveLength(5);
|
|
});
|
|
|
|
it('should not truncate when itemsLimit is -1 (unlimited)', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
itemsLimit: -1,
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.metadata.itemsShown).toBe(50);
|
|
expect(nodeData?.data?.metadata.totalItems).toBe(50);
|
|
expect(nodeData?.data?.metadata.truncated).toBe(false);
|
|
});
|
|
|
|
it('should not truncate when items are less than limit', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(3),
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
itemsLimit: 5,
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.metadata.itemsShown).toBe(3);
|
|
expect(nodeData?.data?.metadata.truncated).toBe(false);
|
|
});
|
|
|
|
it('should include input data when requested', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': [
|
|
{
|
|
startTime: Date.now(),
|
|
executionTime: 100,
|
|
inputData: [[{ json: { input: 'test' } }]],
|
|
data: {
|
|
main: [[{ json: { output: 'result' } }]],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
includeInputData: true,
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.input).toBeDefined();
|
|
expect(nodeData?.data?.input?.[0]?.[0]?.json?.input).toBe('test');
|
|
});
|
|
|
|
it('should not include input data by default', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': [
|
|
{
|
|
startTime: Date.now(),
|
|
executionTime: 100,
|
|
inputData: [[{ json: { input: 'test' } }]],
|
|
data: {
|
|
main: [[{ json: { output: 'result' } }]],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const options: ExecutionFilterOptions = {
|
|
mode: 'filtered',
|
|
};
|
|
|
|
const result = filterExecutionData(execution, options);
|
|
const nodeData = result.nodes?.['HTTP Request'];
|
|
|
|
expect(nodeData?.data?.input).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Mode Tests
|
|
*/
|
|
describe('ExecutionProcessor - Modes', () => {
|
|
it('should handle preview mode', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'preview' });
|
|
|
|
expect(result.mode).toBe('preview');
|
|
expect(result.preview).toBeDefined();
|
|
expect(result.recommendation).toBeDefined();
|
|
expect(result.nodes).toBeUndefined();
|
|
});
|
|
|
|
it('should handle summary mode', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.mode).toBe('summary');
|
|
expect(result.summary).toBeDefined();
|
|
expect(result.nodes).toBeDefined();
|
|
expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(2);
|
|
});
|
|
|
|
it('should handle filtered mode', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, {
|
|
mode: 'filtered',
|
|
itemsLimit: 5,
|
|
});
|
|
|
|
expect(result.mode).toBe('filtered');
|
|
expect(result.summary).toBeDefined();
|
|
expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(5);
|
|
});
|
|
|
|
it('should handle full mode', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'full' });
|
|
|
|
expect(result.mode).toBe('full');
|
|
expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(50);
|
|
expect(result.nodes?.['HTTP Request']?.data?.metadata.truncated).toBe(false);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Edge Cases
|
|
*/
|
|
describe('ExecutionProcessor - Edge Cases', () => {
|
|
it('should handle execution with no data', () => {
|
|
const execution: Execution = {
|
|
id: 'test-1',
|
|
workflowId: 'workflow-1',
|
|
status: ExecutionStatus.SUCCESS,
|
|
mode: 'manual',
|
|
finished: true,
|
|
startedAt: '2024-01-01T10:00:00.000Z',
|
|
stoppedAt: '2024-01-01T10:00:05.000Z',
|
|
};
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.summary?.totalNodes).toBe(0);
|
|
expect(result.summary?.executedNodes).toBe(0);
|
|
});
|
|
|
|
it('should handle execution with error', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
hasError: true,
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('should handle empty node data arrays', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': [],
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.nodes?.['HTTP Request']).toBeDefined();
|
|
expect(result.nodes?.['HTTP Request'].itemsOutput).toBe(0);
|
|
});
|
|
|
|
it('should handle nested data structures', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': [
|
|
{
|
|
startTime: Date.now(),
|
|
executionTime: 100,
|
|
data: {
|
|
main: [[{
|
|
json: {
|
|
deeply: {
|
|
nested: {
|
|
structure: {
|
|
value: 'test',
|
|
array: [1, 2, 3],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}]],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const { preview } = generatePreview(execution);
|
|
const structure = preview.nodes['HTTP Request'].dataStructure;
|
|
|
|
expect(structure.json.deeply).toBeDefined();
|
|
expect(typeof structure.json.deeply).toBe('object');
|
|
});
|
|
|
|
it('should calculate duration correctly', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.duration).toBe(5000); // 5 seconds
|
|
});
|
|
|
|
it('should handle execution without stop time', () => {
|
|
const execution: Execution = {
|
|
id: 'test-1',
|
|
workflowId: 'workflow-1',
|
|
status: ExecutionStatus.WAITING,
|
|
mode: 'manual',
|
|
finished: false,
|
|
startedAt: '2024-01-01T10:00:00.000Z',
|
|
data: {
|
|
resultData: {
|
|
runData: {},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.duration).toBeUndefined();
|
|
expect(result.finished).toBe(false);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* processExecution Tests
|
|
*/
|
|
describe('ExecutionProcessor - processExecution', () => {
|
|
it('should return original execution when no options provided', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
});
|
|
|
|
const result = processExecution(execution, {});
|
|
|
|
expect(result).toBe(execution);
|
|
});
|
|
|
|
it('should process when mode is specified', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
},
|
|
});
|
|
|
|
const result = processExecution(execution, { mode: 'preview' });
|
|
|
|
expect(result).not.toBe(execution);
|
|
expect((result as any).mode).toBe('preview');
|
|
});
|
|
|
|
it('should process when filtering options are provided', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(5),
|
|
'Filter': createNodeData(3),
|
|
},
|
|
});
|
|
|
|
const result = processExecution(execution, { nodeNames: ['HTTP Request'] });
|
|
|
|
expect(result).not.toBe(execution);
|
|
expect((result as any).nodes).toHaveProperty('HTTP Request');
|
|
expect((result as any).nodes).not.toHaveProperty('Filter');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Summary Statistics Tests
|
|
*/
|
|
describe('ExecutionProcessor - Summary Statistics', () => {
|
|
it('should calculate hasMoreData correctly', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(50),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, {
|
|
mode: 'summary',
|
|
itemsLimit: 2,
|
|
});
|
|
|
|
expect(result.summary?.hasMoreData).toBe(true);
|
|
});
|
|
|
|
it('should set hasMoreData to false when all data is included', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(2),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, {
|
|
mode: 'summary',
|
|
itemsLimit: 5,
|
|
});
|
|
|
|
expect(result.summary?.hasMoreData).toBe(false);
|
|
});
|
|
|
|
it('should count total items correctly across multiple nodes', () => {
|
|
const execution = createMockExecution({
|
|
nodeData: {
|
|
'HTTP Request': createNodeData(10),
|
|
'Filter': createNodeData(5),
|
|
'Set': createNodeData(3),
|
|
},
|
|
});
|
|
|
|
const result = filterExecutionData(execution, { mode: 'summary' });
|
|
|
|
expect(result.summary?.totalItems).toBe(18);
|
|
});
|
|
});
|