From 5e51b07b68e7aa20c7fb795f5eead484e615d8ea Mon Sep 17 00:00:00 2001 From: James Gee <1285296+geemanjs@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:25:32 +0200 Subject: [PATCH] chore(n8n-test): Apply v2 verification findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard mockNode against unknown node names, reset captures per run, refuse Code-node/sub-workflow capabilities with clear messages, pin nodes-base to its release-train pairing (2.16.0 — avoids a nested second n8n-workflow copy), and fix the mock-persistence wording. --- packages/n8n-test/README.md | 4 ++++ packages/n8n-test/scripts/bundle.mjs | 9 ++++++--- packages/n8n-test/src/mock-node.ts | 19 ++++++++++++++----- packages/n8n-test/src/run-workflow.ts | 12 ++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/n8n-test/README.md b/packages/n8n-test/README.md index f7ec1e9f5e0..1b0a521994b 100644 --- a/packages/n8n-test/README.md +++ b/packages/n8n-test/README.md @@ -122,6 +122,10 @@ Feasibility is proven, but these are the edges the production version must own: interception point — mocking there would also catch non-HTTP-module clients. - **Speed is a non-issue.** The lazy loader brings node-type load to ~1s per process; the full demo suite runs in under 3s. +- **(v2) Code nodes and sub-workflows need mocking.** Task-runner-backed nodes + (the Code node) and Execute Sub-workflow call engine capabilities a test + harness cannot provide; `runWorkflow` refuses them with a clear message — + `mockNode()` is the answer for both. - **(v2) Released-engine drift is the real prebake tax.** Three shims were needed to run on npm releases: `vitest-mock-extended`'s CJS build cannot be `require`d outside Vite's pipeline (hence the plain `additionalData` object); diff --git a/packages/n8n-test/scripts/bundle.mjs b/packages/n8n-test/scripts/bundle.mjs index 35879322a1d..0c543d49db9 100644 --- a/packages/n8n-test/scripts/bundle.mjs +++ b/packages/n8n-test/scripts/bundle.mjs @@ -27,10 +27,12 @@ const externals = [ // Pinned to current npm releases; bump deliberately, the engine API is the risk surface. // The @n8n helper pins MUST match what the released n8n-core depends on, or npm installs // a second copy and the DI container splits (`npm view n8n-core@ dependencies`). +// nodes-base must be the release-train pairing for this core/workflow (check +// `npm view n8n@ dependencies`) or it nests its own second n8n-workflow copy. const runtimeVersions = { 'n8n-core': '2.16.1', 'n8n-workflow': '2.16.0', - 'n8n-nodes-base': '2.15.1', + 'n8n-nodes-base': '2.16.0', '@n8n/di': '0.10.0', '@n8n/decorators': '1.16.0', }; @@ -112,8 +114,9 @@ export declare function runWorkflow( ): Promise>; /** - * Replaces the named node's execution with a canned output for the next runWorkflow of - * this workflow object. Multiple nodes may be mocked; last mock per node name wins. + * Replaces the named node's execution with a canned output for every subsequent + * runWorkflow of this workflow object, until clearNodeMocks() runs. Multiple nodes may + * be mocked; last mock per node name wins. */ export declare function mockNode( workflowJson: unknown, diff --git a/packages/n8n-test/src/mock-node.ts b/packages/n8n-test/src/mock-node.ts index f9fcc3610cf..58d64f9b0d6 100644 --- a/packages/n8n-test/src/mock-node.ts +++ b/packages/n8n-test/src/mock-node.ts @@ -27,17 +27,23 @@ interface MockEntry { const registry = new Map>(); /** - * Replaces the named node's execution with a canned output for the next - * {@link runWorkflow} of this workflow object. The JSON itself is never mutated. - * Mocking the same node again replaces the previous mock (last one wins). + * Replaces the named node's execution with a canned output for every subsequent + * {@link runWorkflow} of this workflow object, until {@link clearNodeMocks} runs. + * The JSON itself is never mutated. Mocking the same node again replaces the + * previous mock (last one wins). * - * The returned handle captures what the node received, for assertions. + * The returned handle captures what the node received on the latest run. */ export function mockNode( workflow: WorkflowJson, nodeName: string, output: IDataObject, ): MockedNodeHandle { + if (!workflow.nodes.some((node) => node.name === nodeName)) { + throw new UnexpectedError( + `Cannot mock node "${nodeName}": the workflow has no node with that name`, + ); + } let mocks = registry.get(workflow); if (!mocks) { mocks = new Map(); @@ -54,7 +60,10 @@ export function clearNodeMocks(): void { } export function getNodeMocks(workflow: WorkflowJson): Map | undefined { - return registry.get(workflow); + const mocks = registry.get(workflow); + // A fresh run means fresh captures: a handle must never report a previous run's input. + if (mocks) for (const entry of mocks.values()) entry.capturedInput = undefined; + return mocks; } /** diff --git a/packages/n8n-test/src/run-workflow.ts b/packages/n8n-test/src/run-workflow.ts index 999115634a4..cb1d06cc9ca 100644 --- a/packages/n8n-test/src/run-workflow.ts +++ b/packages/n8n-test/src/run-workflow.ts @@ -126,6 +126,18 @@ export async function runWorkflow( instanceBaseUrl: 'http://localhost', variables: {}, currentNodeExecutionIndex: 0, + // Friendly refusals for engine capabilities a test harness cannot provide — without + // these, nodes fail with raw TypeErrors ("... is not a function"). + executeWorkflow: () => { + throw new UnexpectedError( + 'Sub-workflow execution is not supported by n8n-test — mock the node with mockNode() instead', + ); + }, + startRunnerTask: () => { + throw new UnexpectedError( + 'Task-runner nodes (e.g. the Code node) are not supported by n8n-test — mock them with mockNode() instead', + ); + }, } as unknown as IWorkflowExecuteAdditionalData; const runExecutionData = createRunExecutionData({