From 2b685c33b68feacad613665f74a87093a788dfcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Repe=C4=87?= Date: Tue, 6 Oct 2026 13:07:49 +0000 Subject: [PATCH] test(core): Run only expression tests once per engine in the workflow package (no-changelog) (#40242) --- packages/workflow/test/setup-vm-evaluator.ts | 31 ++++++++++- packages/workflow/vitest.config.ts | 56 ++++++++++++++------ 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/packages/workflow/test/setup-vm-evaluator.ts b/packages/workflow/test/setup-vm-evaluator.ts index 0919b0e20b9..8f92f0624f9 100644 --- a/packages/workflow/test/setup-vm-evaluator.ts +++ b/packages/workflow/test/setup-vm-evaluator.ts @@ -1,9 +1,36 @@ import { Expression } from '../src/expression'; -// Only runs when N8N_EXPRESSION_ENGINE is set to 'vm' or 'quickjs'. +// The engine projects (vitest.config.ts) run the tests that evaluate +// expressions once per engine and set N8N_EXPRESSION_ENGINE; the default +// project runs everything else once and sets it to ''. +const engine = (process.env.N8N_EXPRESSION_ENGINE || undefined) as + | 'vm' + | 'legacy' + | 'quickjs' + | undefined; + +// A test that evaluates an expression outside the engine projects would pass +// against one engine only, so it fails here until it is added to ENGINE_TESTS. +// With no engine initialised every entry point (resolveSimpleParameterValue, +// resolveWithoutWorkflow) ends in the legacy evaluator, so that is the guard. +// vi.mock is hoisted above every other statement, so the factory reads the +// environment itself instead of the `engine` constant below. +vi.mock('../src/expression-evaluator-proxy', async (importOriginal) => { + const original = await importOriginal(); + if (process.env.N8N_EXPRESSION_ENGINE) return original; + + return { + ...original, + evaluateExpression: () => { + throw new Error( + `${expect.getState().testPath} evaluates an expression. Add it to ENGINE_TESTS in vitest.config.ts so it runs once per engine.`, + ); + }, + }; +}); + // Initializes the expression evaluator once per vitest worker before all tests, // and disposes it after. -const engine = process.env.N8N_EXPRESSION_ENGINE as 'vm' | 'quickjs' | undefined; if (engine === 'vm' || engine === 'quickjs') { beforeAll(async () => { await Expression.initExpressionEngine({ diff --git a/packages/workflow/vitest.config.ts b/packages/workflow/vitest.config.ts index a011d7ab377..abe06fda7cd 100644 --- a/packages/workflow/vitest.config.ts +++ b/packages/workflow/vitest.config.ts @@ -1,11 +1,43 @@ import { defineConfig } from 'vitest/config'; import { createBaseInlineConfig } from '@n8n/vitest-config/node'; +// Tests that evaluate expressions, so their outcome depends on the engine. +// Only these run once per engine project; everything else runs once in the +// default project. A test outside this list that evaluates an expression fails +// there (see test/setup-vm-evaluator.ts), so the list cannot drift silently. +const ENGINE_TESTS = [ + 'test/expression.test.ts', + 'test/expression-array-proxy-semantics.test.ts', + 'test/expression-compatibility.test.ts', + 'test/expression-engine-start-failure.test.ts', + 'test/expression-item-accessor-engine-parity.test.ts', + 'test/expression-items-engine-parity.test.ts', + 'test/expression-nested-data.test.ts', + 'test/expression-vm-errors.test.ts', + 'test/expression-with-isolate.test.ts', + 'test/ExpressionExtensions/*.test.ts', + 'test/native-evaluation-fuzz.test.ts', + 'test/native-evaluation-parity.test.ts', + 'test/webhook-description-fields.test.ts', + 'test/workflow.test.ts', + 'test/workflow-data-proxy.test.ts', + 'test/workflow-expression.test.ts', +]; + const { reporters, outputFile, ...sharedTestConfig } = createBaseInlineConfig({ include: ['test/**/*.test.ts'], setupFiles: ['./test/setup-vm-evaluator.ts'], }); +const engineProject = (engine: 'vm' | 'legacy' | 'quickjs') => ({ + test: { + ...sharedTestConfig, + include: ENGINE_TESTS, + name: `${engine}-engine`, + env: { N8N_EXPRESSION_ENGINE: engine }, + }, +}); + export default defineConfig({ test: { reporters, @@ -14,24 +46,16 @@ export default defineConfig({ { test: { ...sharedTestConfig, - name: 'vm-engine', - env: { N8N_EXPRESSION_ENGINE: 'vm' }, - }, - }, - { - test: { - ...sharedTestConfig, - name: 'legacy-engine', - env: { N8N_EXPRESSION_ENGINE: 'legacy' }, - }, - }, - { - test: { - ...sharedTestConfig, - name: 'quickjs-engine', - env: { N8N_EXPRESSION_ENGINE: 'quickjs' }, + exclude: [...(sharedTestConfig.exclude ?? []), ...ENGINE_TESTS], + name: 'default', + // Explicitly no engine, so a shell N8N_EXPRESSION_ENGINE cannot turn + // this project into a fourth engine run and disable the guard. + env: { N8N_EXPRESSION_ENGINE: '' }, }, }, + engineProject('vm'), + engineProject('legacy'), + engineProject('quickjs'), ], }, });