mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
Fixed type issues in mutation-tracker and mutation-validator tests: - Import and use MutationToolName enum instead of string literals - Fix ValidationResult.errors to use proper object structure - Add UpdateNodeOperation type assertion for operation with nodeName All TypeScript errors resolved, lint now passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com>
558 lines
18 KiB
TypeScript
558 lines
18 KiB
TypeScript
/**
|
|
* Unit tests for MutationValidator - Data Quality Validation
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { MutationValidator } from '../../../src/telemetry/mutation-validator';
|
|
import { WorkflowMutationData, MutationToolName } from '../../../src/telemetry/mutation-types';
|
|
import type { UpdateNodeOperation } from '../../../src/types/workflow-diff';
|
|
|
|
describe('MutationValidator', () => {
|
|
let validator: MutationValidator;
|
|
|
|
beforeEach(() => {
|
|
validator = new MutationValidator();
|
|
});
|
|
|
|
describe('Workflow Structure Validation', () => {
|
|
it('should accept valid workflow structure', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Valid mutation',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test Updated',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject workflow without nodes array', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Invalid mutation',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
connections: {}
|
|
} as any,
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Invalid workflow_before structure');
|
|
});
|
|
|
|
it('should reject workflow without connections object', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Invalid mutation',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: []
|
|
} as any,
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Invalid workflow_before structure');
|
|
});
|
|
|
|
it('should reject null workflow', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Invalid mutation',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: null as any,
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Invalid workflow_before structure');
|
|
});
|
|
});
|
|
|
|
describe('Workflow Size Validation', () => {
|
|
it('should accept workflows within size limit', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Size test',
|
|
operations: [{ type: 'addNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).not.toContain(expect.stringContaining('size'));
|
|
});
|
|
|
|
it('should reject oversized workflows', () => {
|
|
// Create a very large workflow (over 500KB default limit)
|
|
// 600KB string = 600,000 characters
|
|
const largeArray = new Array(600000).fill('x').join('');
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Oversized test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Large',
|
|
type: 'n8n-nodes-base.code',
|
|
position: [100, 100],
|
|
parameters: {
|
|
code: largeArray
|
|
}
|
|
}],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.some(err => err.includes('size') && err.includes('exceeds'))).toBe(true);
|
|
});
|
|
|
|
it('should respect custom size limit', () => {
|
|
const customValidator = new MutationValidator({ maxWorkflowSizeKb: 1 });
|
|
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Custom size test',
|
|
operations: [{ type: 'addNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [{
|
|
id: 'node1',
|
|
name: 'Medium',
|
|
type: 'n8n-nodes-base.code',
|
|
position: [100, 100],
|
|
parameters: {
|
|
code: 'x'.repeat(2000) // ~2KB
|
|
}
|
|
}],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = customValidator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.some(err => err.includes('exceeds maximum (1KB)'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Intent Validation', () => {
|
|
it('should warn about empty intent', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: '',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('User intent is empty');
|
|
});
|
|
|
|
it('should warn about very short intent', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'fix',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('User intent is too short (less than 5 characters)');
|
|
});
|
|
|
|
it('should warn about very long intent', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'x'.repeat(1001),
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('User intent is very long (over 1000 characters)');
|
|
});
|
|
|
|
it('should accept good intent length', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Add error handling to API nodes',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).not.toContain(expect.stringContaining('intent'));
|
|
});
|
|
});
|
|
|
|
describe('Operations Validation', () => {
|
|
it('should reject empty operations array', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('No operations provided');
|
|
});
|
|
|
|
it('should accept operations array with items', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [
|
|
{ type: 'addNode' },
|
|
{ type: 'addConnection' }
|
|
],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).not.toContain('No operations provided');
|
|
});
|
|
});
|
|
|
|
describe('Duration Validation', () => {
|
|
it('should reject negative duration', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: -100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Duration cannot be negative');
|
|
});
|
|
|
|
it('should warn about very long duration', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 400000 // Over 5 minutes
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('Duration is very long (over 5 minutes)');
|
|
});
|
|
|
|
it('should accept reasonable duration', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
mutationSuccess: true,
|
|
durationMs: 150
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).not.toContain(expect.stringContaining('Duration'));
|
|
});
|
|
});
|
|
|
|
describe('Meaningful Change Detection', () => {
|
|
it('should warn when workflows are identical', () => {
|
|
const workflow = {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.start',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'No actual change',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: workflow,
|
|
workflowAfter: JSON.parse(JSON.stringify(workflow)), // Deep clone
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('No meaningful change detected between before and after workflows');
|
|
});
|
|
|
|
it('should not warn when workflows are different', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Real change',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'Test',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'Test Updated',
|
|
nodes: [],
|
|
connections: {}
|
|
},
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).not.toContain(expect.stringContaining('meaningful change'));
|
|
});
|
|
});
|
|
|
|
describe('Validation Data Consistency', () => {
|
|
it('should warn about invalid validation structure', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
validationBefore: { valid: 'yes' } as any, // Invalid structure
|
|
validationAfter: { valid: true, errors: [] },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).toContain('Invalid validation_before structure');
|
|
});
|
|
|
|
it('should accept valid validation structure', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
validationBefore: { valid: false, errors: [{ type: 'test_error', message: 'Error 1' }] },
|
|
validationAfter: { valid: true, errors: [] },
|
|
mutationSuccess: true,
|
|
durationMs: 100
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.warnings).not.toContain(expect.stringContaining('validation'));
|
|
});
|
|
});
|
|
|
|
describe('Comprehensive Validation', () => {
|
|
it('should collect multiple errors and warnings', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: '', // Empty - warning
|
|
operations: [], // Empty - error
|
|
workflowBefore: null as any, // Invalid - error
|
|
workflowAfter: { nodes: [] } as any, // Missing connections - error
|
|
mutationSuccess: true,
|
|
durationMs: -50 // Negative - error
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should pass validation with all criteria met', () => {
|
|
const data: WorkflowMutationData = {
|
|
sessionId: 'test-session-123',
|
|
toolName: MutationToolName.UPDATE_PARTIAL,
|
|
userIntent: 'Add error handling to HTTP Request nodes',
|
|
operations: [
|
|
{ type: 'updateNode', nodeName: 'node1', updates: { onError: 'continueErrorOutput' } } as UpdateNodeOperation
|
|
],
|
|
workflowBefore: {
|
|
id: 'wf1',
|
|
name: 'API Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'HTTP Request',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [300, 200],
|
|
parameters: {
|
|
url: 'https://api.example.com',
|
|
method: 'GET'
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
},
|
|
workflowAfter: {
|
|
id: 'wf1',
|
|
name: 'API Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node1',
|
|
name: 'HTTP Request',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [300, 200],
|
|
parameters: {
|
|
url: 'https://api.example.com',
|
|
method: 'GET'
|
|
},
|
|
onError: 'continueErrorOutput'
|
|
}
|
|
],
|
|
connections: {}
|
|
},
|
|
validationBefore: { valid: true, errors: [] },
|
|
validationAfter: { valid: true, errors: [] },
|
|
mutationSuccess: true,
|
|
durationMs: 245
|
|
};
|
|
|
|
const result = validator.validate(data);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|