mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(core): Make the vm expression engine the default (#35738)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
95e13260c4
commit
98e0952c36
5
.github/WORKFLOWS.md
vendored
5
.github/WORKFLOWS.md
vendored
|
|
@ -71,7 +71,6 @@ Complete reference for n8n's `.github/` folder.
|
|||
│ │ Schedule │───▶│ Nightly/Weekly Jobs │ ┌────────────┐ │
|
||||
│ │ (cron) │ │ ├─ docker-build-push (nightly) │───▶│ Images │ │
|
||||
│ └──────────┘ │ ├─ test-benchmark-nightly │───▶│ Metrics │ │
|
||||
│ │ ├─ test-e2e-vm-expressions │ │
|
||||
│ │ └─ test-e2e-coverage-weekly │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ │
|
||||
|
|
@ -298,9 +297,6 @@ release-publish.yml
|
|||
test-workflows-nightly.yml (manual dispatch only — nightly schedule disabled, DEVP-544)
|
||||
└──────────────────────────▶ test-workflows-callable.yml
|
||||
|
||||
test-e2e-vm-expressions-nightly.yml
|
||||
└──────────────────────────▶ test-e2e-reusable.yml
|
||||
|
||||
PR Comment Dispatchers (triggered by /command in PR comments):
|
||||
test-workflows-pr-comment.yml
|
||||
└──────────────────────────▶ test-workflows-callable.yml
|
||||
|
|
@ -414,7 +410,6 @@ Push to master/1.x
|
|||
| Daily 00:00 | `util-check-docs-urls.yml` | Doc link validation |
|
||||
| Daily 01:30, 02:30, 03:30 | `test-benchmark-nightly.yml` | Performance benchmarks |
|
||||
| Daily 02:00 | `test-get-n8n.yml` | get.n8n.io installer health |
|
||||
| Daily 04:00 | `test-e2e-vm-expressions-nightly.yml`| VM expression E2E |
|
||||
| Daily 05:00 | `test-benchmark-destroy-nightly.yml`| Cleanup benchmark env |
|
||||
| Daily 06:00 | `util-sync-master-to-3x.yml` | Replay 3.x onto master (v3) |
|
||||
| Daily 08:00 | `build-v3-nightly.yml` | Nightly v3 Docker images |
|
||||
|
|
|
|||
2
.github/workflows/test-e2e-reusable.yml
vendored
2
.github/workflows/test-e2e-reusable.yml
vendored
|
|
@ -43,7 +43,7 @@ on:
|
|||
default: '[{"shard":1,"images":""}]'
|
||||
type: string
|
||||
n8n-env:
|
||||
description: 'JSON string of n8n env vars to inject into test containers, e.g. {"N8N_EXPRESSION_ENGINE":"vm"}'
|
||||
description: 'JSON string of n8n env vars to inject into test containers, e.g. {"N8N_EXPRESSION_ENGINE":"legacy"}'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
name: 'Test: E2E VM Expressions Nightly'
|
||||
run-name: "Test: E2E VM Expressions (${{ inputs.branch || 'nightly' }})"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch/ref to test'
|
||||
required: false
|
||||
type: string
|
||||
default: 'master'
|
||||
|
||||
jobs:
|
||||
prepare-docker:
|
||||
uses: ./.github/workflows/prepare-docker-reusable.yml
|
||||
with:
|
||||
branch: ${{ github.event_name == 'schedule' && 'master' || inputs.branch }}
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E
|
||||
needs: [prepare-docker]
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
branch: ${{ github.event_name == 'schedule' && 'master' || inputs.branch }}
|
||||
test-mode: docker-artifact
|
||||
test-command: pnpm --filter=n8n-playwright test:container:multi-main:e2e
|
||||
workers: '1'
|
||||
pre-generated-matrix: '[{"shard":1},{"shard":2},{"shard":3},{"shard":4},{"shard":5},{"shard":6},{"shard":7},{"shard":8},{"shard":9},{"shard":10},{"shard":11},{"shard":12},{"shard":13},{"shard":14},{"shard":15},{"shard":16}]'
|
||||
n8n-env: '{"N8N_EXPRESSION_ENGINE":"vm"}'
|
||||
artifact-prefix: vm-expressions
|
||||
currents-project-id: 'LRxcNt'
|
||||
secrets: inherit
|
||||
|
||||
notify-on-failure:
|
||||
name: Notify Slack on failure
|
||||
runs-on: ubuntu-slim
|
||||
needs: [e2e]
|
||||
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: .github/scripts/slack
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Notify Slack
|
||||
env:
|
||||
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
|
||||
run: |
|
||||
node .github/scripts/slack/notify.mjs \
|
||||
--channel '#project-secure-expression-evaluation' \
|
||||
--text 'VM expression E2E nightly failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
|
|
@ -10,6 +10,10 @@ describe('ExpressionEngineConfig', () => {
|
|||
});
|
||||
|
||||
describe('defaults', () => {
|
||||
test('engine defaults to vm', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).engine).toBe('vm');
|
||||
});
|
||||
|
||||
test('bridgeTimeout defaults to 5000', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(5000);
|
||||
});
|
||||
|
|
@ -19,6 +23,21 @@ describe('ExpressionEngineConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('N8N_EXPRESSION_ENGINE', () => {
|
||||
test('overrides engine to legacy', () => {
|
||||
vi.stubEnv('N8N_EXPRESSION_ENGINE', 'legacy');
|
||||
expect(Container.get(ExpressionEngineConfig).engine).toBe('legacy');
|
||||
});
|
||||
|
||||
test('falls back to default on invalid value', () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
vi.stubEnv('N8N_EXPRESSION_ENGINE', 'not-an-engine');
|
||||
expect(Container.get(ExpressionEngineConfig).engine).toBe('vm');
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT', () => {
|
||||
test('overrides idleTimeout', () => {
|
||||
vi.stubEnv('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT', '60');
|
||||
|
|
|
|||
|
|
@ -8,13 +8,11 @@ const expressionEngineSchema = z.enum(['legacy', 'vm']);
|
|||
export class ExpressionEngineConfig {
|
||||
/**
|
||||
* Which expression engine to use.
|
||||
* - `legacy` runs expressions without isolation.
|
||||
* - `vm` runs expressions in a V8 isolate.
|
||||
*
|
||||
* `vm` is currently **experimental**. Use at your own risk.
|
||||
* - `vm` (default) runs expressions in a V8 isolate.
|
||||
* - `legacy` runs expressions without isolation. Less secure and soon to be deprecated.
|
||||
*/
|
||||
@Env('N8N_EXPRESSION_ENGINE', expressionEngineSchema)
|
||||
engine: 'legacy' | 'vm' = 'legacy';
|
||||
engine: 'legacy' | 'vm' = 'vm';
|
||||
|
||||
/** Number of V8 isolates ready in the pool. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_POOL_SIZE')
|
||||
|
|
|
|||
|
|
@ -627,7 +627,7 @@ describe('GlobalConfig', () => {
|
|||
trimOnStartUp: false,
|
||||
},
|
||||
expressionEngine: {
|
||||
engine: 'legacy',
|
||||
engine: 'vm',
|
||||
poolSize: 1,
|
||||
maxCodeCacheSize: 1024,
|
||||
bridgeTimeout: 5000,
|
||||
|
|
|
|||
|
|
@ -4,18 +4,16 @@ Secure, isolated expression evaluation runtime for n8n workflows.
|
|||
|
||||
## Status
|
||||
|
||||
**In progress — landing as a series of incremental PRs.**
|
||||
**Shipped — the `vm` engine is n8n's default expression engine.**
|
||||
|
||||
Implemented so far:
|
||||
- ✅ TypeScript interfaces and architecture design (PR 1)
|
||||
- ✅ Core architecture documentation (PR 1)
|
||||
- ✅ Runtime bundle: extension functions, deep lazy proxy system (PR 2)
|
||||
- ✅ `IsolatedVmBridge`: V8 isolate management via `isolated-vm` (PR 3)
|
||||
- ✅ `ExpressionEvaluator`: tournament integration, expression code caching (PR 4)
|
||||
- ✅ Integration tests (PR 4)
|
||||
- ✅ TypeScript interfaces and architecture design
|
||||
- ✅ Runtime bundle: extension functions, deep lazy proxy system
|
||||
- ✅ `IsolatedVmBridge`: V8 isolate management via `isolated-vm`
|
||||
- ✅ `ExpressionEvaluator`: tournament integration, expression code caching, isolate pooling
|
||||
- ✅ Workflow integration — default engine; `N8N_EXPRESSION_ENGINE=legacy` opts out
|
||||
- ✅ Observability (metrics, traces, logs) wired up in `packages/cli`
|
||||
|
||||
Coming in later PRs:
|
||||
- 🚧 Workflow integration behind `N8N_EXPRESSION_ENGINE=vm` flag (PR 5)
|
||||
Coming later:
|
||||
- 🚧 Web Worker support (Phase 2+)
|
||||
- 🚧 Performance optimizations (Phase 3)
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ Future support (Phase 2+):
|
|||
|
||||
- 🔒 **Secure**: Expressions run in isolated V8 contexts with memory limits (128MB) and timeouts (5s)
|
||||
- 🚀 **Performant**: Lazy data loading via proxies, script compilation caching, and expression code caching
|
||||
- 📊 **Observable**: Built-in metrics, traces, and logs support (interfaces defined; providers coming later)
|
||||
- 📊 **Observable**: Built-in metrics, traces, and logs support via `ObservabilityProvider`
|
||||
- 🌐 **Universal**: Works in Node.js backend (browsers and task runners in Phase 2+)
|
||||
- 🛡️ **AST Security**: Tournament AST hooks (`ThisSanitizer`, `PrototypeSanitizer`, `DollarSignValidator`) validate expressions before execution
|
||||
|
||||
|
|
@ -61,32 +59,33 @@ pnpm add @n8n/expression-runtime
|
|||
```typescript
|
||||
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
|
||||
|
||||
// Create bridge
|
||||
const bridge = new IsolatedVmBridge({
|
||||
memoryLimit: 128,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Create evaluator
|
||||
// Create evaluator with a bridge factory (bridges are pooled)
|
||||
const evaluator = new ExpressionEvaluator({
|
||||
bridge,
|
||||
createBridge: () => new IsolatedVmBridge({ memoryLimit: 128, timeout: 5000 }),
|
||||
maxCodeCacheSize: 1024,
|
||||
});
|
||||
|
||||
// Initialize
|
||||
await evaluator.initialize();
|
||||
|
||||
// Evaluate expression using {{ }} template syntax
|
||||
// Acquire an isolate for a caller, evaluate, release
|
||||
const caller = {};
|
||||
await evaluator.acquire(caller);
|
||||
|
||||
const result = evaluator.evaluate(
|
||||
'{{ $json.user.email }}',
|
||||
{
|
||||
$json: {
|
||||
user: { email: 'test@example.com' }
|
||||
}
|
||||
}
|
||||
},
|
||||
caller,
|
||||
);
|
||||
|
||||
console.log(result); // "test@example.com"
|
||||
|
||||
await evaluator.release(caller);
|
||||
|
||||
// Clean up
|
||||
await evaluator.dispose();
|
||||
```
|
||||
|
|
@ -103,9 +102,9 @@ import {
|
|||
DollarSignValidator,
|
||||
} from 'n8n-workflow/expression-sandboxing';
|
||||
|
||||
const bridge = new IsolatedVmBridge({ timeout: 5000 });
|
||||
const evaluator = new ExpressionEvaluator({
|
||||
bridge,
|
||||
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
||||
maxCodeCacheSize: 1024,
|
||||
hooks: {
|
||||
before: [ThisSanitizer],
|
||||
after: [PrototypeSanitizer, DollarSignValidator],
|
||||
|
|
@ -117,22 +116,19 @@ await evaluator.initialize();
|
|||
|
||||
When `hooks` is omitted the evaluator still runs tournament transformation (template parsing, `this` binding) but without AST security validation — suitable for development and testing.
|
||||
|
||||
### With Observability (Not Yet Implemented)
|
||||
### With Observability
|
||||
|
||||
Pass an `ObservabilityProvider` implementation to emit metrics, traces, and logs for evaluations:
|
||||
|
||||
```typescript
|
||||
import { OpenTelemetryProvider } from '@n8n/expression-runtime/observability';
|
||||
|
||||
const observability = new OpenTelemetryProvider({
|
||||
serviceName: 'n8n-expressions',
|
||||
});
|
||||
|
||||
const evaluator = new ExpressionEvaluator({
|
||||
bridge,
|
||||
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
||||
maxCodeCacheSize: 1024,
|
||||
observability,
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: Observability providers are not yet implemented. The `ObservabilityProvider` interface exists but no implementations are available yet.
|
||||
This package defines the `ObservabilityProvider` interface; the production implementation lives in `packages/cli/src/expression-observability/expression-observability.provider.ts` and is wired up during backend startup. It is controlled via the `N8N_EXPRESSION_ENGINE_OBSERVABILITY_*` and `N8N_EXPRESSION_ENGINE_TRACES_*` environment variables (see below).
|
||||
|
||||
## API
|
||||
|
||||
|
|
@ -144,7 +140,9 @@ Main class for expression evaluation.
|
|||
class ExpressionEvaluator {
|
||||
constructor(config: EvaluatorConfig);
|
||||
initialize(): Promise<void>;
|
||||
evaluate(expression: string, data: WorkflowData, options?: EvaluateOptions): unknown;
|
||||
acquire(owner: object): Promise<boolean>;
|
||||
evaluate(expression: string, data: WorkflowData, caller: object, options?: EvaluateOptions): unknown;
|
||||
release(owner: object): Promise<void>;
|
||||
dispose(): Promise<void>;
|
||||
isDisposed(): boolean;
|
||||
}
|
||||
|
|
@ -179,38 +177,41 @@ interface RuntimeBridge {
|
|||
|
||||
```typescript
|
||||
interface EvaluatorConfig {
|
||||
bridge: RuntimeBridge; // required
|
||||
observability?: ObservabilityProvider; // optional - interfaces defined, providers not yet implemented
|
||||
createBridge: () => RuntimeBridge; // required - factory, bridges are pooled
|
||||
maxCodeCacheSize: number; // required - LRU size for tournament-transformed code
|
||||
observability?: ObservabilityProvider; // optional - metrics/traces/logs provider
|
||||
hooks?: TournamentHooks; // optional - AST security hooks for tournament
|
||||
}
|
||||
|
||||
interface BridgeConfig {
|
||||
memoryLimit?: number; // Default: 128 MB
|
||||
timeout?: number; // Default: 5000 ms
|
||||
debug?: boolean; // Default: false
|
||||
poolSize?: number; // optional - pre-warmed bridges, default 1
|
||||
idleTimeoutMs?: number; // optional - scale pool to 0 after idle period
|
||||
logger?: Logger; // optional - falls back to no-op
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables (Not Yet Implemented)
|
||||
## Environment Variables
|
||||
|
||||
In n8n, the evaluator is configured via `ExpressionEngineConfig` (`@n8n/config`):
|
||||
|
||||
```bash
|
||||
# Bridge configuration (not yet implemented)
|
||||
N8N_EXPRESSION_MEMORY_LIMIT_MB=128
|
||||
N8N_EXPRESSION_TIMEOUT_MS=5000
|
||||
N8N_EXPRESSION_DEBUG=false
|
||||
# Engine selection ('vm' is the default; 'legacy' opts out of isolation)
|
||||
N8N_EXPRESSION_ENGINE=vm
|
||||
|
||||
# Code cache (not yet implemented - caches transformed code, not results)
|
||||
N8N_EXPRESSION_CODE_CACHE_ENABLED=true
|
||||
N8N_EXPRESSION_CODE_CACHE_MAX_SIZE=1000
|
||||
# Isolate pool and code cache
|
||||
N8N_EXPRESSION_ENGINE_POOL_SIZE=1
|
||||
N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE=1024
|
||||
N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT= # seconds; unset = pool never scales to 0
|
||||
|
||||
# Observability (not yet implemented)
|
||||
N8N_EXPRESSION_OBSERVABILITY_ENABLED=true
|
||||
N8N_EXPRESSION_METRICS_ENABLED=true
|
||||
N8N_EXPRESSION_TRACES_ENABLED=true
|
||||
N8N_EXPRESSION_TRACE_SAMPLE_RATE=0.01
|
||||
# Bridge limits
|
||||
N8N_EXPRESSION_ENGINE_TIMEOUT=5000 # ms
|
||||
N8N_EXPRESSION_ENGINE_MEMORY_LIMIT=128 # MB
|
||||
|
||||
# Observability
|
||||
N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED=true
|
||||
N8N_EXPRESSION_ENGINE_TRACES_ENABLED=true
|
||||
N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS=50
|
||||
N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE=0.0
|
||||
```
|
||||
|
||||
**Note**: Currently, configuration is passed via constructor options. Environment variable support will be added in future phases.
|
||||
See `packages/@n8n/config/src/configs/expression-engine.config.ts` for the authoritative list and defaults.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -243,13 +244,18 @@ import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
|
|||
|
||||
describe('ExpressionEvaluator', () => {
|
||||
it('evaluates simple expression', async () => {
|
||||
const bridge = new IsolatedVmBridge({ timeout: 5000 });
|
||||
const evaluator = new ExpressionEvaluator({ bridge });
|
||||
const evaluator = new ExpressionEvaluator({
|
||||
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
||||
maxCodeCacheSize: 1024,
|
||||
});
|
||||
|
||||
await evaluator.initialize();
|
||||
|
||||
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } });
|
||||
const caller = {};
|
||||
await evaluator.acquire(caller);
|
||||
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } }, caller);
|
||||
expect(result).toBe(42);
|
||||
await evaluator.release(caller);
|
||||
|
||||
await evaluator.dispose();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { DeprecationService } from '@/deprecation/deprecation.service';
|
|||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||
import { WorkflowFailureNotificationEventRelay } from '@/events/relays/workflow-failure-notification.event-relay';
|
||||
import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
|
|
@ -40,6 +41,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
|||
const shutdownService = mockInstance(ShutdownService);
|
||||
const deprecationService = mockInstance(DeprecationService);
|
||||
mockInstance(MessageEventBus);
|
||||
mockInstance(ExpressionObservabilityProvider);
|
||||
const posthogClient = mockInstance(PostHogClient);
|
||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||
const externalHooks = mockInstance(ExternalHooks);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { DeprecationService } from '@/deprecation/deprecation.service';
|
|||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||
import { WorkflowFailureNotificationEventRelay } from '@/events/relays/workflow-failure-notification.event-relay';
|
||||
import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
|
|
@ -40,6 +41,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
|||
const shutdownService = mockInstance(ShutdownService);
|
||||
const deprecationService = mockInstance(DeprecationService);
|
||||
mockInstance(MessageEventBus);
|
||||
mockInstance(ExpressionObservabilityProvider);
|
||||
const posthogClient = mockInstance(PostHogClient);
|
||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||
const externalHooks = mockInstance(ExternalHooks);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ describe('DeprecationService', () => {
|
|||
['WEBHOOK_URL', 'https://example.com/', true],
|
||||
['N8N_DEFAULT_BINARY_DATA_MODE', 'default', true],
|
||||
['N8N_DEFAULT_BINARY_DATA_MODE', 'filesystem', false],
|
||||
['N8N_EXPRESSION_ENGINE', 'legacy', true],
|
||||
['N8N_EXPRESSION_ENGINE', 'vm', false],
|
||||
])('should detect when %s is `%s`', (envVar, value, mustWarn) => {
|
||||
toTest(envVar, value, mustWarn);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ export class DeprecationService {
|
|||
'The default for this variable will be reduced from 5000 to 1000 in a future version. Set it explicitly to keep your current limit.',
|
||||
checkValue: (value?: string) => value === undefined,
|
||||
},
|
||||
{
|
||||
envVar: 'N8N_EXPRESSION_ENGINE',
|
||||
message:
|
||||
'The `legacy` expression engine runs expressions without isolation, is no longer considered secure, and will be removed in a future version. Remove this environment variable to use the default `vm` engine.',
|
||||
checkValue: (value?: string) => value === 'legacy',
|
||||
},
|
||||
{
|
||||
envVar: 'N8N_DEFAULT_BINARY_DATA_MODE',
|
||||
message:
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export class N8NStartupError extends Error {
|
|||
|
||||
const BASE_ENV: Record<string, string> = {
|
||||
N8N_LOG_LEVEL: 'debug',
|
||||
N8N_EXPRESSION_ENGINE: 'vm',
|
||||
N8N_ENCRYPTION_KEY: process.env.N8N_ENCRYPTION_KEY ?? 'test-encryption-key',
|
||||
E2E_TESTS: 'false',
|
||||
QUEUE_HEALTH_CHECK_ACTIVE: 'true',
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ if (BACKEND_URL && !SKIP_WEB_SERVER) {
|
|||
N8N_METRICS: 'true',
|
||||
N8N_RESTRICT_FILE_ACCESS_TO: '',
|
||||
N8N_DYNAMIC_BANNERS_ENABLED: 'false',
|
||||
N8N_EXPRESSION_ENGINE: 'vm',
|
||||
...getTestEnv(),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export class Expression {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize the VM evaluator (if feature flag is enabled).
|
||||
* Initialize the VM evaluator (no-op when the legacy engine is selected).
|
||||
* Should be called once during application startup.
|
||||
* Only available in Node.js environments (not in browser).
|
||||
*/
|
||||
|
|
@ -645,7 +645,7 @@ export class Expression {
|
|||
if (Expression.expressionEngine === 'vm' && !IS_FRONTEND) {
|
||||
if (!Expression.vmEvaluator) {
|
||||
throw new UnexpectedError(
|
||||
'N8N_EXPRESSION_ENGINE=vm is enabled but VM evaluator is not initialized. Call Expression.initExpressionEngine() during application startup.',
|
||||
'The VM expression engine has not been initialized. Call Expression.initExpressionEngine() during application startup.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export default {
|
|||
packageManager: 'pnpm',
|
||||
testRunner: 'vitest',
|
||||
plugins: ['@stryker-mutator/vitest-runner'],
|
||||
// Use the Stryker-specific vitest config (vm-engine only) — see
|
||||
// Use the Stryker-specific vitest config (legacy-engine only) — see
|
||||
// vitest.stryker.config.ts for the rationale.
|
||||
vitest: {
|
||||
configFile: 'vitest.stryker.config.ts',
|
||||
|
|
@ -23,7 +23,7 @@ export default {
|
|||
jsonReporter: { fileName: 'reports/mutation/raw.json' },
|
||||
// Cap low: a string/utils mutant running 15s+ is a spinning loop-condition mutant; fail it fast.
|
||||
timeoutMS: 15_000,
|
||||
// Each Stryker worker spawns vitest with one project (vm-engine, via
|
||||
// Each Stryker worker spawns vitest with one project (legacy-engine, via
|
||||
// vitest.stryker.config.ts). Default 4 is fine on CI runners; lower
|
||||
// locally if you hit OOM during the initial dry run via
|
||||
// `STRYKER_CONCURRENCY=1`.
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@ export default defineConfig({
|
|||
{
|
||||
test: {
|
||||
...sharedTestConfig,
|
||||
name: 'legacy-engine',
|
||||
name: 'vm-engine',
|
||||
env: { N8N_EXPRESSION_ENGINE: 'vm' },
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
...sharedTestConfig,
|
||||
name: 'vm-engine',
|
||||
env: { N8N_EXPRESSION_ENGINE: 'vm' },
|
||||
name: 'legacy-engine',
|
||||
env: { N8N_EXPRESSION_ENGINE: 'legacy' },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// Runs the legacy expression engine, not vm/isolated-vm: the vm evaluator
|
||||
// SIGABRTs in Stryker's worker on teardown (upstream isolated-vm #464, repros on
|
||||
// Node 22 + 24). Trade-off: vm-only branches in expression.ts go unscored. Swap
|
||||
// Node 22 + 24). Trade-off: vm-only branches in expression.ts — the production
|
||||
// default engine — go unscored. Swap
|
||||
// back to N8N_EXPRESSION_ENGINE=vm once #464 is fixed or we pnpm-patch the guard.
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user