Files
n8n-mcp/tests/unit/services/workflow-auto-fixer.test.ts
czlonkowski 5ea00e12a2 fix: Mock getNodeVersions in workflow-auto-fixer tests
Add missing mock for getNodeVersions() method in WorkflowAutoFixer tests.
This fixes 6 failing tests that were encountering undefined values when
NodeVersionService attempted to query node versions.

The tests now properly mock the repository method to return an empty array,
allowing the version service to handle the "no versions available" case
gracefully.

Fixes #359 CI test failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
2025-10-24 10:47:49 +02:00

405 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { WorkflowAutoFixer, isNodeFormatIssue } from '@/services/workflow-auto-fixer';
import { NodeRepository } from '@/database/node-repository';
import type { WorkflowValidationResult } from '@/services/workflow-validator';
import type { ExpressionFormatIssue } from '@/services/expression-format-validator';
import type { Workflow, WorkflowNode } from '@/types/n8n-api';
vi.mock('@/database/node-repository');
vi.mock('@/services/node-similarity-service');
describe('WorkflowAutoFixer', () => {
let autoFixer: WorkflowAutoFixer;
let mockRepository: NodeRepository;
const createMockWorkflow = (nodes: WorkflowNode[]): Workflow => ({
id: 'test-workflow',
name: 'Test Workflow',
active: false,
nodes,
connections: {},
settings: {},
createdAt: '',
updatedAt: ''
});
const createMockNode = (id: string, type: string, parameters: any = {}): WorkflowNode => ({
id,
name: id,
type,
typeVersion: 1,
position: [0, 0],
parameters
});
beforeEach(() => {
vi.clearAllMocks();
mockRepository = new NodeRepository({} as any);
// Mock getNodeVersions to return empty array (no versions available)
vi.spyOn(mockRepository, 'getNodeVersions').mockReturnValue([]);
autoFixer = new WorkflowAutoFixer(mockRepository);
});
describe('Type Guards', () => {
it('should identify NodeFormatIssue correctly', () => {
const validIssue: ExpressionFormatIssue = {
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Missing = prefix'
} as any;
(validIssue as any).nodeName = 'httpRequest';
(validIssue as any).nodeId = 'node-1';
const invalidIssue: ExpressionFormatIssue = {
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Missing = prefix'
};
expect(isNodeFormatIssue(validIssue)).toBe(true);
expect(isNodeFormatIssue(invalidIssue)).toBe(false);
});
});
describe('Expression Format Fixes', () => {
it('should fix missing prefix in expressions', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', {
url: '{{ $json.url }}',
method: 'GET'
})
]);
const formatIssues: ExpressionFormatIssue[] = [{
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Expression must start with =',
nodeName: 'node-1',
nodeId: 'node-1'
} as any];
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, formatIssues);
expect(result.fixes).toHaveLength(1);
expect(result.fixes[0].type).toBe('expression-format');
expect(result.fixes[0].before).toBe('{{ $json.url }}');
expect(result.fixes[0].after).toBe('={{ $json.url }}');
expect(result.fixes[0].confidence).toBe('high');
expect(result.operations).toHaveLength(1);
expect(result.operations[0].type).toBe('updateNode');
});
it('should handle multiple expression fixes in same node', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', {
url: '{{ $json.url }}',
body: '{{ $json.body }}'
})
]);
const formatIssues: ExpressionFormatIssue[] = [
{
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Expression must start with =',
nodeName: 'node-1',
nodeId: 'node-1'
} as any,
{
fieldPath: 'body',
currentValue: '{{ $json.body }}',
correctedValue: '={{ $json.body }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Expression must start with =',
nodeName: 'node-1',
nodeId: 'node-1'
} as any
];
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, formatIssues);
expect(result.fixes).toHaveLength(2);
expect(result.operations).toHaveLength(1); // Single update operation for the node
});
});
describe('TypeVersion Fixes', () => {
it('should fix typeVersion exceeding maximum', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', {})
]);
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [{
type: 'error',
nodeId: 'node-1',
nodeName: 'node-1',
message: 'typeVersion 3.5 exceeds maximum supported version 2.0'
}],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, []);
expect(result.fixes).toHaveLength(1);
expect(result.fixes[0].type).toBe('typeversion-correction');
expect(result.fixes[0].before).toBe(3.5);
expect(result.fixes[0].after).toBe(2);
expect(result.fixes[0].confidence).toBe('medium');
});
});
describe('Error Output Configuration Fixes', () => {
it('should remove conflicting onError setting', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', {})
]);
workflow.nodes[0].onError = 'continueErrorOutput';
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [{
type: 'error',
nodeId: 'node-1',
nodeName: 'node-1',
message: "Node has onError: 'continueErrorOutput' but no error output connections"
}],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, []);
expect(result.fixes).toHaveLength(1);
expect(result.fixes[0].type).toBe('error-output-config');
expect(result.fixes[0].before).toBe('continueErrorOutput');
expect(result.fixes[0].after).toBeUndefined();
expect(result.fixes[0].confidence).toBe('medium');
});
});
describe('setNestedValue Validation', () => {
it('should throw error for non-object target', () => {
expect(() => {
autoFixer['setNestedValue'](null, ['field'], 'value');
}).toThrow('Cannot set value on non-object');
expect(() => {
autoFixer['setNestedValue']('string', ['field'], 'value');
}).toThrow('Cannot set value on non-object');
});
it('should throw error for empty path', () => {
expect(() => {
autoFixer['setNestedValue']({}, [], 'value');
}).toThrow('Cannot set value with empty path');
});
it('should handle nested paths correctly', () => {
const obj = { level1: { level2: { level3: 'old' } } };
autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'new');
expect(obj.level1.level2.level3).toBe('new');
});
it('should create missing nested objects', () => {
const obj = {};
autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'value');
expect(obj).toEqual({
level1: {
level2: {
level3: 'value'
}
}
});
});
it('should handle array indices in paths', () => {
const obj: any = { items: [] };
autoFixer['setNestedValue'](obj, ['items[0]', 'name'], 'test');
expect(obj.items[0].name).toBe('test');
});
it('should throw error for invalid array notation', () => {
const obj = {};
expect(() => {
autoFixer['setNestedValue'](obj, ['field[abc]'], 'value');
}).toThrow('Invalid array notation: field[abc]');
});
it('should throw when trying to traverse non-object', () => {
const obj = { field: 'string' };
expect(() => {
autoFixer['setNestedValue'](obj, ['field', 'nested'], 'value');
}).toThrow('Cannot traverse through string at field');
});
});
describe('Confidence Filtering', () => {
it('should filter fixes by confidence level', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
]);
const formatIssues: ExpressionFormatIssue[] = [{
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Expression must start with =',
nodeName: 'node-1',
nodeId: 'node-1'
} as any];
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, formatIssues, {
confidenceThreshold: 'low'
});
expect(result.fixes.length).toBeGreaterThan(0);
expect(result.fixes.every(f => ['high', 'medium', 'low'].includes(f.confidence))).toBe(true);
});
});
describe('Summary Generation', () => {
it('should generate appropriate summary for fixes', async () => {
const workflow = createMockWorkflow([
createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
]);
const formatIssues: ExpressionFormatIssue[] = [{
fieldPath: 'url',
currentValue: '{{ $json.url }}',
correctedValue: '={{ $json.url }}',
issueType: 'missing-prefix',
severity: 'error',
explanation: 'Expression must start with =',
nodeName: 'node-1',
nodeId: 'node-1'
} as any];
const validationResult: WorkflowValidationResult = {
valid: false,
errors: [],
warnings: [],
statistics: {
totalNodes: 1,
enabledNodes: 1,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, formatIssues);
expect(result.summary).toContain('expression format');
expect(result.stats.total).toBe(1);
expect(result.stats.byType['expression-format']).toBe(1);
});
it('should handle empty fixes gracefully', async () => {
const workflow = createMockWorkflow([]);
const validationResult: WorkflowValidationResult = {
valid: true,
errors: [],
warnings: [],
statistics: {
totalNodes: 0,
enabledNodes: 0,
triggerNodes: 0,
validConnections: 0,
invalidConnections: 0,
expressionsValidated: 0
},
suggestions: []
};
const result = await autoFixer.generateFixes(workflow, validationResult, []);
expect(result.summary).toBe('No fixes available');
expect(result.stats.total).toBe(0);
expect(result.operations).toEqual([]);
});
});
});