docs: Add n8n-test PoC v2 spec and plan

This commit is contained in:
James Gee 2026-08-12 14:14:03 +02:00
parent 97225bc0cf
commit 267de8a650
3 changed files with 327 additions and 133 deletions

View File

@ -0,0 +1,147 @@
# Spec: `n8n-test` PoC — version 2
> Extends [`n8n-test-poc.md`](n8n-test-poc.md) (v1, feasibility proven on branch
> `ligo-workflow-testing-poc`). Same ground rules: throwaway spike on branch
> `ligo-workflow-testing-v2`, never merged, no tests for the harness itself.
> Source of intent: the "Hack - Workflow testing demo" Notion page, §Version 2.
## Objective
Two phases:
- **Phase A — `mockNode`.** Swap one named node's execution for a canned result
while everything else runs real, with input capture for assertions. Star
demo: mocking an Execute Sub-workflow node — which v1 proved cannot run for
real under the test harness, so mocking it is both the honest and the useful
move.
- **Phase B — the CI story v1 kept as talk-track.** A git repo holding an
exploded project package export; a PR changing a workflow runs the workflow
tests in a GitHub Action, fast; the reviewer watches the check go red → green.
## Public API additions (Phase A)
```ts
import { mockNode, runWorkflow } from 'n8n-test';
// Registers an override for the named node in this workflow. Does not mutate the JSON.
const sub = mockNode(workflowJson, 'Execute sub-workflow', { subworkflowOutput: 'sfoutput' });
const result = await runWorkflow(workflowJson);
expect(result.subworkflowOutput).toBe('sfoutput');
// What the mocked node received (first item's json) — the capture half of the API.
expect(sub.input()).toMatchObject({ data: 'Hello world' });
```
Multiple nodes in one workflow can be mocked independently — each call returns
its own handle, and with the HTTP node mocked too the test needs no nock at all:
```ts
const http = mockNode(workflowJson, 'Http request', { data: 'canned response' });
const sub = mockNode(workflowJson, 'Execute sub-workflow', { subworkflowOutput: 'sfoutput' });
const result = await runWorkflow(workflowJson);
expect(result.subworkflowOutput).toBe('sfoutput');
expect(sub.input()).toMatchObject({ 'test-output': 'canned response' });
```
Semantics:
- Matched by **node name** within the given workflow object; the override lives
in a module-level registry keyed by the workflow object (WeakMap), applied at
`runWorkflow` time — the JSON is never mutated.
- **Any number of nodes per workflow** may be mocked; mocking the same node name
again replaces the previous override (last wins) and returns a fresh handle.
- The mocked node emits the canned object as a single item; the handle's
`input()` returns the first item's `json` the node received (undefined until run).
- `clearNodeMocks()` is exported and called from the demo `afterEach` so mocks
never leak between tests.
- Mechanism: at run time the target node's `type` is swapped for an in-process
mock node implementation (registered alongside the loader's node types, the
way `core/nodes-testing`'s `testData` node already is) whose `execute` records
`getInputData()` and returns the canned item.
## Demo additions (Phase A)
- New `demo/workflow-with-subworkflow.json`: trigger → HTTP Request →
Edit Fields → **Execute Sub-workflow** (last node). Kept separate from the v1
`workflow.json` so the three v1 tests stay untouched — and because without a
mock this workflow *cannot* run, which is exactly the story.
- New demo test `workflow with subworkflow mocked`, per the API sketch above.
- New demo test `workflow with every dependency mocked`: HTTP node **and**
sub-workflow node both mocked, no nock in the test — node-level mocking
standing on its own.
## Phase B — repo, artifact, Action
- **Demo repo** on James's personal GitHub (disposable): an **exploded project
package export** (the Ligo `.n8np` layout untarred: `manifest.json`,
`projects/<project>/workflows/*.json`), a `tests/` folder whose test files
import workflow JSONs from that layout, a vanilla vitest setup, and one
GitHub Action running on `pull_request`.
- **Prebaked artifact, npm runtime.** CI must be fast (~2-4 min), so the Action
does NOT build the monorepo. Instead:
- `n8n-test` is bundled locally (esbuild/tsdown) into a self-contained
tarball: our `src/` **plus vendored `core/nodes-testing` components** (they
are not in the published `n8n-core` npm tarball; their relative `../dist/*`
imports get rewritten to `n8n-core/dist/*` deep requires).
- The tarball is attached to a GitHub release on the demo repo; the demo
repo's `package.json` installs it by URL, alongside **released npm**
`n8n-core` / `n8n-workflow` / `n8n-nodes-base` / `nock` / `vitest`.
- In a plain node_modules world the DI single-container problem disappears
(everything loads as CJS dist), so the demo repo's vitest config is vanilla
plus `reflect-metadata` setup.
- **Demo script:** PR changes the workflow's field mapping → check fails with a
readable assertion diff → fix commit → green.
## Commands
```bash
# Phase A (monorepo, as v1)
pnpm --filter n8n-test demo
# Phase B (monorepo side)
pnpm --filter n8n-test bundle # produce the self-contained tarball for the demo repo
```
## Success Criteria
1. Phase A: `pnpm --filter n8n-test demo` runs 5+ tests green, including the
sub-workflow mock with an `input()` capture assertion and the multi-node
test (HTTP + sub-workflow both mocked, nock-free); v1's three tests
unchanged.
2. Phase A honesty check: running the sub-workflow demo workflow *without*
`mockNode` rejects (it cannot execute for real) — asserted in a demo test.
3. Phase B: a real PR on the demo repo shows the workflow-test check failing on
a broken workflow change and passing once fixed, in under ~4 minutes.
4. The README gains a v2 section: `mockNode` usage + how the demo repo is wired.
## Boundaries
- **Always:** keep monorepo changes inside `packages/n8n-test` (+ specs/plan);
spike branch only.
- **Ask first:** anything that must run against the spike branch's n8n-core
rather than released npm (that flips the CI strategy); creating anything
under the n8n-io org; publishing anything to npm.
- **Never:** merge; expose secrets in the demo repo; real network calls in tests.
## Out of Scope
`replaceNode` / `fetchWorkflow`, non-throw error modes, credentials injection,
multi-item canned outputs, mocking by node type (name-only), npm publishing.
The SSRF-helper seam remains the documented production direction for HTTP
mocking; `mockNode` is orthogonal (node-level, not transport-level).
## Risks
| Risk | Impact | Mitigation |
|------|--------|------------|
| Released npm `n8n-core` engine API drifts from master (v1 was built against master source) | High — bundle may not run | Validate the bundle against npm versions locally before creating the repo; if it fights back, fall back to a GitHub Actions cache of a built monorepo checkout (slower first run, fast after) |
| Vendored `nodes-testing` imports (`../dist/*` → `n8n-core/dist/*`) break if core ships an exports map | Med | Check `n8n-core` package.json `exports` up front; deep-require works today |
| Execute Sub-workflow node's parameters demand a real workflow reference before our mock swaps it | Low | The swap happens before the engine touches the node; parameters go unread by the mock implementation |
## Open Questions
- None blocking. Demo repo name and the npm version pin are picked during
implementation (latest stable n8n release).

View File

@ -1,70 +1,79 @@
# Implementation Plan: `n8n-test` workflow-testing PoC
# Implementation Plan: `n8n-test` PoC — version 2
Spec: `.agents/specs/n8n-test-poc.md`. Spike branch off master (suggested:
`ligo-workflow-testing-poc`); never merged.
Spec: `.agents/specs/n8n-test-poc-v2.md`. Branch `ligo-workflow-testing-v2`
(off the v1 spike); never merged. v1 plan is in this file's git history.
## Overview
Build `packages/n8n-test`, a throwaway monorepo package exposing
`await runWorkflow(workflowJson, input?)`, which executes a workflow through the
real n8n engine with real `nodes-base` node types, HTTP intercepted by nock, and
demo it as a two-test vitest suite run locally.
Phase A adds `mockNode` (multi-node, canned output, input capture) to
`packages/n8n-test`, starring an Execute Sub-workflow mock. Phase B builds the
real CI story: a disposable GitHub repo (James's account) holding an exploded
project package export, whose PRs run the workflow tests in under ~4 minutes
against a prebaked `n8n-test` bundle + released npm n8n packages.
## Architecture Decisions
- **Reuse `packages/core/nodes-testing/` wholesale.** `node-test-harness.ts`
already proves the exact mechanics: `LoadNodesAndCredentials` + `NodeTypes` +
`CredentialsHelper` (stub), `mock<IWorkflowExecuteAdditionalData>()`, seed the
start node on `nodeExecutionStack` with `main: [[{ json: input }]]`, run
`new WorkflowExecute(...).processRunExecutionData(workflow)`, await the
`workflowExecuteAfter` hook. `runWorkflow` is a ~100-line rearrangement of
`executeWorkflow` (node-test-harness.ts:199-264), not new machinery.
- **Import the harness components via a tsconfig path alias**, exactly as
nodes-base does (`"@nodes-testing/*": ["../core/nodes-testing/*"]` in
`packages/nodes-base/tsconfig.json:8`). No changes to `packages/core`.
- **Result contract:** read `result.data.resultData`; on `error` → throw it
(throw-mode); otherwise return
`runData[lastNodeExecuted].at(-1).data.main[0][0].json`.
- **nock, not the SSRF request-helper seam.** Zero engine changes for the PoC;
the helper seam is the documented production direction (spec, Out of Scope).
- **Demo assets fixed for consistency:** the Notion sample workflow POSTs to
requestcatcher while the sample test nocks GET `https://test-endpoint.com/test`;
the packaged `demo/workflow.json` is retargeted so they line up.
- **Mock mechanism: per-run node-type swap.** `mockNode` writes an entry into a
module-level `WeakMap<workflowJson, Map<nodeName, entry>>`. `runWorkflow`
builds, per run, a mock `INodeType` closing over those entries and an
`INodeTypes` wrapper that serves it for the sentinel type; mocked nodes get
their `type` rewritten to the sentinel when the `Workflow` is constructed.
The mock's `execute` records `getInputData()` into the entry (feeding
`handle.input()`) and returns the canned object as one item. JSON never
mutates; last `mockNode` per name wins; `clearNodeMocks()` empties the map.
- **Sub-workflow demo is a separate JSON** (`demo/workflow-with-subworkflow.json`,
trigger → HTTP → Edit Fields → Execute Sub-workflow @1.3) so v1's tests stay
untouched — and the unmockable-for-real node doubles as the honesty test
(without `mockNode` the run must reject).
- **Prebake: esbuild bundle, npm runtime.** `n8n-core` has no `exports` map
(verified: `main` only), so the vendored `core/nodes-testing` sources'
`../dist/*` imports can be rewritten to `n8n-core/dist/*` deep requires by a
small esbuild `onResolve` plugin. Bundle externals: `n8n-core`,
`n8n-workflow`, `n8n-nodes-base`, `nock`, `vitest`, `reflect-metadata`.
Runtime pins: npm `n8n-core@2.16.x`, `n8n-workflow@2.16.x`,
`n8n-nodes-base@2.15.x` (current releases).
- **Validate the prebake before building the repo** (risk-first): npm releases
lag master's engine; if the bundle can't run on them (e.g. a missing
`createRunExecutionData`, changed `establishExecutionContext` shape), first
try shimming the gap inside the bundle; if the engine itself diverges, stop
and ask — that flips the CI strategy (spec's "Ask first" boundary).
- **Demo repo anatomy:** exploded `.n8np` layout (`manifest.json`,
`projects/<project>/workflows/*.json`) authentic to the Ligo export format,
`tests/*.test.ts` importing those JSONs, vanilla vitest + `reflect-metadata`
setup (no DI-container gymnastics outside the monorepo), one `pull_request`
Action with npm caching.
## Task List
### Phase 1: Foundation
- [x] Task 1: Scaffold `packages/n8n-test` (builds; empty vitest suite runs)
### Phase A: mockNode
- [ ] Task 1: `mockNode` + sub-workflow demo (mock, capture, honesty test)
- [ ] Task 2: Multi-node demo test (HTTP + sub-workflow mocked, nock-free)
### Checkpoint: Foundation
- [x] `pnpm --filter n8n-test build` and `pnpm --filter n8n-test demo` both exit 0
### Checkpoint: Phase A
- [ ] `pnpm --filter n8n-test demo` — 6 tests green (3 × v1 untouched + 3 new)
- [ ] Typecheck + lint clean
### Phase 2: Feasibility core (highest risk first)
- [x] Task 2: `runWorkflow` happy path through the real engine; demo happy-day test green
- [x] Task 3: Throw-mode errors + network lockdown; demo unhappy-day test green
### Phase B: CI story (risk first)
- [ ] Task 3: `bundle` command + validation against released npm packages (go/no-go)
- [ ] Task 4: Demo repo — exploded export, tests, Action, release asset, first green run
- [ ] Task 5: The demo PR — red on a broken workflow change, green on the fix
### Checkpoint: Feasibility proven (the go/no-go moment)
- [x] Both demo tests green via real `WorkflowExecute` + real node types
- [x] Un-mocked HTTP fails loudly (`nock.disableNetConnect`)
- [x] Review with James before polish
### Checkpoint: Phase B
- [ ] PR check red → green in under ~4 minutes end to end
### Phase 3: Demo polish
- [x] Task 4: `input` parameter, demo-day tidy-up, gotchas write-up
### Checkpoint: Complete
- [x] Demo dry-run from a clean checkout of the spike branch matches the spec's success criteria
### Phase C: Wrap
- [ ] Task 6: README v2 section, gotchas, verification fan-out, push
## Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| `core/nodes-testing` imports from `packages/core/dist` → needs a built monorepo | Med | Build `n8n-core`/`nodes-base` once up front; document in the package README |
| Full nodes-base load is slow (~30s `beforeAll` in the existing harness) | Med (demo latency) | Accept for PoC; load once per suite; note as a gotcha for the real feature |
| Alias/vitest resolution for `@nodes-testing/*` from a new package | Low | Copy nodes-base's tsconfig-paths setup; fall back to relative imports |
| HTTP Request node's client bypasses nock | Low | The existing harness already nocks this node's traffic; nock 14 covers http/fetch |
| Set node expression needs full expression context | Low | Real engine provides it; proven by harness usage across nodes-base tests |
| npm-released engine lags master APIs the bundle relies on | High | Task 3 validates first; shim small gaps in-bundle; stop-and-ask if the engine diverges (CI strategy flip) |
| Mock node type must satisfy engine plumbing (inputs/outputs, versioning) | Med | Mirror `core/nodes-testing/test-data-node.ts`, the in-repo precedent for an in-process node |
| Handle capture across vitest worker/module boundaries | Low | Registry and handles live in one module instance per test file — same-process by construction |
| Demo repo Action flakes on npm install time | Low | `actions/setup-node` npm cache; pin exact versions |
## Open Questions
- None blocking (spec §Open Questions). Branch name and demo timing are
James's call.
- None blocking. Demo repo name decided at Task 4 (suggestion:
`n8n-workflow-tests-demo`).

View File

@ -1,121 +1,159 @@
# Tasks: `n8n-test` workflow-testing PoC
# Tasks: `n8n-test` PoC — version 2
Spec: `.agents/specs/n8n-test-poc.md` · Plan: `tasks/plan.md`
Spec: `.agents/specs/n8n-test-poc-v2.md` · Plan: `tasks/plan.md` · v1 tasks in git history
## Task 1: Scaffold `packages/n8n-test`
## Task 1: `mockNode` + sub-workflow demo
**Description:** Create the workspace package so it builds and runs an empty
vitest suite. Wires the `@nodes-testing/*` tsconfig path alias (copied from
`packages/nodes-base/tsconfig.json:8`) and workspace deps on `n8n-core`,
`n8n-workflow`, `n8n-nodes-base`, `nock`, `vitest`.
**Description:** Add `mockNode(workflowJson, nodeName, cannedOutput)` returning
a handle with `input()`, plus `clearNodeMocks()`. Implementation per plan: a
`WeakMap` registry, a per-run mock `INodeType` (modelled on
`core/nodes-testing/test-data-node.ts`) served by an `INodeTypes` wrapper, and
node-type rewriting inside `runWorkflow`. Add
`demo/workflow-with-subworkflow.json` (trigger → HTTP → Edit Fields →
Execute Sub-workflow @1.3) and two demo tests: the mocked sub-workflow test
(canned output + `input()` capture) and the honesty test (same workflow
without the mock rejects — sub-workflows cannot run for real in the harness).
**Acceptance criteria:**
- [x] `pnpm --filter n8n-test build` exits 0
- [x] `pnpm --filter n8n-test demo` runs a placeholder test green
- [x] No other package is modified
- [ ] `workflow with subworkflow mocked` green: canned output surfaces in the
result and `sub.input()` captures what Edit Fields produced
- [ ] Honesty test green: un-mocked run of the same workflow rejects
- [ ] v1's three tests untouched and green
**Verification:**
- [x] Build succeeds: `pnpm --filter n8n-test build`
- [x] Tests pass: `pnpm --filter n8n-test demo`
- [ ] Tests pass: `pnpm --filter n8n-test demo`
- [ ] Build succeeds: `pnpm --filter n8n-test build`
**Dependencies:** None
**Files likely touched:**
- `packages/n8n-test/package.json`
- `packages/n8n-test/tsconfig.json`
- `packages/n8n-test/vitest.config.ts`
- `packages/n8n-test/src/mock-node.ts`
- `packages/n8n-test/src/run-workflow.ts`
- `packages/n8n-test/src/index.ts`
- `packages/n8n-test/demo/workflow-with-subworkflow.json`
- `packages/n8n-test/demo/workflow.test.ts` (or a new demo test file)
**Estimated scope:** S
**Estimated scope:** M
## Task 2: `runWorkflow` happy path through the real engine
## Task 2: Multi-node demo test
**Description:** Implement `runWorkflow(workflowJson, input?)` by rearranging
`executeWorkflow` from `packages/core/nodes-testing/node-test-harness.ts:199-264`:
load real node types once (`LoadNodesAndCredentials`/`NodeTypes`), build a
`Workflow`, seed the start node's `nodeExecutionStack` with
`main: [[{ json: input ?? {} }]]`, run `WorkflowExecute.processRunExecutionData`,
await the `workflowExecuteAfter` hook, and return
`runData[lastNodeExecuted].at(-1).data.main[0][0].json`. Add
`demo/workflow.json` (Notion sample retargeted to GET
`https://test-endpoint.com/test`) and the happy-day demo test.
**Description:** Demo test `workflow with every dependency mocked`: HTTP node
and sub-workflow node both mocked on the sub-workflow demo JSON, no nock in
the test; assert the real Edit Fields node's output flowed between the mocks
via `sub.input()`, and re-mocking a name replaces the previous override.
**Acceptance criteria:**
- [x] Happy-day demo test green: nock 200 `{ data: 'Hello world' }` →
`output['test-output'] === 'Hello world'` via the real Set-node expression
- [x] No mocked execution: real `WorkflowExecute`, real `nodes-base` node types
- [ ] Test green with zero nock usage
- [ ] `sub.input()` proves the real middle node ran between two mocks
**Verification:**
- [x] Tests pass: `pnpm --filter n8n-test demo`
- [x] Manual check: temporarily change the nock reply body and watch the
assertion fail (proves the engine, not a canned value, produced output)
- [ ] Tests pass: `pnpm --filter n8n-test demo` (6 green)
- [ ] Manual check: lint + typecheck clean
**Dependencies:** Task 1
**Files likely touched:**
- `packages/n8n-test/src/run-workflow.ts`
- `packages/n8n-test/src/index.ts`
- `packages/n8n-test/demo/workflow.json`
- `packages/n8n-test/demo/workflow.test.ts`
- `packages/n8n-test/demo/workflow.test.ts` (or the new demo test file)
**Estimated scope:** S
## Checkpoint: Phase A — 6 tests green, typecheck + lint clean
## Task 3: `bundle` command + npm-runtime validation (go/no-go)
**Description:** Add `pnpm --filter n8n-test bundle`: an esbuild script
(`scripts/bundle.mjs`) producing a self-contained CJS tarball of `n8n-test` —
vendoring the `@nodes-testing/*` sources with their relative `../dist/*`
imports rewritten to `n8n-core/dist/*` via an `onResolve` plugin; externals:
`n8n-core`, `n8n-workflow`, `n8n-nodes-base`, `nock`, `vitest`,
`reflect-metadata`. Validate in a scratch dir outside the monorepo: npm-install
the tarball + released `n8n-core@2.16.x` / `n8n-workflow@2.16.x` /
`n8n-nodes-base@2.15.x` / `nock` / `vitest`, copy the demo tests, run them.
Shim small API gaps inside the bundle; stop and ask if the released engine
diverges structurally.
**Acceptance criteria:**
- [ ] Tarball builds reproducibly from the package dir
- [ ] All 6 demo tests pass in the scratch dir against released npm packages
**Verification:**
- [ ] Manual check: scratch-dir vitest run output attached to the task notes
**Dependencies:** Task 2 (bundles the final API)
**Files likely touched:**
- `packages/n8n-test/scripts/bundle.mjs`
- `packages/n8n-test/package.json`
**Estimated scope:** M
## Task 3: Throw-mode errors and network lockdown
## Task 4: Demo repo with exploded export, Action, release
**Description:** When the run finishes with `result.data.resultData.error`,
`runWorkflow` rejects with that error (n8n default `onError=stopWorkflow`).
Add a vitest setup file calling `nock.disableNetConnect()` (mirroring
`node-test-harness.ts:58`) so un-mocked HTTP fails loudly. Add the unhappy-day
demo test (500 → `await expect(...).rejects.toThrowError()`).
**Description:** Create a disposable repo under James's GitHub (suggestion:
`n8n-workflow-tests-demo`): exploded `.n8np` layout (`manifest.json`,
`projects/<project>/workflows/*.json` — the two demo workflows, authentic to
the Ligo export format), `tests/*.test.ts` importing those JSONs,
vanilla vitest config + `reflect-metadata`/nock setup, README, and a
`pull_request` GitHub Action (node 22, npm cache) running vitest. Attach the
Task 3 tarball as a GitHub release asset; `package.json` installs it by URL.
First push must show a green run on the default branch.
**Acceptance criteria:**
- [x] Unhappy-day demo test green: nock 500 → `runWorkflow` rejects with the
HTTP Request node's error
- [x] Removing a nock intercept makes the test fail with a blocked-connection
error, not a real network call
- [ ] Fresh clone + `npm ci` + `npx vitest run` passes locally
- [ ] Action green on the default branch, wall-clock under ~4 min
**Verification:**
- [x] Tests pass: `pnpm --filter n8n-test demo` (both tests)
- [x] Manual check: comment out a nock intercept → loud failure
**Dependencies:** Task 2
**Files likely touched:**
- `packages/n8n-test/src/run-workflow.ts`
- `packages/n8n-test/demo/workflow.test.ts`
- `packages/n8n-test/vitest.config.ts` (setup file)
**Estimated scope:** S
## Checkpoint: Feasibility proven — review with James before Task 4
- [x] Both demo tests green through the real engine
- [x] Un-mocked network blocked
- [x] Go/no-go on polish
## Task 4: `input` parameter, demo tidy-up, gotchas write-up
**Description:** Prove the `input` seeding end-to-end (a demo assertion that a
field from `input` flows through), make the demo test file read like the Notion
"Sample test" (it is the artifact on screen), add a short package README with
the demo commands, and write the gotchas list (node-load time, dist-build
requirement, error-mode semantics, SSRF-helper seam as production direction)
into the spike branch/PR description.
**Acceptance criteria:**
- [x] `runWorkflow(workflowJson, { some: 'field' })` demonstrably influences output
- [x] README documents: build prerequisite, `pnpm --filter n8n-test demo`
- [x] Gotchas list exists (branch/PR description)
**Verification:**
- [x] Tests pass: `pnpm --filter n8n-test demo`
- [x] Manual check: demo dry-run from clean checkout following only the README
- [ ] Manual check: Actions run URL green
**Dependencies:** Task 3
**Files likely touched:**
- `packages/n8n-test/demo/workflow.test.ts`
- `packages/n8n-test/demo/workflow.json`
- `packages/n8n-test/README.md`
**Files likely touched:** (new repo, not the monorepo)
**Estimated scope:** M
## Task 5: The demo PR — red → green
**Description:** In the demo repo, raise a PR that breaks a workflow (change
the Edit Fields mapping in the exploded workflow JSON) — the check must fail
with a readable assertion diff. Push a fix commit — the check must go green.
Leave the PR open as the demo artifact.
**Acceptance criteria:**
- [ ] Failing check on the breaking commit, readable diff in the log
- [ ] Green check on the fix commit, each run under ~4 min
**Verification:**
- [ ] Manual check: PR URL with both check runs
**Dependencies:** Task 4
**Files likely touched:** (demo repo)
**Estimated scope:** S
## Checkpoint: Phase B — PR shows red → green in under ~4 minutes
## Task 6: README v2, gotchas, verification fan-out, push
**Description:** README v2 section (`mockNode` usage, bundle command, demo-repo
wiring + links), extend the gotchas with what v2 surfaced (npm-runtime deltas,
mock-node plumbing), run the adversarial verification fan-out over the v2
diff, apply must-fixes, push, and update the draft-PR description with a v2
summary.
**Acceptance criteria:**
- [ ] README covers phase A API and phase B wiring with real links
- [ ] Verification fan-out findings triaged; must-fixes applied
- [ ] Branch pushed; draft PR body updated
**Verification:**
- [ ] Tests pass: `pnpm --filter n8n-test demo`
- [ ] Manual check: draft PR renders the v2 summary
**Dependencies:** Task 5
**Files likely touched:**
- `packages/n8n-test/README.md`
- PR #36120 body
**Estimated scope:** S