mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-19 00:43:07 +00:00
Improve workflow mutation tracking to capture comprehensive data that helps provide better responses when users update workflows. This enhancement collects workflow state, user intent, and operation details to enable more context-aware assistance.
Key improvements:
- Reduce auto-flush threshold from 5 to 2 for more reliable mutation tracking
- Add comprehensive workflow and credential sanitization to mutation tracker
- Document intent parameter in workflow update tools for better UX
- Fix mutation queue handling in telemetry manager (flush now handles 3 queues)
- Add extensive unit tests for mutation tracking and validation (35 new tests)
Technical changes:
- mutation-tracker.ts: Multi-layer sanitization (workflow, node, parameter levels)
- batch-processor.ts: Support mutation data flushing to Supabase
- telemetry-manager.ts: Auto-flush mutations at threshold 2, track mutations queue
- handlers-workflow-diff.ts: Track workflow mutations with sanitized data
- Tests: 13 tests for mutation-tracker, 22 tests for mutation-validator
The intent parameter messaging emphasizes user benefit ("helps to return better response") rather than technical implementation details.
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
557 lines
18 KiB
TypeScript
557 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 } from '../../../src/telemetry/mutation-types';
|
|
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
userIntent: 'Test',
|
|
operations: [{ type: 'updateNode' }],
|
|
workflowBefore: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
workflowAfter: { id: 'wf1', name: 'Test', nodes: [], connections: {} },
|
|
validationBefore: { valid: false, errors: ['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: 'n8n_update_partial_workflow',
|
|
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: 'n8n_update_partial_workflow',
|
|
userIntent: 'Add error handling to HTTP Request nodes',
|
|
operations: [
|
|
{ type: 'updateNode', nodeId: 'node1', updates: { onError: 'continueErrorOutput' } }
|
|
],
|
|
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);
|
|
});
|
|
});
|
|
});
|