chore(n8n-test): Apply v2 verification findings

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.
This commit is contained in:
James Gee 2026-08-12 15:25:32 +02:00
parent 1c44da8eda
commit 5e51b07b68
4 changed files with 36 additions and 8 deletions

View File

@ -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);

View File

@ -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@<v> dependencies`).
// nodes-base must be the release-train pairing for this core/workflow (check
// `npm view n8n@<train> 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<Record<string, unknown>>;
/**
* 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,

View File

@ -27,17 +27,23 @@ interface MockEntry {
const registry = new Map<WorkflowJson, Map<string, MockEntry>>();
/**
* 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<string, MockEntry> | 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;
}
/**

View File

@ -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({