fix(core): AI agent node data accessibility (#18757)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Csaba Tuncsik
2025-09-01 17:37:16 +02:00
committed by GitHub
parent 897c55aefe
commit f0e9221cb3
6 changed files with 724 additions and 68 deletions

View File

@@ -160,6 +160,10 @@ describe('WorkflowDataProxy', () => {
expect(proxy.$('Rename').params).toEqual({ value1: 'data', value2: 'initialName' });
});
test('$("NodeName").context', () => {
expect(proxy.$('Rename').context).toBeDefined();
});
test('$("NodeName") not in workflow should throw', () => {
expect(() => proxy.$('doNotExist')).toThrowError(ExpressionError);
});
@@ -188,6 +192,12 @@ describe('WorkflowDataProxy', () => {
test('$input.item', () => {
expect(proxy.$input.item?.json?.data).toEqual(105);
});
test('$input.context', () => {
expect(proxy.$input.context).toBeDefined();
});
test('$input.params', () => {
expect(proxy.$input.params).toBeDefined();
});
test('$thisItem', () => {
expect(proxy.$thisItem.json.data).toEqual(105);
});
@@ -262,8 +272,8 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
expect(exprError.message).toEqual("Node 'Impossible' hasn't been executed");
expect(exprError.context.type).toEqual('no_execution_data');
}
});
@@ -274,8 +284,8 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('No execution data available');
expect(exprError.context.type).toEqual('no_input_connection');
expect(exprError.message).toEqual("Node 'NoInputConnection' hasn't been executed");
expect(exprError.context.type).toEqual('no_execution_data');
}
});
@@ -286,8 +296,8 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
expect(exprError.message).toEqual("Node 'Impossible if' hasn't been executed");
expect(exprError.context.type).toEqual('no_execution_data');
}
});
@@ -298,7 +308,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('No execution data available');
expect(exprError.message).toEqual("Node 'Impossible' hasn't been executed");
expect(exprError.context.type).toEqual('no_execution_data');
}
});
@@ -826,4 +836,306 @@ 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("Node 'Telegram Trigger' hasn't been executed");
expect(error!.context.type).toBe('no_execution_data');
expect(error!.context.messageTemplate).toBe(
'An expression references this node, but the node is unexecuted. Consider re-wiring your nodes or checking for execution first, i.e. {{ $if( $("{{nodeName}}").isExecuted, <action_if_executed>, "") }}',
);
});
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!.message).toBe("Node 'HTTP Request' hasn't been executed");
expect(error!.context.type).toBe('no_execution_data');
expect(error!.context.messageTemplate).toBe(
'An expression references this node, but the node is unexecuted. Consider re-wiring your nodes or checking for execution first, i.e. {{ $if( $("{{nodeName}}").isExecuted, <action_if_executed>, "") }}',
);
});
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!.message).toBe("Node 'Start Node' hasn't been executed");
expect(error!.context.messageTemplate).toBe(
'An expression references this node, but the node is unexecuted. Consider re-wiring your nodes or checking for execution first, i.e. {{ $if( $("{{nodeName}}").isExecuted, <action_if_executed>, "") }}',
);
// Test last() method
error = undefined;
try {
proxy.$('Start Node').last();
} catch (e) {
error = e as ExpressionError;
}
expect(error).toBeDefined();
expect(error!.message).toBe("Node 'Start Node' hasn't been executed");
expect(error!.context.messageTemplate).toBe(
'An expression references this node, but the node is unexecuted. Consider re-wiring your nodes or checking for execution first, i.e. {{ $if( $("{{nodeName}}").isExecuted, <action_if_executed>, "") }}',
);
// Test all() method
error = undefined;
try {
proxy.$('Start Node').all();
} catch (e) {
error = e as ExpressionError;
}
expect(error).toBeDefined();
expect(error!.message).toBe("Node 'Start Node' hasn't been executed");
expect(error!.context.messageTemplate).toBe(
'An expression references this node, but the node is unexecuted. Consider re-wiring your nodes or checking for execution first, i.e. {{ $if( $("{{nodeName}}").isExecuted, <action_if_executed>, "") }}',
);
});
test('should show helpful error message when accessing non-existent node', () => {
const workflow: IWorkflowBase = {
id: '1',
name: 'test-workflow',
nodes: [
{
id: '1',
name: 'Real Node',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
],
connections: {},
active: false,
isArchived: false,
createdAt: new Date(),
updatedAt: new Date(),
};
const run = {
data: {
resultData: {
runData: {
'Real Node': [
{
data: {
main: [[{ json: { test: 'data' } }]],
},
source: [null],
startTime: 123,
executionTime: 456,
executionIndex: 0,
},
],
},
},
},
mode: 'manual' as const,
startedAt: new Date(),
status: 'success' as const,
};
const proxy = getProxyFromFixture(workflow, run, 'Real Node');
// Should throw helpful error when trying to access a non-existent node
let error: ExpressionError | undefined;
try {
proxy.$('NonExistentNode').item;
} catch (e) {
error = e as ExpressionError;
}
expect(error).toBeDefined();
expect(error).toBeInstanceOf(ExpressionError);
expect(error!.message).toBe("Referenced node doesn't exist");
expect(error!.context.descriptionKey).toBe('nodeNotFound');
expect(error!.context.nodeCause).toBe('NonExistentNode');
});
test('should show error when accessing item with invalid index via direct proxy access', () => {
// Use existing fixture data to test the item index validation path
const fixture = loadFixture('base');
// Create a proxy with itemIndex that exceeds available items for a node
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Set Node', 'manual', {
throwOnMissingExecutionData: true,
runIndex: 10, // itemIndex way too high
});
let error: ExpressionError | undefined;
try {
// This should trigger the error path for invalid item index
proxy.$('Set Node').item;
} catch (e) {
error = e as ExpressionError;
}
expect(error).toBeDefined();
expect(error).toBeInstanceOf(ExpressionError);
});
});
});