fix: AI connection type propagation and get_node improvements (v2.28.1) (#461)

* fix: AI connection type propagation and get_node improvements (v2.28.1)

Bug fixes:
- Issue #458: addConnection now preserves AI connection types (ai_tool, ai_memory, ai_languageModel) instead of defaulting to 'main'
- Fixed false positive "AI Agent has no tools connected" validation warning

Enhancements:
- Added expectedFormat field to resourceLocator properties in get_node output
- Added versionNotice field to make typeVersion more prominent

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>

* test: add missing test coverage for PR #461 improvements

- Added test for AI Agent validation positive case (tools properly connected)
- Added 3 tests for expectedFormat on resourceLocator properties

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>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2025-12-01 18:54:15 +01:00
committed by GitHub
parent 33690c5650
commit 3188d209b7
9 changed files with 408 additions and 9 deletions

View File

@@ -406,5 +406,74 @@ describe('PropertyFilter', () => {
const complex = result.common.find(p => p.name === 'complex');
expect(complex?.default).toBeUndefined();
});
it('should add expectedFormat for resourceLocator type properties', () => {
const properties = [
{
name: 'channel',
type: 'resourceLocator',
displayName: 'Channel',
description: 'The channel to send message to',
modes: [
{ name: 'list', displayName: 'From List' },
{ name: 'id', displayName: 'By ID' },
{ name: 'url', displayName: 'By URL' }
],
default: { mode: 'list', value: '' }
}
];
const result = PropertyFilter.getEssentials(properties, 'nodes-base.slack');
const channelProp = result.common.find(p => p.name === 'channel');
expect(channelProp).toBeDefined();
expect(channelProp?.expectedFormat).toBeDefined();
expect(channelProp?.expectedFormat?.structure).toEqual({
mode: 'string',
value: 'string'
});
expect(channelProp?.expectedFormat?.modes).toEqual(['list', 'id', 'url']);
expect(channelProp?.expectedFormat?.example).toBeDefined();
expect(channelProp?.expectedFormat?.example.mode).toBe('id');
expect(channelProp?.expectedFormat?.example.value).toBeDefined();
});
it('should handle resourceLocator without modes array', () => {
const properties = [
{
name: 'resource',
type: 'resourceLocator',
displayName: 'Resource',
default: { mode: 'id', value: 'test-123' }
}
];
const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');
const resourceProp = result.common.find(p => p.name === 'resource');
expect(resourceProp?.expectedFormat).toBeDefined();
// Should default to common modes
expect(resourceProp?.expectedFormat?.modes).toEqual(['list', 'id']);
expect(resourceProp?.expectedFormat?.example.value).toBe('test-123');
});
it('should handle resourceLocator with no default value', () => {
const properties = [
{
name: 'item',
type: 'resourceLocator',
displayName: 'Item',
modes: [{ name: 'search' }, { name: 'id' }]
}
];
const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');
const itemProp = result.common.find(p => p.name === 'item');
expect(itemProp?.expectedFormat).toBeDefined();
expect(itemProp?.expectedFormat?.modes).toEqual(['search', 'id']);
// Should use fallback value
expect(itemProp?.expectedFormat?.example.value).toBe('your-resource-id');
});
});
});

View File

@@ -4665,4 +4665,223 @@ describe('WorkflowDiffEngine', () => {
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
});
});
// Issue #458: AI connection type propagation
describe('AI Connection Type Propagation (Issue #458)', () => {
it('should propagate ai_tool connection type when targetInput is not specified', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'tool1',
name: 'Calculator',
type: '@n8n/n8n-nodes-langchain.toolCalculator',
typeVersion: 1,
position: [300, 400] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Calculator',
target: 'AI Agent',
sourceOutput: 'ai_tool'
// targetInput not specified - should default to sourceOutput ('ai_tool')
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Calculator']).toBeDefined();
expect(result.workflow.connections['Calculator']['ai_tool']).toBeDefined();
// The inner type should be 'ai_tool', NOT 'main'
expect(result.workflow.connections['Calculator']['ai_tool'][0][0].type).toBe('ai_tool');
expect(result.workflow.connections['Calculator']['ai_tool'][0][0].node).toBe('AI Agent');
});
it('should propagate ai_languageModel connection type', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'llm1',
name: 'OpenAI Chat Model',
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [300, 200] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'OpenAI Chat Model',
target: 'AI Agent',
sourceOutput: 'ai_languageModel'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['OpenAI Chat Model']['ai_languageModel'][0][0].type).toBe('ai_languageModel');
});
it('should propagate ai_memory connection type', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'memory1',
name: 'Window Buffer Memory',
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
typeVersion: 1.3,
position: [300, 500] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Window Buffer Memory',
target: 'AI Agent',
sourceOutput: 'ai_memory'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Window Buffer Memory']['ai_memory'][0][0].type).toBe('ai_memory');
});
it('should allow explicit targetInput override for mixed connection types', async () => {
const workflowWithNodes = {
...baseWorkflow,
nodes: [
{
id: 'node1',
name: 'Source Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [300, 300] as [number, number],
parameters: {}
},
{
id: 'node2',
name: 'Target Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [500, 300] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Source Node',
target: 'Target Node',
sourceOutput: 'main',
targetInput: 'main' // Explicit override
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithNodes as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Source Node']['main'][0][0].type).toBe('main');
});
it('should default to main for regular connections when sourceOutput is not specified', async () => {
const workflowWithNodes = {
...baseWorkflow,
nodes: [
{
id: 'node1',
name: 'Source Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [300, 300] as [number, number],
parameters: {}
},
{
id: 'node2',
name: 'Target Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [500, 300] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Source Node',
target: 'Target Node'
// Neither sourceOutput nor targetInput specified - should default to 'main'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithNodes as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Source Node']['main'][0][0].type).toBe('main');
});
});
});

View File

@@ -1329,6 +1329,37 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(true);
});
it('should NOT warn about AI agents WITH tools properly connected', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Calculator Tool',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Agent',
type: '@n8n/n8n-nodes-langchain.agent',
position: [300, 100],
parameters: {}
}
],
connections: {
'Calculator Tool': {
ai_tool: [[{ node: 'Agent', type: 'ai_tool', index: 0 }]]
}
}
} as any;
const result = await validator.validateWorkflow(workflow as any);
// Should NOT have warning about missing tools
expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(false);
});
it('should suggest community package setting for AI tools', async () => {
const workflow = {
nodes: [