diff --git a/.github/workflows/units-tests-reusable.yml b/.github/workflows/units-tests-reusable.yml index 60bf593e82..6475dcccc5 100644 --- a/.github/workflows/units-tests-reusable.yml +++ b/.github/workflows/units-tests-reusable.yml @@ -49,7 +49,6 @@ jobs: run: pnpm install --frozen-lockfile - name: Setup build cache - if: inputs.collectCoverage != true uses: rharkor/caching-for-turbo@v1.5 - name: Build diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index e19959453f..46448b4966 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -41,7 +41,9 @@ describe('Data mapping', () => { ndv.actions.mapDataFromHeader(1, 'value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}'); ndv.getters.inlineExpressionEditorInput().type('{esc}'); - ndv.getters.parameterExpressionPreview('value').should('include.text', '2024'); + ndv.getters + .parameterExpressionPreview('value') + .should('include.text', new Date().getFullYear()); ndv.actions.mapDataFromHeader(2, 'value'); ndv.getters diff --git a/packages/cli/src/errors/feature-not-licensed.error.ts b/packages/cli/src/errors/feature-not-licensed.error.ts index a61015f2e4..aa53655154 100644 --- a/packages/cli/src/errors/feature-not-licensed.error.ts +++ b/packages/cli/src/errors/feature-not-licensed.error.ts @@ -6,6 +6,7 @@ export class FeatureNotLicensedError extends ApplicationError { constructor(feature: (typeof LICENSE_FEATURES)[keyof typeof LICENSE_FEATURES]) { super( `Your license does not allow for ${feature}. To enable ${feature}, please upgrade to a license that supports this feature.`, + { level: 'warning' }, ); } } diff --git a/packages/core/src/error-reporter.ts b/packages/core/src/error-reporter.ts index cd20dc9f9a..910b309270 100644 --- a/packages/core/src/error-reporter.ts +++ b/packages/core/src/error-reporter.ts @@ -29,7 +29,10 @@ export class ErrorReporter { const context = executionId ? ` (execution ${executionId})` : ''; do { - const msg = [e.message + context, e.stack ? `\n${e.stack}\n` : ''].join(''); + const msg = [ + e.message + context, + e instanceof ApplicationError && e.level === 'error' && e.stack ? `\n${e.stack}\n` : '', + ].join(''); const meta = e instanceof ApplicationError ? e.extra : undefined; this.logger.error(msg, meta); e = e.cause as Error; diff --git a/packages/core/test/error-reporter.test.ts b/packages/core/test/error-reporter.test.ts index 7cd94fdb4b..9edc27f15c 100644 --- a/packages/core/test/error-reporter.test.ts +++ b/packages/core/test/error-reporter.test.ts @@ -5,6 +5,7 @@ import { mock } from 'jest-mock-extended'; import { ApplicationError } from 'n8n-workflow'; import { ErrorReporter } from '@/error-reporter'; +import type { Logger } from '@/logging/logger'; jest.mock('@sentry/node', () => ({ init: jest.fn(), @@ -101,4 +102,29 @@ describe('ErrorReporter', () => { expect(result).toBeNull(); }); }); + + describe('error', () => { + let error: ApplicationError; + let logger: Logger; + let errorReporter: ErrorReporter; + const metadata = undefined; + + beforeEach(() => { + error = new ApplicationError('Test error'); + logger = mock(); + errorReporter = new ErrorReporter(logger); + }); + + it('should include stack trace for error-level `ApplicationError`', () => { + error.level = 'error'; + errorReporter.error(error); + expect(logger.error).toHaveBeenCalledWith(`Test error\n${error.stack}\n`, metadata); + }); + + it('should exclude stack trace for warning-level `ApplicationError`', () => { + error.level = 'warning'; + errorReporter.error(error); + expect(logger.error).toHaveBeenCalledWith('Test error', metadata); + }); + }); }); diff --git a/packages/editor-ui/src/components/NodeCredentials.test.ts b/packages/editor-ui/src/components/NodeCredentials.test.ts index f5f816b0b1..2839a75c03 100644 --- a/packages/editor-ui/src/components/NodeCredentials.test.ts +++ b/packages/editor-ui/src/components/NodeCredentials.test.ts @@ -7,6 +7,7 @@ import { createComponentRenderer } from '@/__tests__/render'; import { useCredentialsStore } from '@/stores/credentials.store'; import { mockedStore } from '@/__tests__/utils'; import type { INodeUi } from '@/Interface'; +import { useNDVStore } from '@/stores/ndv.store'; const httpNode: INodeUi = { parameters: { @@ -31,6 +32,25 @@ const httpNode: INodeUi = { issues: { parameters: { url: ['Parameter "URL" is required.'] } }, }; +const openAiNode: INodeUi = { + parameters: { + resource: 'text', + operation: 'message', + modelId: { __rl: true, mode: 'list', value: '' }, + messages: { values: [{ content: '', role: 'user' }] }, + simplify: true, + jsonOutput: false, + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.openAi', + typeVersion: 1.8, + position: [440, 0], + id: '17241295-a277-4cdf-8c46-6c3f85b335e9', + name: 'OpenAI', + credentials: { openAiApi: { id: 'byDFnd7vN5GzMVD2', name: 'n8n free OpenAI API credits' } }, + issues: { parameters: { modelId: ['Parameter "Model" is required.'] } }, +}; + describe('NodeCredentials', () => { const defaultRenderOptions: RenderOptions = { pinia: createTestingPinia(), @@ -45,6 +65,9 @@ describe('NodeCredentials', () => { const renderComponent = createComponentRenderer(NodeCredentials, defaultRenderOptions); + const credentialsStore = mockedStore(useCredentialsStore); + const ndvStore = mockedStore(useNDVStore); + beforeAll(() => { credentialsStore.state.credentialTypes = { openAiApi: { @@ -80,9 +103,8 @@ describe('NodeCredentials', () => { }; }); - const credentialsStore = mockedStore(useCredentialsStore); - it('should display available credentials in the dropdown', async () => { + ndvStore.activeNode = httpNode; credentialsStore.state.credentials = { c8vqdPpPClh4TgIO: { id: 'c8vqdPpPClh4TgIO', @@ -103,7 +125,8 @@ describe('NodeCredentials', () => { expect(screen.queryByText('OpenAi account')).toBeInTheDocument(); }); - it('should ignore managed credentials in the dropdown', async () => { + it('should ignore managed credentials in the dropdown if active node is the HTTP node', async () => { + ndvStore.activeNode = httpNode; credentialsStore.state.credentials = { c8vqdPpPClh4TgIO: { id: 'c8vqdPpPClh4TgIO', @@ -132,4 +155,42 @@ describe('NodeCredentials', () => { expect(screen.queryByText('OpenAi account')).toBeInTheDocument(); expect(screen.queryByText('OpenAi account 2')).not.toBeInTheDocument(); }); + + it('should not ignored managed credentials in the dropdown if active node is not the HTTP node', async () => { + ndvStore.activeNode = openAiNode; + credentialsStore.state.credentials = { + c8vqdPpPClh4TgIO: { + id: 'c8vqdPpPClh4TgIO', + name: 'OpenAi account', + type: 'openAiApi', + isManaged: false, + createdAt: '', + updatedAt: '', + }, + SkXM3oUkQvvYS31c: { + id: 'c8vqdPpPClh4TgIO', + name: 'OpenAi account 2', + type: 'openAiApi', + isManaged: true, + createdAt: '', + updatedAt: '', + }, + }; + + renderComponent( + { + props: { + node: openAiNode, + }, + }, + { merge: true }, + ); + + const credentialsSelect = screen.getByTestId('node-credentials-select'); + + await fireEvent.click(credentialsSelect); + + expect(screen.queryByText('OpenAi account')).toBeInTheDocument(); + expect(screen.queryByText('OpenAi account 2')).toBeInTheDocument(); + }); }); diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index 1872db8191..8fe4b15153 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -1,9 +1,10 @@