diff --git a/packages/workflow/src/workflow-data-proxy.ts b/packages/workflow/src/workflow-data-proxy.ts index 013078c24c..8256a1b9cc 100644 --- a/packages/workflow/src/workflow-data-proxy.ts +++ b/packages/workflow/src/workflow-data-proxy.ts @@ -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); } }; diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index ec72681621..b08bab4342 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -676,13 +676,24 @@ export class Workflow { return returnConns; } - getParentMainInputNode(node: INode | null | undefined): INode | null | undefined { + getParentMainInputNode( + node: INode | null | undefined, + visitedNodes: Set = 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); } } } diff --git a/packages/workflow/test/paired-item-path-detection.test.ts b/packages/workflow/test/paired-item-path-detection.test.ts index 26ff54da43..a1e94fc5a4 100644 --- a/packages/workflow/test/paired-item-path-detection.test.ts +++ b/packages/workflow/test/paired-item-path-detection.test.ts @@ -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(); + }); + }); }); diff --git a/packages/workflow/test/workflow-data-proxy.test.ts b/packages/workflow/test/workflow-data-proxy.test.ts index 7cbcf3a8e0..f3a8b3bc34 100644 --- a/packages/workflow/test/workflow-data-proxy.test.ts +++ b/packages/workflow/test/workflow-data-proxy.test.ts @@ -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'); + }); + }); });