mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(core): Replace misleading "No path back to node" error with helpful execution message (#17759)
This commit is contained in:
@@ -1080,7 +1080,13 @@ export class WorkflowDataProxy {
|
||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||
) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
// Always show helpful "Execute node for preview" message
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
messageTemplate: `Execute node "${nodeName}" for preview`,
|
||||
nodeCause: nodeName,
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1098,7 +1104,10 @@ export class WorkflowDataProxy {
|
||||
contextNode = parentMainInputNode?.name ?? contextNode;
|
||||
}
|
||||
|
||||
if (!that.workflow.hasPath(nodeName, contextNode)) {
|
||||
// For .first(), .last(), .all() methods, use unidirectional path checking
|
||||
// (forward only) to maintain traditional paired item behavior
|
||||
const hasForwardPath = that.workflow.getChildNodes(nodeName).includes(contextNode);
|
||||
if (!hasForwardPath) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -676,13 +676,24 @@ export class Workflow {
|
||||
return returnConns;
|
||||
}
|
||||
|
||||
getParentMainInputNode(node: INode | null | undefined): INode | null | undefined {
|
||||
getParentMainInputNode(
|
||||
node: INode | null | undefined,
|
||||
visitedNodes: Set<string> = new Set(),
|
||||
): INode | null | undefined {
|
||||
if (!node) return node;
|
||||
|
||||
const nodeConnections = this.connectionsBySourceNode[node.name];
|
||||
if (!nodeConnections) return node;
|
||||
// Prevent infinite recursion by tracking visited nodes
|
||||
if (visitedNodes.has(node.name)) {
|
||||
return node;
|
||||
}
|
||||
visitedNodes.add(node.name);
|
||||
|
||||
// Get non-main connection types
|
||||
const nodeConnections = this.connectionsBySourceNode[node.name];
|
||||
if (!nodeConnections) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Get non-main connection types that this node connects TO (outgoing connections)
|
||||
const nonMainConnectionTypes = Object.keys(nodeConnections).filter(
|
||||
(type) => type !== NodeConnectionTypes.Main,
|
||||
);
|
||||
@@ -696,7 +707,7 @@ export class Workflow {
|
||||
if (!returnNode) {
|
||||
throw new ApplicationError(`Node "${connection.node}" not found`);
|
||||
}
|
||||
return this.getParentMainInputNode(returnNode);
|
||||
return this.getParentMainInputNode(returnNode, visitedNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,4 +754,480 @@ describe('Paired Item Path Detection', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI/Tool Node Path Detection Fix', () => {
|
||||
test('should properly detect paths in complex Telegram workflow scenario', () => {
|
||||
// Recreate the exact workflow structure from the reported issue
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: 'cb00be8d-004b-4d3d-986e-60386516c67a',
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
parameters: {
|
||||
updates: ['message'],
|
||||
additionalFields: { download: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c5bf285a-d6a5-4767-b369-a48ef504a38e',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 2.1,
|
||||
position: [208, 0],
|
||||
parameters: {
|
||||
promptType: 'define',
|
||||
text: '={{ $json.message.text }}',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'eab6fbe5-1998-46f8-9804-8f04147f9624',
|
||||
name: 'Anthropic Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
||||
typeVersion: 1.3,
|
||||
position: [32, 368],
|
||||
parameters: {
|
||||
model: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1923ef8d-d459-4d4b-a6bb-0317ab54c2be',
|
||||
name: 'Zep',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryZep',
|
||||
typeVersion: 1.3,
|
||||
position: [192, 384],
|
||||
parameters: {
|
||||
sessionIdType: 'customKey',
|
||||
sessionKey: "={{ $('Telegram Trigger').item.json.message.chat.id }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '8015fce3-73e2-443b-8dfa-26e6effdc596',
|
||||
name: 'AI Agent Tool',
|
||||
type: '@n8n/n8n-nodes-langchain.agentTool',
|
||||
typeVersion: 2.2,
|
||||
position: [368, 208],
|
||||
parameters: {
|
||||
toolDescription:
|
||||
'AI Agent that can call get emails from Gmail and create drafts in Gmail',
|
||||
text: "={{ $fromAI('Prompt__User_Message_', ``, 'string') }}",
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '201385a6-8adb-4946-92d5-8d46267750b5',
|
||||
name: 'Send a text message',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1.2,
|
||||
position: [576, 0],
|
||||
parameters: {
|
||||
chatId: "={{ $('Telegram Trigger').item.json.message.chat.id }}",
|
||||
text: '={{ $json.output }}',
|
||||
additionalFields: { appendAttribution: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
'Telegram Trigger': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
'AI Agent': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Send a text message', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
'Anthropic Chat Model': {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[
|
||||
{ node: 'AI Agent', type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
{ node: 'AI Agent Tool', type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
Zep: {
|
||||
[NodeConnectionTypes.AiMemory]: [
|
||||
[
|
||||
{ node: 'AI Agent', type: NodeConnectionTypes.AiMemory, index: 0 },
|
||||
{ node: 'AI Agent Tool', type: NodeConnectionTypes.AiMemory, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
'AI Agent Tool': {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = createWorkflow(nodes, connections);
|
||||
const wf = new Workflow({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: workflow.active,
|
||||
nodeTypes: NodeTypes(),
|
||||
settings: workflow.settings,
|
||||
});
|
||||
|
||||
// Test the key path detections that were failing before the fix
|
||||
expect(wf.hasPath('Telegram Trigger', 'Zep')).toBe(true);
|
||||
expect(wf.hasPath('Telegram Trigger', 'AI Agent')).toBe(true);
|
||||
expect(wf.hasPath('Telegram Trigger', 'Send a text message')).toBe(true);
|
||||
expect(wf.hasPath('Telegram Trigger', 'AI Agent Tool')).toBe(true);
|
||||
expect(wf.hasPath('Telegram Trigger', 'Anthropic Chat Model')).toBe(true);
|
||||
|
||||
// Test reverse paths (bidirectional)
|
||||
expect(wf.hasPath('Zep', 'Telegram Trigger')).toBe(true);
|
||||
expect(wf.hasPath('AI Agent', 'Telegram Trigger')).toBe(true);
|
||||
expect(wf.hasPath('Send a text message', 'Telegram Trigger')).toBe(true);
|
||||
expect(wf.hasPath('AI Agent Tool', 'AI Agent')).toBe(true);
|
||||
expect(wf.hasPath('Anthropic Chat Model', 'AI Agent')).toBe(true);
|
||||
|
||||
// Test getParentMainInputNode for AI/tool nodes
|
||||
const zepNode = wf.getNode('Zep');
|
||||
const zepParent = wf.getParentMainInputNode(zepNode);
|
||||
expect(zepParent?.name).toBe('AI Agent');
|
||||
|
||||
const toolNode = wf.getNode('AI Agent Tool');
|
||||
const toolParent = wf.getParentMainInputNode(toolNode);
|
||||
expect(toolParent?.name).toBe('AI Agent');
|
||||
|
||||
const modelNode = wf.getNode('Anthropic Chat Model');
|
||||
const modelParent = wf.getParentMainInputNode(modelNode);
|
||||
expect(modelParent?.name).toBe('AI Agent');
|
||||
});
|
||||
|
||||
test('should handle getParentMainInputNode with cycle detection', () => {
|
||||
// Create a scenario where AI/tool nodes could create cycles
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Agent1',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 2.1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Agent2',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 2.1,
|
||||
position: [300, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Tool1',
|
||||
type: '@n8n/n8n-nodes-langchain.toolCalculator',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
// Create connections that could form a cycle
|
||||
const connections = {
|
||||
Agent1: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'Agent2', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
Agent2: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'Tool1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
Tool1: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'Agent1', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = createWorkflow(nodes, connections);
|
||||
const wf = new Workflow({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: workflow.active,
|
||||
nodeTypes: NodeTypes(),
|
||||
settings: workflow.settings,
|
||||
});
|
||||
|
||||
// This should not cause infinite recursion due to cycle detection
|
||||
const agent1Node = wf.getNode('Agent1');
|
||||
const agent1Parent = wf.getParentMainInputNode(agent1Node);
|
||||
expect(agent1Parent?.name).toBe('Agent1'); // Returns self due to cycle detection
|
||||
|
||||
const tool1Node = wf.getNode('Tool1');
|
||||
const tool1Parent = wf.getParentMainInputNode(tool1Node);
|
||||
expect(tool1Parent?.name).toBe('Tool1'); // Returns self due to cycle detection
|
||||
});
|
||||
|
||||
test('should correctly identify parent main input nodes in complex AI scenarios', () => {
|
||||
// Test complex scenario with multiple connection types
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'MainAgent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 2.1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryBufferMemory',
|
||||
typeVersion: 1,
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'ChatModel',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
position: [300, 200],
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
Trigger: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'MainAgent', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
Memory: {
|
||||
[NodeConnectionTypes.AiMemory]: [
|
||||
[{ node: 'MainAgent', type: NodeConnectionTypes.AiMemory, index: 0 }],
|
||||
],
|
||||
},
|
||||
ChatModel: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ node: 'MainAgent', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = createWorkflow(nodes, connections);
|
||||
const wf = new Workflow({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: workflow.active,
|
||||
nodeTypes: NodeTypes(),
|
||||
settings: workflow.settings,
|
||||
});
|
||||
|
||||
// Memory node should trace back to MainAgent as its parent main input
|
||||
const memoryNode = wf.getNode('Memory');
|
||||
const memoryParent = wf.getParentMainInputNode(memoryNode);
|
||||
expect(memoryParent?.name).toBe('MainAgent');
|
||||
|
||||
// ChatModel node should trace back to MainAgent as its parent main input
|
||||
const chatModelNode = wf.getNode('ChatModel');
|
||||
const chatModelParent = wf.getParentMainInputNode(chatModelNode);
|
||||
expect(chatModelParent?.name).toBe('MainAgent');
|
||||
|
||||
// MainAgent should trace back to itself (no further parent main input)
|
||||
const mainAgentNode = wf.getNode('MainAgent');
|
||||
const mainAgentParent = wf.getParentMainInputNode(mainAgentNode);
|
||||
expect(mainAgentParent?.name).toBe('MainAgent');
|
||||
});
|
||||
|
||||
test('should handle workflow with multiple AI connections', () => {
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: '85056f63-f461-4b64-a8ca-807b019b30da',
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [-272, 16],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '9670cb60-8926-40d0-bcba-efab28b477ee',
|
||||
name: 'AI Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 2.1,
|
||||
position: [1056, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '7fe1aa70-3418-4ad7-940d-af0b7f9b6fb2',
|
||||
name: 'Anthropic Chat Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
||||
typeVersion: 1.3,
|
||||
position: [1072, 224],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'e7820a46-6f6e-48b2-8dfe-744d3515af79',
|
||||
name: 'Zep',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryZep',
|
||||
typeVersion: 1.3,
|
||||
position: [1200, 224],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'f259429d-3a70-4746-945e-c8056160408c',
|
||||
name: 'Send a text message',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1.2,
|
||||
position: [1696, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '92756ecf-546f-454c-9423-ae273f07a2f2',
|
||||
name: 'AI Agent Tool',
|
||||
type: '@n8n/n8n-nodes-langchain.agentTool',
|
||||
typeVersion: 2.2,
|
||||
position: [1536, 272],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '193c4482-8477-4a72-9bdb-cc8dc46fe34c',
|
||||
name: 'Anthropic Chat Model1',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
||||
typeVersion: 1.3,
|
||||
position: [1424, 480],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '141264f6-dcfa-4a50-9212-a4fbc76fead6',
|
||||
name: 'Switch',
|
||||
type: 'n8n-nodes-base.switch',
|
||||
typeVersion: 3.2,
|
||||
position: [160, 16],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'b9498c03-f517-43e8-830b-7b694be1199f',
|
||||
name: 'Edit Fields',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [368, 112],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '564bfeaf-85a6-46f7-bc00-b6ed4e305c8f',
|
||||
name: 'Typing ...',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1.2,
|
||||
position: [-64, 16],
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
'Telegram Trigger': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Typing ...', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
'AI Agent': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Send a text message', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
'Anthropic Chat Model': {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
|
||||
],
|
||||
},
|
||||
Zep: {
|
||||
[NodeConnectionTypes.AiMemory]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.AiMemory, index: 0 }],
|
||||
],
|
||||
},
|
||||
'Anthropic Chat Model1': {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ node: 'AI Agent Tool', type: NodeConnectionTypes.AiLanguageModel, index: 0 }],
|
||||
],
|
||||
},
|
||||
'AI Agent Tool': {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
Switch: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Edit Fields', type: NodeConnectionTypes.Main, index: 1 }],
|
||||
],
|
||||
},
|
||||
'Edit Fields': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'AI Agent', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
'Typing ...': {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Switch', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = createWorkflow(nodes, connections);
|
||||
const wf = new Workflow({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: workflow.active,
|
||||
nodeTypes: NodeTypes(),
|
||||
settings: workflow.settings,
|
||||
});
|
||||
|
||||
// Test bidirectional path detection for the complex workflow
|
||||
// Main flow: Telegram Trigger -> Typing ... -> Switch -> Edit Fields -> AI Agent -> Send a text message
|
||||
expect(wf.hasPath('Telegram Trigger', 'Send a text message')).toBe(true);
|
||||
expect(wf.hasPath('Typing ...', 'AI Agent')).toBe(true);
|
||||
expect(wf.hasPath('Switch', 'AI Agent')).toBe(true);
|
||||
expect(wf.hasPath('Edit Fields', 'AI Agent')).toBe(true);
|
||||
|
||||
// Test AI connections that should be reachable via bidirectional path detection
|
||||
expect(wf.hasPath('Zep', 'Send a text message')).toBe(true); // ai_memory -> AI Agent -> Send a text message
|
||||
expect(wf.hasPath('Anthropic Chat Model', 'Send a text message')).toBe(true); // ai_languageModel -> AI Agent -> Send a text message
|
||||
expect(wf.hasPath('AI Agent Tool', 'Send a text message')).toBe(true); // ai_tool -> AI Agent -> Send a text message
|
||||
expect(wf.hasPath('Anthropic Chat Model1', 'Send a text message')).toBe(true); // ai_languageModel -> AI Agent Tool -> AI Agent -> Send a text message
|
||||
|
||||
// Test WorkflowDataProxy access from 'Send a text message' to all other nodes
|
||||
const proxy = createProxy(workflow, 'Send a text message');
|
||||
|
||||
// These should all work without throwing path detection errors
|
||||
expect(() => proxy.$('Telegram Trigger')).not.toThrow();
|
||||
expect(() => proxy.$('Typing ...')).not.toThrow();
|
||||
expect(() => proxy.$('Switch')).not.toThrow();
|
||||
expect(() => proxy.$('Edit Fields')).not.toThrow();
|
||||
expect(() => proxy.$('AI Agent')).not.toThrow();
|
||||
expect(() => proxy.$('Zep')).not.toThrow();
|
||||
expect(() => proxy.$('Anthropic Chat Model')).not.toThrow();
|
||||
expect(() => proxy.$('AI Agent Tool')).not.toThrow();
|
||||
expect(() => proxy.$('Anthropic Chat Model1')).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,8 +286,10 @@ describe('WorkflowDataProxy', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Error finding the referenced node');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_connection');
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.messageTemplate).toEqual(
|
||||
'Execute node "Impossible if" for preview',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -826,4 +828,207 @@ describe('WorkflowDataProxy', () => {
|
||||
expect(proxy.$('Set main variable').item.json.main_variable).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Improved error messages for missing execution data', () => {
|
||||
test('should show helpful error message when accessing node without execution data', () => {
|
||||
// Create a simple workflow with two connected nodes
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Send a text message',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1.2,
|
||||
position: [576, 0],
|
||||
parameters: {
|
||||
chatId: "={{ $('Telegram Trigger').item.json.message.chat.id }}",
|
||||
text: 'Test message',
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Telegram Trigger': {
|
||||
main: [[{ node: 'Send a text message', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Create run data without execution data for Telegram Trigger
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'Send a text message');
|
||||
|
||||
// Should throw helpful error when trying to access Telegram Trigger data
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('Telegram Trigger').item;
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
expect(error!.message).toBe('No execution data available');
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Telegram Trigger" for preview');
|
||||
expect(error!.context.nodeCause).toBe('Telegram Trigger');
|
||||
});
|
||||
|
||||
test('should show helpful error message for different node names', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Process Data',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [300, 0],
|
||||
parameters: {
|
||||
jsCode: "return $('HTTP Request').all();",
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'HTTP Request': {
|
||||
main: [[{ node: 'Process Data', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'Process Data');
|
||||
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('HTTP Request').item;
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "HTTP Request" for preview');
|
||||
expect(error!.context.nodeCause).toBe('HTTP Request');
|
||||
});
|
||||
|
||||
test('should use improved error for first(), last(), and all() methods', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Start Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'End Node',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [300, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Start Node': {
|
||||
main: [[{ node: 'End Node', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'End Node');
|
||||
|
||||
// Test first() method
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('Start Node').first();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
|
||||
// Test last() method
|
||||
try {
|
||||
proxy.$('Start Node').last();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
|
||||
// Test all() method
|
||||
try {
|
||||
proxy.$('Start Node').all();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user