Skip to content

Commit

Permalink
Merge branch 'master' into ask-assistant
Browse files Browse the repository at this point in the history
* master:
  refactor(core): Clean up event relays (no-changelog) (#10284)
  fix(editor): Fix execution retry button (#10275)
  feat(core): Show sub-node error on the logs pane. Open logs pane on sub-node error (#10248)
  refactor(core): Move instanceRole to InstanceSettings (no-changelog) (#10242)
  feat(core): Allow filtering executions and users by project in Public API  (#10250)
  fix(core): Make execution and its data creation atomic (#10276)
  refactor(core): Mark schema env vars used by cloud hooks (no-changelog) (#10283)
  ci: Fix DB tests (no-changelog) (#10282)
  feat(core): Support create, delete, edit role for users in Public API (#10279)
  refactor(core): Decouple post workflow execute event from internal hooks (no-changelog) (#10280)
  feat(core): Allow transferring credentials in Public API (#10259)
  feat(core): Support create, read, update, delete projects in Public API (#10269)
  ci: Introduce lint rule `no-type-unsafe-event-emitter` (no-changelog) (#10254)
  fix(core): Surface enterprise trial error message (#10267)
  fix(editor): Enable moving resources only if team projects are available by the license (#10271)
  fix(core): Upgrade tournament to address some XSS vulnerabilities (#10277)

# Conflicts:
#	packages/cli/src/Server.ts
  • Loading branch information
MiloradFilipovic committed Aug 2, 2024
2 parents 21ba59d + aa0a470 commit 1bbbc81
Show file tree
Hide file tree
Showing 118 changed files with 2,734 additions and 987 deletions.
2 changes: 2 additions & 0 deletions cypress/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set';
Expand All @@ -57,6 +58,7 @@ export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook';

Expand Down
279 changes: 279 additions & 0 deletions cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import type { ExecutionError } from 'n8n-workflow/src';
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addToolNodeToParent,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import {
AGENT_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_MEMORY_POSTGRES_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_TRIGGER_NODE_NAME,
} from '../constants';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
} from '../composables/ndv';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
closeManualChatModal,
getManualChatMessages,
getManualChatModalLogs,
getManualChatModalLogsEntries,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';

const ndv = new NDV();
const WorkflowPage = new WorkflowPageClass();

function createRunDataWithError(inputMessage: string) {
return [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
jsonData: {
ai_memory: {
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
},
inputOverride: {
ai_memory: [
[
{
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
],
],
},
error: {
message: 'Internal error',
timestamp: 1722591723244,
name: 'NodeOperationError',
description: 'Internal error',
context: {},
cause: {
name: 'error',
severity: 'FATAL',
code: '3D000',
file: 'postinit.c',
line: '885',
routine: 'InitPostgres',
} as unknown as Error,
} as ExecutionError,
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error',
error: {
level: 'error',
tags: {
packageName: 'workflow',
},
context: {},
functionality: 'configuration-node',
name: 'NodeOperationError',
timestamp: 1722591723244,
node: {
parameters: {
notice: '',
sessionIdType: 'fromInput',
tableName: 'n8n_chat_histories',
},
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
name: 'Postgres Chat Memory',
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
typeVersion: 1,
position: [1140, 500],
credentials: {
postgres: {
id: 'RkyZetVpGsSfEAhQ',
name: 'Postgres account',
},
},
},
messages: ['database "chat11" does not exist'],
description: 'Internal error',
message: 'Internal error',
} as unknown as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
];
}

function setupTestWorkflow(chatTrigger: boolean = false) {
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
if (chatTrigger) {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
} else {
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
}

addNodeToCanvas(AGENT_NODE_NAME, true);

if (!chatTrigger) {
// Remove chat trigger
WorkflowPage.getters
.canvasNodeByName(MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME)
.find('[data-test-id="delete-node-button"]')
.click({ force: true });

// Set manual trigger to output standard pinned data
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
ndv.actions.editPinnedData();
ndv.actions.savePinnedData();
ndv.actions.close();
}

// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();

addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);

clickCreateNewCredential();
setCredentialValues({
password: 'testtesttest',
});

ndv.getters.parameterInput('sessionIdType').click();
getVisibleSelect().contains('Define below').click();
ndv.getters.parameterInput('sessionKey').type('asdasd');

clickGetBackToCanvas();

addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);

clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();

WorkflowPage.actions.zoomToFit();
}

function checkMessages(inputMessage: string, outputMessage: string) {
const messages = getManualChatMessages();
messages.should('have.length', 2);
messages.should('contain', inputMessage);
messages.should('contain', outputMessage);

getManualChatModalLogs().should('exist');
getManualChatModalLogsEntries()
.should('have.length', 1)
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
}

describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
beforeEach(() => {
navigateToNewWorkflowPage();
});

it('should open logs tab by default when there was an error', () => {
setupTestWorkflow(true);

openNode(AGENT_NODE_NAME);

const inputMessage = 'Test the code tool';

clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});

checkMessages(inputMessage, '[ERROR: Internal error]');
closeManualChatModal();

// Open the AI Agent node to see the logs
openNode(AGENT_NODE_NAME);

// Finally check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');

ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');

ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});

it('should switch to logs tab on error, when NDV is already opened', () => {
setupTestWorkflow(false);

openNode(AGENT_NODE_NAME);

const inputMessage = 'Test the code tool';

runMockWorkflowExecution({
trigger: () => clickExecuteNode(),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});

// Check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');

ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');

ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});
});
1 change: 1 addition & 0 deletions cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class NDV extends BasePage {
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
aiOutputModeToggle: () => cy.getByTestId('ai-output-mode-select'),
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
savePinnedDataButton: () =>
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SupplyData,
ExecutionError,
} from 'n8n-workflow';

import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type { Sandbox } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
Expand Down Expand Up @@ -208,7 +209,7 @@ export class ToolCode implements INodeType {
try {
response = await runFunction(query);
} catch (error: unknown) {
executionError = error as ExecutionError;
executionError = new NodeOperationError(this.getNode(), error as ExecutionError);
response = `There was an error: "${executionError.message}"`;
}

Expand All @@ -229,6 +230,7 @@ export class ToolCode implements INodeType {
} else {
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
}

return response;
},
}),
Expand Down
30 changes: 30 additions & 0 deletions packages/@n8n_io/eslint-config/local-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,36 @@ module.exports = {
};
},
},

'no-type-unsafe-event-emitter': {
meta: {
type: 'problem',
docs: {
description: 'Disallow extending from `EventEmitter`, which is not type-safe.',
recommended: 'error',
},
messages: {
noExtendsEventEmitter: 'Extend from the type-safe `TypedEmitter` class instead.',
},
},
create(context) {
return {
ClassDeclaration(node) {
if (
node.superClass &&
node.superClass.type === 'Identifier' &&
node.superClass.name === 'EventEmitter' &&
node.id.name !== 'TypedEmitter'
) {
context.report({
node: node.superClass,
messageId: 'noExtendsEventEmitter',
});
}
},
};
},
},
};

const isJsonParseCall = (node) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
rules: {
'n8n-local-rules/no-dynamic-import-template': 'error',
'n8n-local-rules/misplaced-n8n-typeorm-import': 'error',
'n8n-local-rules/no-type-unsafe-event-emitter': 'error',
complexity: 'error',

// TODO: Remove this
Expand All @@ -44,6 +45,12 @@ module.exports = {
'n8n-local-rules/misplaced-n8n-typeorm-import': 'off',
},
},
{
files: ['./test/**/*.ts'],
rules: {
'n8n-local-rules/no-type-unsafe-event-emitter': 'off',
},
},
{
files: ['./src/decorators/**/*.ts'],
rules: {
Expand Down
Loading

0 comments on commit 1bbbc81

Please sign in to comment.