Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/n8n-io/n8n into node-2266-…
Browse files Browse the repository at this point in the history
…community-issue-the-microsoft-onedrive-node-is-not-working
  • Loading branch information
michael-radency committed Feb 11, 2025
2 parents 32b1828 + 67b951e commit 37a9583
Show file tree
Hide file tree
Showing 53 changed files with 739 additions and 559 deletions.
20 changes: 20 additions & 0 deletions cypress/e2e/11-inline-expression-editor.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { NDV } from '../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';

Expand All @@ -24,6 +25,25 @@ describe('Inline expression editor', () => {
ndv.getters.outputPanel().click();
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
});

it('should switch between expression and fixed using keyboard', () => {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);

// Should switch to expression with =
ndv.getters.assignmentCollectionAdd('assignments').click();
ndv.actions.typeIntoParameterInput('value', '=');

// Should complete {{ --> {{ | }}
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().should('have.text', '{{ }}');

// Should switch back to fixed with backspace on empty expression
ndv.actions.typeIntoParameterInput('value', '{selectall}{backspace}');
ndv.getters.parameterInput('value').click();
ndv.actions.typeIntoParameterInput('value', '{backspace}');
ndv.getters.inlineExpressionEditorInput().should('not.exist');
});
});

describe('Static data', () => {
Expand Down
3 changes: 2 additions & 1 deletion cypress/e2e/41-editors.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ describe('Editors', () => {
ndv.getters
.sqlEditorContainer()
.find('.cm-content')
.type('SELECT * FROM {{ $json.table }}', { parseSpecialCharSequences: false });
// }} is inserted automatically by bracket matching
.type('SELECT * FROM {{ $json.table', { parseSpecialCharSequences: false });
workflowPage.getters
.inlineExpressionEditorOutput()
.should('have.text', 'SELECT * FROM test_table');
Expand Down
2 changes: 1 addition & 1 deletion cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class NDV extends BasePage {
typeIntoParameterInput: (
parameterName: string,
content: string,
opts?: { parseSpecialCharSequences: boolean },
opts?: Partial<Cypress.TypeOptions>,
) => {
this.getters.parameterInput(parameterName).type(content, opts);
},
Expand Down
2 changes: 2 additions & 0 deletions packages/@n8n/api-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type * from './user';
export type * from './api-keys';

export type { Collaborator } from './push/collaboration';
export type { HeartbeatMessage } from './push/heartbeat';
export { createHeartbeatMessage, heartbeatMessageSchema } from './push/heartbeat';
export type { SendWorkerStatusMessage } from './push/worker';

export type { BannerName } from './schemas/bannerName.schema';
Expand Down
11 changes: 11 additions & 0 deletions packages/@n8n/api-types/src/push/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod';

export const heartbeatMessageSchema = z.object({
type: z.literal('heartbeat'),
});

export type HeartbeatMessage = z.infer<typeof heartbeatMessageSchema>;

export const createHeartbeatMessage = (): HeartbeatMessage => ({
type: 'heartbeat',
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { promptTypeOptions } from '@utils/descriptions';
import { getConnectedTools } from '@utils/helpers';
import { getTracingConfig } from '@utils/tracing';

import { formatToOpenAIAssistantTool } from '../../helpers/utils';
import { formatToOpenAIAssistantTool, getChatMessages } from '../../helpers/utils';
import { assistantRLC } from '../descriptions';

const properties: INodeProperties[] = [
Expand Down Expand Up @@ -252,7 +252,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
};
let thread: OpenAIClient.Beta.Threads.Thread;
if (memory) {
const chatMessages = await memory.chatHistory.getMessages();
const chatMessages = await getChatMessages(memory);

// Construct a new thread from the chat history to map the memory
if (chatMessages.length) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { BaseMessage } from '@langchain/core/messages';
import type { StructuredTool } from '@langchain/core/tools';
import type { OpenAIClient } from '@langchain/openai';
import type { BufferWindowMemory } from 'langchain/memory';
import { zodToJsonSchema } from 'zod-to-json-schema';

// Copied from langchain(`langchain/src/tools/convert_to_openai.ts`)
Expand Down Expand Up @@ -43,3 +45,7 @@ export function formatToOpenAIAssistantTool(tool: StructuredTool): OpenAIClient.
},
};
}

export async function getChatMessages(memory: BufferWindowMemory): Promise<BaseMessage[]> {
return (await memory.loadMemoryVariables({}))[memory.memoryKey] as BaseMessage[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import { BufferWindowMemory } from 'langchain/memory';

import { getChatMessages } from '../helpers/utils';

describe('OpenAI message history', () => {
it('should only get a limited number of messages', async () => {
const memory = new BufferWindowMemory({
returnMessages: true,
k: 2,
});
expect(await getChatMessages(memory)).toEqual([]);

await memory.saveContext(
[new HumanMessage({ content: 'human 1' })],
[new AIMessage({ content: 'ai 1' })],
);
// `k` means turns, but `getChatMessages` returns messages, so a Human and an AI message.
expect((await getChatMessages(memory)).length).toEqual(2);

await memory.saveContext(
[new HumanMessage({ content: 'human 2' })],
[new AIMessage({ content: 'ai 2' })],
);
expect((await getChatMessages(memory)).length).toEqual(4);
expect((await getChatMessages(memory)).map((msg) => msg.content)).toEqual([
'human 1',
'ai 1',
'human 2',
'ai 2',
]);

// We expect this to be trimmed...
await memory.saveContext(
[new HumanMessage({ content: 'human 3' })],
[new AIMessage({ content: 'ai 3' })],
);
expect((await getChatMessages(memory)).length).toEqual(4);
expect((await getChatMessages(memory)).map((msg) => msg.content)).toEqual([
'human 2',
'ai 2',
'human 3',
'ai 3',
]);
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Container } from '@n8n/di';
import Csrf from 'csrf';
import type { Response } from 'express';
import { mock } from 'jest-mock-extended';
import { Cipher } from 'n8n-core';
import { Logger } from 'n8n-core';
import { captor, mock } from 'jest-mock-extended';
import { Cipher, type InstanceSettings, Logger } from 'n8n-core';
import type { IWorkflowExecuteAdditionalData } from 'n8n-workflow';
import nock from 'nock';

Expand Down Expand Up @@ -35,7 +34,8 @@ describe('OAuth1CredentialController', () => {
const additionalData = mock<IWorkflowExecuteAdditionalData>();
(WorkflowExecuteAdditionalData.getBase as jest.Mock).mockReturnValue(additionalData);

const cipher = mockInstance(Cipher);
const cipher = new Cipher(mock<InstanceSettings>({ encryptionKey: 'password' }));
Container.set(Cipher, cipher);
const credentialsHelper = mockInstance(CredentialsHelper);
const credentialsRepository = mockInstance(CredentialsRepository);
const sharedCredentialsRepository = mockInstance(SharedCredentialsRepository);
Expand All @@ -51,6 +51,7 @@ describe('OAuth1CredentialController', () => {
id: '1',
name: 'Test Credential',
type: 'oAuth1Api',
data: cipher.encrypt({}),
});

const controller = Container.get(OAuth1CredentialController);
Expand Down Expand Up @@ -98,21 +99,23 @@ describe('OAuth1CredentialController', () => {
})
.once()
.reply(200, { oauth_token: 'random-token' });
cipher.encrypt.mockReturnValue('encrypted');

const req = mock<OAuthRequest.OAuth1Credential.Auth>({ user, query: { id: '1' } });
const authUri = await controller.getAuthUri(req);
expect(authUri).toEqual('https://example.domain/oauth/authorize?oauth_token=random-token');
const dataCaptor = captor();
expect(credentialsRepository.update).toHaveBeenCalledWith(
'1',
expect.objectContaining({
data: 'encrypted',
data: dataCaptor,
id: '1',
name: 'Test Credential',
type: 'oAuth1Api',
}),
);
expect(cipher.encrypt).toHaveBeenCalledWith({ csrfSecret });
expect(cipher.decrypt(dataCaptor.value)).toEqual(
JSON.stringify({ csrfSecret: 'csrf-secret' }),
);
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
additionalData,
credential,
Expand Down Expand Up @@ -233,22 +236,22 @@ describe('OAuth1CredentialController', () => {
})
.once()
.reply(200, 'access_token=new_token');
cipher.encrypt.mockReturnValue('encrypted');

await controller.handleCallback(req, res);

expect(cipher.encrypt).toHaveBeenCalledWith({
oauthTokenData: { access_token: 'new_token' },
});
const dataCaptor = captor();
expect(credentialsRepository.update).toHaveBeenCalledWith(
'1',
expect.objectContaining({
data: 'encrypted',
data: dataCaptor,
id: '1',
name: 'Test Credential',
type: 'oAuth1Api',
}),
);
expect(cipher.decrypt(dataCaptor.value)).toEqual(
JSON.stringify({ oauthTokenData: { access_token: 'new_token' } }),
);
expect(res.render).toHaveBeenCalledWith('oauth-callback');
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
additionalData,
Expand Down
Loading

0 comments on commit 37a9583

Please sign in to comment.