Skip to main content
Glama
mutation-validator.test.tsβ€’18.4 kB
/** * 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); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server