mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-20 17:33:08 +00:00
feat(auto-fixer): add 5 connection structure fix types
Add automatic repair for malformed workflow connections commonly generated by AI models: - connection-numeric-keys: "0","1" keys → main[0], main[1] - connection-invalid-type: type:"0" → type:"main" (or parent key) - connection-id-to-name: node ID refs → node name refs - connection-duplicate-removal: dedup identical connection entries - connection-input-index: out-of-bounds input index → clamped Includes collision-safe ID-to-name renames, medium confidence on merge conflicts and index clamping, shared CONNECTION_FIX_TYPES constant, and 24 unit tests. Concieved by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
566
tests/unit/services/workflow-auto-fixer-connections.test.ts
Normal file
566
tests/unit/services/workflow-auto-fixer-connections.test.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WorkflowAutoFixer } from '@/services/workflow-auto-fixer';
|
||||
import { NodeRepository } from '@/database/node-repository';
|
||||
import type { WorkflowValidationResult } from '@/services/workflow-validator';
|
||||
import type { Workflow, WorkflowNode } from '@/types/n8n-api';
|
||||
|
||||
vi.mock('@/database/node-repository');
|
||||
vi.mock('@/services/node-similarity-service');
|
||||
|
||||
describe('WorkflowAutoFixer - Connection Fixes', () => {
|
||||
let autoFixer: WorkflowAutoFixer;
|
||||
let mockRepository: NodeRepository;
|
||||
|
||||
const createMockWorkflow = (
|
||||
nodes: WorkflowNode[],
|
||||
connections: any = {}
|
||||
): Workflow => ({
|
||||
id: 'test-workflow',
|
||||
name: 'Test Workflow',
|
||||
active: false,
|
||||
nodes,
|
||||
connections,
|
||||
settings: {},
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
});
|
||||
|
||||
const createMockNode = (id: string, name: string, type: string = 'n8n-nodes-base.noOp'): WorkflowNode => ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {}
|
||||
});
|
||||
|
||||
const emptyValidation: WorkflowValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
statistics: {
|
||||
totalNodes: 0,
|
||||
enabledNodes: 0,
|
||||
triggerNodes: 0,
|
||||
validConnections: 0,
|
||||
invalidConnections: 0,
|
||||
expressionsValidated: 0
|
||||
},
|
||||
suggestions: []
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRepository = new NodeRepository({} as any);
|
||||
vi.spyOn(mockRepository, 'getNodeVersions').mockReturnValue([]);
|
||||
autoFixer = new WorkflowAutoFixer(mockRepository);
|
||||
});
|
||||
|
||||
describe('Numeric Keys', () => {
|
||||
it('should convert single numeric key to main[index]', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('0');
|
||||
expect(connFixes[0].after).toBe('main[0]');
|
||||
|
||||
// Verify replaceConnections operation
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections');
|
||||
expect(replaceOp).toBeDefined();
|
||||
const connOp = replaceOp as any;
|
||||
expect(connOp.connections.Node1['main']).toBeDefined();
|
||||
expect(connOp.connections.Node1['0']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert multiple numeric keys', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'1': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(connFixes).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should merge with existing main entries', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'1': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1['main']).toHaveLength(2);
|
||||
expect(replaceOp.connections.Node1['main'][0]).toEqual([{ node: 'Node2', type: 'main', index: 0 }]);
|
||||
expect(replaceOp.connections.Node1['main'][1]).toEqual([{ node: 'Node3', type: 'main', index: 0 }]);
|
||||
});
|
||||
|
||||
it('should handle sparse numeric keys with gap filling', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'3': [[{ node: 'Node3', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1['main']).toHaveLength(4);
|
||||
expect(replaceOp.connections.Node1['main'][1]).toEqual([]);
|
||||
expect(replaceOp.connections.Node1['main'][2]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid Type', () => {
|
||||
it('should fix numeric type to "main"', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('0');
|
||||
expect(connFixes[0].after).toBe('main');
|
||||
});
|
||||
|
||||
it('should use parent output key for AI connection types', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
ai_tool: [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].after).toBe('ai_tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ID-to-Name', () => {
|
||||
it('should replace source key when it matches a node ID', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'Node1'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
'abc-123': {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('abc-123');
|
||||
expect(connFixes[0].after).toBe('Node1');
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections['Node1']).toBeDefined();
|
||||
expect(replaceOp.connections['abc-123']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should replace target node value when it matches a node ID', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'Node1'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'def-456', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe('def-456');
|
||||
expect(connFixes[0].after).toBe('Node2');
|
||||
});
|
||||
|
||||
it('should NOT fix when key matches both an ID and a name', async () => {
|
||||
// Node with name that looks like an ID of another node
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('abc-123', 'abc-123'), createMockNode('def-456', 'Node2')],
|
||||
{
|
||||
'abc-123': {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dedup', () => {
|
||||
it('should remove exact duplicate connections', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-duplicate-removal');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections.Node1.main[0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should keep near-duplicates with different index', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[
|
||||
{ node: 'Node2', type: 'main', index: 0 },
|
||||
{ node: 'Node2', type: 'main', index: 1 },
|
||||
]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-duplicate-removal');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Index', () => {
|
||||
it('should reset to 0 for single-input nodes', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Input index 3 on node "Node2" exceeds its input count (1). Connection from "Node1" targets input 3, but this node has 1 main input(s) (indices 0-0).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2', 'n8n-nodes-base.httpRequest')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 3 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe(3);
|
||||
expect(connFixes[0].after).toBe(0);
|
||||
expect(connFixes[0].confidence).toBe('medium');
|
||||
});
|
||||
|
||||
it('should clamp for Merge nodes', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'MergeNode',
|
||||
message: 'Input index 5 on node "MergeNode" exceeds its input count (2). Connection from "Node1" targets input 5, but this node has 2 main input(s) (indices 0-1).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'MergeNode', 'n8n-nodes-base.merge')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'MergeNode', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(1);
|
||||
expect(connFixes[0].before).toBe(5);
|
||||
expect(connFixes[0].after).toBe(1); // clamped to max valid index
|
||||
});
|
||||
|
||||
it('should not fix valid indices', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Combined', () => {
|
||||
it('should fix multiple issues in one workflow', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[
|
||||
createMockNode('id1', 'Node1'),
|
||||
createMockNode('id2', 'Node2'),
|
||||
createMockNode('id3', 'Node3')
|
||||
],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[
|
||||
{ node: 'Node2', type: '0', index: 0 },
|
||||
{ node: 'Node2', type: '0', index: 0 }, // duplicate
|
||||
]]
|
||||
},
|
||||
'id3': { // ID instead of name
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.fixes.length).toBeGreaterThan(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeDefined();
|
||||
|
||||
// Should have numeric key, invalid type, dedup, and id-to-name fixes
|
||||
const types = new Set(result.fixes.map(f => f.type));
|
||||
expect(types.has('connection-numeric-keys')).toBe(true);
|
||||
expect(types.has('connection-id-to-name')).toBe(true);
|
||||
});
|
||||
|
||||
it('should be idempotent (no fixes on valid connections)', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connectionFixTypes = [
|
||||
'connection-numeric-keys',
|
||||
'connection-invalid-type',
|
||||
'connection-id-to-name',
|
||||
'connection-duplicate-removal',
|
||||
'connection-input-index'
|
||||
];
|
||||
const connFixes = result.fixes.filter(f => connectionFixTypes.includes(f.type));
|
||||
expect(connFixes).toHaveLength(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty connections', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1')],
|
||||
{}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should respect fixTypes filtering', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: '0', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Only allow numeric key fixes, not invalid type fixes
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, [], {
|
||||
fixTypes: ['connection-numeric-keys']
|
||||
});
|
||||
|
||||
const numericFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
const typeFixes = result.fixes.filter(f => f.type === 'connection-invalid-type');
|
||||
expect(numericFixes.length).toBeGreaterThan(0);
|
||||
expect(typeFixes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should filter replaceConnections from operations when confidence threshold filters all connection fixes', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Input index 5 on node "Node2" exceeds its input count (1). Connection from "Node1" targets input 5, but this node has 1 main input(s) (indices 0-0).',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
// Input index fixes are medium confidence. Filter to high only.
|
||||
const result = await autoFixer.generateFixes(workflow, validation, [], {
|
||||
confidenceThreshold: 'high'
|
||||
});
|
||||
|
||||
// Medium confidence fixes should be filtered out
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
expect(result.operations.find(op => op.type === 'replaceConnections')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include connection issues in summary', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'Node2', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.summary).toContain('connection');
|
||||
});
|
||||
|
||||
it('should handle non-existent target nodes gracefully', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1')],
|
||||
{
|
||||
Node1: {
|
||||
'0': [[{ node: 'NonExistent', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
expect(result.fixes.some(f => f.type === 'connection-numeric-keys')).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip unparseable INPUT_INDEX_OUT_OF_BOUNDS errors gracefully', async () => {
|
||||
const validation: WorkflowValidationResult = {
|
||||
...emptyValidation,
|
||||
errors: [{
|
||||
type: 'error',
|
||||
nodeName: 'Node2',
|
||||
message: 'Something unexpected about input indices',
|
||||
code: 'INPUT_INDEX_OUT_OF_BOUNDS'
|
||||
}]
|
||||
};
|
||||
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 5 }]]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, validation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-input-index');
|
||||
expect(connFixes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fix both source keys and target .node values as IDs in the same workflow', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[
|
||||
createMockNode('abc-123', 'Node1'),
|
||||
createMockNode('def-456', 'Node2'),
|
||||
createMockNode('ghi-789', 'Node3')
|
||||
],
|
||||
{
|
||||
'abc-123': { // source key is ID
|
||||
main: [[{ node: 'def-456', type: 'main', index: 0 }]] // target .node is also ID
|
||||
},
|
||||
Node2: {
|
||||
main: [[{ node: 'ghi-789', type: 'main', index: 0 }]] // another target ID
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const connFixes = result.fixes.filter(f => f.type === 'connection-id-to-name');
|
||||
|
||||
// Should fix: source key abc-123 → Node1, target def-456 → Node2, target ghi-789 → Node3
|
||||
expect(connFixes).toHaveLength(3);
|
||||
|
||||
const replaceOp = result.operations.find(op => op.type === 'replaceConnections') as any;
|
||||
expect(replaceOp.connections['Node1']).toBeDefined();
|
||||
expect(replaceOp.connections['abc-123']).toBeUndefined();
|
||||
|
||||
// Verify target .node values were also replaced
|
||||
const node1Conns = replaceOp.connections['Node1'].main[0];
|
||||
expect(node1Conns[0].node).toBe('Node2');
|
||||
|
||||
const node2Conns = replaceOp.connections['Node2'].main[0];
|
||||
expect(node2Conns[0].node).toBe('Node3');
|
||||
});
|
||||
|
||||
it('should lower confidence to medium when merging numeric key into non-empty main slot', async () => {
|
||||
const workflow = createMockWorkflow(
|
||||
[createMockNode('id1', 'Node1'), createMockNode('id2', 'Node2'), createMockNode('id3', 'Node3')],
|
||||
{
|
||||
Node1: {
|
||||
main: [[{ node: 'Node2', type: 'main', index: 0 }]],
|
||||
'0': [[{ node: 'Node3', type: 'main', index: 0 }]] // conflicts with existing main[0]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const result = await autoFixer.generateFixes(workflow, emptyValidation, []);
|
||||
const numericFixes = result.fixes.filter(f => f.type === 'connection-numeric-keys');
|
||||
expect(numericFixes).toHaveLength(1);
|
||||
expect(numericFixes[0].confidence).toBe('medium');
|
||||
expect(numericFixes[0].description).toContain('Merged');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user