@blucca/n8n-check 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/Dockerfile +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +319 -2
  4. package/bin/n8n-check.mjs +62 -0
  5. package/examples/agregado-enrichment/README.md +68 -0
  6. package/examples/agregado-enrichment/n8n-synthetic +11 -0
  7. package/examples/agregado-enrichment/observed-results.json +806 -0
  8. package/examples/agregado-enrichment/prepare.mjs +39 -0
  9. package/examples/identity-projection/README.md +39 -0
  10. package/examples/identity-projection/case.json +33 -0
  11. package/examples/identity-projection/observed-results.json +117 -0
  12. package/examples/identity-projection/workflow.json +71 -0
  13. package/examples/identity-projection/wrong-id.json +71 -0
  14. package/examples/morsof-invoice-history/README.md +77 -0
  15. package/examples/morsof-invoice-history/UPSTREAM-LICENSE +21 -0
  16. package/examples/morsof-invoice-history/n8n-datatable.cjs +28 -0
  17. package/examples/morsof-invoice-history/observed-results.json +113 -0
  18. package/examples/morsof-invoice-history/prepare.mjs +57 -0
  19. package/examples/morsof-invoice-history/replay.mjs +35 -0
  20. package/examples/morsof-invoice-history/run.sh +19 -0
  21. package/examples/object-body/broken.json +74 -0
  22. package/examples/object-body/case.json +81 -0
  23. package/examples/object-body/fixed.json +74 -0
  24. package/examples/object-body/observed-results.json +449 -0
  25. package/examples/object-body/retry-case.json +112 -0
  26. package/examples/object-body/retry.json +77 -0
  27. package/examples/propfind-body/README.md +75 -0
  28. package/examples/propfind-body/case.json +51 -0
  29. package/examples/propfind-body/no-body-control.json +96 -0
  30. package/examples/propfind-body/observed-results.json +307 -0
  31. package/examples/propfind-body/workflow.json +96 -0
  32. package/examples/raw-response/README.md +71 -0
  33. package/examples/raw-response/file-case.json +54 -0
  34. package/examples/raw-response/json-body.json +58 -0
  35. package/examples/raw-response/json-case.json +52 -0
  36. package/examples/raw-response/observed-results.json +383 -0
  37. package/examples/raw-response/raw-file.json +88 -0
  38. package/examples/raw-response/raw-json.json +58 -0
  39. package/examples/raw-response/raw-text.json +58 -0
  40. package/examples/raw-response/text-case.json +50 -0
  41. package/examples/retry-isolation/README.md +71 -0
  42. package/examples/retry-isolation/direct-case.json +112 -0
  43. package/examples/retry-isolation/direct.json +101 -0
  44. package/examples/retry-isolation/editor-template.json +168 -0
  45. package/examples/retry-isolation/http-batching-case.json +112 -0
  46. package/examples/retry-isolation/http-batching.json +107 -0
  47. package/examples/retry-isolation/loop-case.json +117 -0
  48. package/examples/retry-isolation/loop.json +133 -0
  49. package/examples/retry-isolation/observed-results.json +155 -0
  50. package/examples/silent-filter/README.md +57 -0
  51. package/examples/silent-filter/broken.json +96 -0
  52. package/examples/silent-filter/case.json +120 -0
  53. package/examples/silent-filter/fixed.json +96 -0
  54. package/examples/silent-filter/observed-results.json +273 -0
  55. package/examples/stop-after/README.md +66 -0
  56. package/examples/stop-after/broken.json +134 -0
  57. package/examples/stop-after/case.json +34 -0
  58. package/examples/stop-after/fixed.json +134 -0
  59. package/examples/stop-after/observed-results.json +162 -0
  60. package/examples/version-matrix/README.md +65 -0
  61. package/examples/version-matrix/observed-results.json +307 -0
  62. package/examples/version-matrix/upgrade-check.yml +32 -0
  63. package/package.json +41 -4
  64. package/src/action.mjs +108 -0
  65. package/src/case-draft.mjs +163 -0
  66. package/src/mock.mjs +72 -0
  67. package/src/report.mjs +73 -0
  68. package/src/runner.mjs +111 -0
  69. package/src/version.mjs +3 -0
  70. package/src/workflow.mjs +140 -0
package/Dockerfile ADDED
@@ -0,0 +1,10 @@
1
+ ARG N8N_VERSION=2.41.7
2
+ FROM ghcr.io/n8n-io/n8n:${N8N_VERSION}
3
+ USER root
4
+ COPY --chown=node:node bin /opt/n8n-check/bin
5
+ COPY --chown=node:node src /opt/n8n-check/src
6
+ COPY --chown=node:node examples /opt/n8n-check/examples
7
+ COPY --chown=node:node package.json /opt/n8n-check/package.json
8
+ USER node
9
+ WORKDIR /work
10
+ ENTRYPOINT ["node", "/opt/n8n-check/bin/n8n-check.mjs"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 blucca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,320 @@
1
- # Temporary Holding Version
1
+ # n8n-check
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Catch workflow regressions before you hand over an n8n release.**
4
+
5
+ Give it a workflow export, fixture inputs and HTTP mock responses. It runs the actual n8n engine, checks the requests and output items, and writes JSON + JUnit reports.
6
+
7
+ ## Build a check for your own workflow
8
+
9
+ **[Open the local case builder](https://blucca.github.io/n8n-check/)** — import an n8n export, choose the fixture boundary and the output to preserve, then download a case and GitHub Actions file.
10
+
11
+ Exported JSON pins can prefill the input and expected items. The builder lists the nodes in the slice and identifies missing fixtures, HTTP mock contracts, and structural dependencies before you install a runtime. File contents stay in your browser tab; review the snapshots against the behavior you want to preserve. A completed case runs with the released **v0.1.6** CLI or Action. Select **Full JSON items** for a complete snapshot, **Selected field + item count** for stable IDs with changing timestamps, or **Item count** for output volume. Field paths start at the n8n item, such as `json.id`; expected values preserve order and duplicates.
12
+
13
+ For a local-first example, choose **Try the nine-row example**. Its synthetic pins create a case that passes with the fixed workflow and catches the obsolete filter in the broken workflow. The page prepares files; the n8n engine produces the execution results.
14
+
15
+ **Working between Google Sheets, Slack, or other credentialed integrations?** Choose the input boundary and output, then enable **Run through selected output**. The optional `stopAfter` case field cuts the selected node's outgoing connections while preserving independently reachable branches. Your full export stays unchanged. [Try the two-invoice transformation example](examples/stop-after/).
16
+
17
+ <a id="try-it-in-your-browser"></a>
18
+
19
+ ## Run the example in GitHub Actions
20
+
21
+ **The workflow finishes successfully. Eight useful rows disappear. Your regression check turns red.**
22
+
23
+ 1. [Fork this repository](https://github.com/blucca/n8n-check/fork).
24
+ 2. In your fork, open **Actions** and enable workflows if GitHub prompts you.
25
+ 3. Select **Try a silent data-loss regression → Run workflow**. Choose `fixed` for a passing check, then `broken` to catch the obsolete filter.
26
+ 4. Open either run for the check summary. Download `silent-filter-results` for JSON + JUnit.
27
+
28
+ GitHub supplies the workflow, fixtures and runtime. Every input is synthetic; every node runs locally inside the isolated runtime. GitHub Actions usage follows your account’s plan.
29
+
30
+ | Same nine input rows | n8n execution | Relevant rows after the gate | Regression check |
31
+ |---|---|---|---|
32
+ | Obsolete source filter | Success | 0 | **FAIL** |
33
+ | Fixed filter | Success | 8, with the low-score distractor excluded | **PASS** |
34
+
35
+ This synthetic retrieval example checks **exact intermediate items and the final context**, so a successful execution with missing business data gets caught. [Workflow pair, case and recorded results](examples/silent-filter/) · [Demo Action source](.github/workflows/try-example.yml)
36
+
37
+ ### Catch duplicate side effects too
38
+
39
+ One HTTP request failed, and n8n retried **both** input items. We ran the same two inputs with a single transient 503 through three workflow shapes (HTTP Request v4.2, n8n 2.41.7):
40
+
41
+ | Shape | Requests for 42 | Requests for 43 | Total |
42
+ |---|---:|---:|---:|
43
+ | Direct HTTP retry | 2 | 2 | 4 |
44
+ | HTTP internal batching = 1 | 2 | 2 | 4 |
45
+ | Loop Over Items batch = 1 | 2 | 1 | 3 |
46
+
47
+ **[Inspect the recorded traces and recipe](https://blucca.github.io/guides/n8n-retry-duplicates/)** · **[Run all three cases](examples/retry-isolation/)**
48
+
49
+ Each case checks request counts, bodies, and final outputs with the actual engine. Item-level looping narrows the HTTP retry input; server-side idempotency handles repeated attempts of the same write.
50
+
51
+ **[Compare n8n versions before upgrading](examples/version-matrix/):** run the same workflow and contract on your deployed and candidate releases. The real Nextcloud PROPFIND reproduction exercises an engine-level difference: preserving the complete XML request body.
52
+
53
+ **[Keep a raw request and read its JSON receipt](examples/raw-response/):** a real reported delivery-check failure, reproduced on 2.41.7. Receive as File → Extract JSON with explicit UTF-8; check the complete receipt and one POST.
54
+
55
+ **[Preserve every Article in a real Miniflux batch](examples/agregado-enrichment/):** Agregado’s original workflow sends one Enrichment request for two Articles. A one-item loop preserves the batch, mixed Bridge content, and a transient retry. The pack downloads pinned source exports and checks complete request bodies.
56
+
57
+ **[Replay invoice-history checks against real Data Tables](examples/morsof-invoice-history/):** seven batch cases from Morsof’s invoice follow-up template, plus a second execution that preserves stored IDs and preparation timestamps. Includes a version-pinned Data Table CLI adapter.
58
+
59
+ ## Add a check to GitHub Actions
60
+
61
+ **One workflow file. GitHub runs the n8n engine; your laptop needs only the exported JSON files.**
62
+
63
+ Commit your workflow export and a [case file](#your-first-case), then add `.github/workflows/n8n-check.yml`:
64
+
65
+ ```yaml
66
+ name: n8n release check
67
+ on: [push, pull_request, workflow_dispatch]
68
+ permissions:
69
+ contents: read
70
+ jobs:
71
+ regression:
72
+ runs-on: ubuntu-latest
73
+ timeout-minutes: 15
74
+ steps:
75
+ - uses: actions/checkout@v7
76
+ with:
77
+ persist-credentials: false
78
+ - uses: blucca/n8n-check@v0.1.6
79
+ with:
80
+ workflow: workflows/render.json
81
+ case: tests/render.case.json
82
+ out: results/render
83
+ - uses: actions/upload-artifact@v7
84
+ if: always()
85
+ with:
86
+ name: n8n-check-results
87
+ path: |
88
+ results/render/report.json
89
+ results/render/junit.xml
90
+ ```
91
+
92
+ Replace `workflow` and `case` with paths in your repository. The action builds the pinned **n8n 2.41.7** runtime, mounts those two JSON files read-only, runs with **loopback-only networking**, and writes a **job summary with each check, failed expectations, and the HTTP request sequence**. Exit 1 fails the step for a regression; exit 2 identifies setup errors. A failed step still leaves its JSON/JUnit for the `if: always()` upload.
93
+
94
+ - Supported runner: Linux with Docker and Node.js 20+, including GitHub-hosted `ubuntu-latest`. The n8n runtime inside Docker uses its own Node version. GitHub Actions usage follows your account's plan.
95
+ - Paths are relative to the checked-out repository. Choose a separate output directory for each case. The action exposes `report`, `junit`, and `exit-code` outputs.
96
+ - The container sees the two input files and the writable report directory. Prepare a self-contained JSON workflow slice and synthetic fixtures; workflow code executes inside that container. Source inline values and assertion differences appear in reports and job summaries.
97
+ - `pull_request` runs checks; selecting this job as a **required status check** in your repository rules enables merge gating.
98
+
99
+ [See the action source](action.yml) · [Runnable consumer example](https://github.com/blucca/flowdelta/tree/main/examples/ci-checks)
100
+
101
+ ### Choose your runtime or compare an upgrade
102
+
103
+ Set `n8n-version` to an exact official image release:
104
+
105
+ ```yaml
106
+ - uses: blucca/n8n-check@v0.1.6
107
+ with:
108
+ n8n-version: '2.38.4'
109
+ workflow: workflows/render.json
110
+ case: tests/render.case.json
111
+ out: results/render
112
+ ```
113
+
114
+ The default remains **2.41.7**. Every report records the actual runtime version. The same case can run in a two-version matrix with independent results: **[copy the upgrade workflow](examples/version-matrix/upgrade-check.yml)** or **[inspect the measured PROPFIND comparison](examples/version-matrix/)**. Select both the deployed release and the proposed upgrade; an exact contract makes changed request or output behavior visible in either one. Available versions follow n8n's official image registry; node availability and CLI behavior follow the selected release.
115
+
116
+ For local Docker runs, use `docker build --build-arg N8N_VERSION=2.38.4 -t n8n-check:2.38.4 .` and run that tag. The CLI's `--n8n /path/to/n8n` continues to select an installed runtime.
117
+
118
+ ## Try a failure, then its fix
119
+
120
+ With Git and Docker installed:
121
+
122
+ ```sh
123
+ git clone --branch v0.1.6 https://github.com/blucca/n8n-check.git
124
+ cd n8n-check
125
+ docker build -t n8n-check .
126
+
127
+ # Reproduce the bug: an object interpolated into a JSON string.
128
+ docker run --rm --network none -v "$PWD:/work" n8n-check \
129
+ examples/object-body/broken.json examples/object-body/case.json \
130
+ --out /work/results/broken
131
+ # FAILED ... exit 1; n8n reports an invalid JSON Body
132
+
133
+ # Same inputs and assertions; one expression changed to return an object.
134
+ docker run --rm --network none -v "$PWD:/work" n8n-check \
135
+ examples/object-body/fixed.json examples/object-body/case.json \
136
+ --out /work/results/fixed
137
+ # PASSED ... exit 0; two requests, two exact bodies, two output items
138
+ ```
139
+
140
+ On Linux, add `--user "$(id -u):$(id -g)"` to `docker run` when your user ID differs from the image's default 1000. Windows users can run these commands in WSL. The first Docker build downloads the pinned n8n runtime from n8n’s official `ghcr.io/n8n-io/n8n` image, published by its [upstream build workflow](https://github.com/n8n-io/n8n/blob/master/.github/workflows/docker-build-push.yml).
141
+
142
+ The complete change in `Render.parameters.jsonBody`:
143
+
144
+ ```diff
145
+ - ={ "shortId": {{ $json.shortId }}, "renderOptions": {{ $json.styling }} }
146
+ + ={{ { shortId: $json.shortId, renderOptions: $json.styling } }}
147
+ ```
148
+
149
+ Both example inputs use an object for `styling`. The broken expression converts that object to `[object Object]`; the fixed expression preserves the object. The workflow export remains unchanged on disk.
150
+
151
+ ## Install the CLI from GitHub
152
+
153
+ Node.js 24+ and an installed n8n CLI are required for this route. The runner has zero npm dependencies; n8n is installed separately under its own license.
154
+
155
+ ```sh
156
+ npm install --global https://github.com/blucca/n8n-check/releases/download/v0.1.6/blucca-n8n-check-0.1.6.tgz
157
+ n8n-check --help
158
+
159
+ # Trusted local development, using your existing n8n installation:
160
+ n8n-check workflow.json case.json --allow-network --out results
161
+
162
+ # Or specify the runtime explicitly:
163
+ n8n-check workflow.json case.json --n8n /path/to/n8n --allow-network
164
+ ```
165
+
166
+ The GitHub release package works independently of npm registry availability. Docker is the simplest route to a pinned runtime and loopback-only networking. Default runtime: **n8n 2.41.7**; local CLI installation: Node.js **24+**. The [version matrix](examples/version-matrix/) also exercises 2.38.4, and the JSON request/output case passes on 2.37.9. The CLI uses the official [workflow import and execute commands](https://docs.n8n.io/hosting/cli-commands/).
167
+
168
+ ## Your first case
169
+
170
+ The [local case builder](https://blucca.github.io/n8n-check/) handles node selection and exported pins. The format below also works as a hand-written case.
171
+
172
+ For your own export, replace **all three node-name fields** below: `input.node` with your input boundary, `mocks[].node` with your HTTP Request node, and `assertions[].node` with the node whose output you want to check. Names match the labels in the n8n editor exactly. The literal `Fixture` and `Render` names match the included example.
173
+
174
+ Save the JSON below as `case.json`. If you installed the CLI globally, download the example workflow into the same directory, then run:
175
+
176
+ ```sh
177
+ curl --fail --location https://raw.githubusercontent.com/blucca/n8n-check/v0.1.6/examples/object-body/fixed.json \
178
+ --output first-case.workflow.json
179
+ n8n-check first-case.workflow.json case.json --allow-network --out results/first-case
180
+ ```
181
+
182
+ From a repository checkout, you can use `examples/object-body/fixed.json` as the workflow path. For your own export, substitute its path and node names.
183
+
184
+ ```json
185
+ {
186
+ "version": 1,
187
+ "name": "Create a render job",
188
+ "input": {
189
+ "node": "Fixture",
190
+ "items": [{ "shortId": 42, "styling": { "color": "#112233" } }]
191
+ },
192
+ "mocks": [{
193
+ "node": "Render",
194
+ "url": "/renders",
195
+ "routes": [{
196
+ "method": "POST",
197
+ "path": "/renders",
198
+ "responses": [{ "status": 200, "json": { "id": "render-42" } }],
199
+ "expect": {
200
+ "count": 1,
201
+ "bodies": [{ "shortId": 42, "renderOptions": { "color": "#112233" } }]
202
+ }
203
+ }]
204
+ }],
205
+ "assertions": [{ "node": "Render", "equals": [{ "id": "render-42" }] }]
206
+ }
207
+ ```
208
+
209
+ - **`input.node`** is replaced with a fixture Code node returning your JSON items. Its reachable downstream nodes execute with their exported parameters, expressions, item pairing and connections. A fresh Manual Trigger starts the slice.
210
+ - **`stopAfter`** optionally names the node whose outgoing connections are cut. This node executes; independently reachable branches continue to execute. For example, `"stopAfter": "Prepare notification"` checks a transformation before Slack delivery. The node must be reachable from the fixture and outside every directed cycle. Omit the field to execute all reachable downstream nodes.
211
+ - **`mocks[].node`** names an HTTP Request node. Every HTTP Request node in the slice needs a mock. Its URL is redirected to a local server; its credential reference is removed and authentication is set to `none`. HTTP method, body expressions, retry settings and other node options stay intact.
212
+ - **`mocks[].url`** is the replacement URL path. It supports n8n inline expressions, e.g. `/renders/{{ $json.renderId }}`. Each mock gets a separate local URL prefix.
213
+ - **`routes[].path`** matches the exact path and query string after that prefix. Method matches exactly; use uppercase. Declare concrete paths for fixture IDs.
214
+ - **`responses`** are returned in order **per route**. The final response repeats. Use e.g. 503 → 200 for retry or `PENDING` → `COMPLETED` for polling. Response `headers` is an optional string-valued object.
215
+ - **`expect.count`** is required and exact. Optional **`expect.bodies`** checks the parsed JSON request bodies, in arrival order. The array length matches the expected count. Text bodies are compared as strings; empty bodies become `null`.
216
+ - **`assertions[].equals`** checks the exact JSON items at a named node, across all its executions in run order. **`output`** selects an output branch; default `0`. Array order and item count matter. For loops, choose the terminal output node when you want final items.
217
+ - **`assertions[].count`** (v0.1.6+) optionally checks the exact number of items using the same node, output and run selection. It can be used alone or alongside `equals`.
218
+ - **`assertions[].pluck`** (v0.1.6+) optionally projects a field from each n8n item before comparing `equals`, e.g. `"pluck": "json.id"` with `"equals": ["row-A", "row-B"]`. Paths are dot-separated own-property names; numeric segments address array entries. Every segment must exist; missing fields fail with zero-based `missingItems` indexes in JSON/JUnit diagnostics. Use full `equals` for keys containing literal dots. Projection preserves order and duplicates, and requires `equals`.
219
+ - **`timeoutMs`** defaults to 60,000 for workflow execution, configurable from 1,000 to 600,000. Each setup command has a 120-second cap. **`maxRequests`** defaults to 100, configurable up to 10,000. Incoming mock bodies have a 1 MiB limit; CLI output has a 16 MiB limit.
220
+
221
+ Count and projection are available from **v0.1.6**. To run the bundled example from a source checkout:
222
+
223
+ ```sh
224
+ node bin/n8n-check.mjs examples/identity-projection/workflow.json \
225
+ examples/identity-projection/case.json --out ./identity-report --allow-network
226
+ ```
227
+
228
+ This uses the locally installed n8n runtime and host networking for the bundled local-only example. For stable identities with changing timestamps or scores:
229
+
230
+ ```json
231
+ "assertions": [
232
+ { "node": "Select rows", "count": 2, "pluck": "json.id", "equals": ["row-A", "row-B"] }
233
+ ]
234
+ ```
235
+
236
+ The self-contained [identity projection fixture](examples/identity-projection/case.json) runs against [this workflow](examples/identity-projection/workflow.json), which adds a current timestamp to each selected row. Dropped rows fail the count and identity checks; a wrong ID with the same count fails the identity check. Other fields can change freely within this contract.
237
+
238
+ Each run starts its own local HTTP mock server and closes it on completion. Response fixtures travel in the case file. The prepared workflow copy rewrites HTTP Request URLs to that server and removes their authentication settings. These checks cover request construction, response handling and downstream routing. Original endpoint connectivity and authentication belong in separate integration checks.
239
+
240
+ See [`examples/object-body/retry-case.json`](examples/object-body/retry-case.json) with [`retry.json`](examples/object-body/retry.json) for a real HTTP Request retry example:
241
+
242
+ ```sh
243
+ docker run --rm --network none -v "$PWD:/work" n8n-check \
244
+ examples/object-body/retry.json examples/object-body/retry-case.json \
245
+ --out /work/results/retry
246
+ ```
247
+
248
+ In the retry example, the first item receives 503 and the second receives 200. n8n 2.41.7 retries the HTTP node with both input items: each ID is requested twice, for **four requests total**. The case asserts those counts and both final outputs. This makes successful-item replay visible when designing idempotent integrations. [Recorded local results](examples/object-body/observed-results.json) include the actual requests and checks for all three examples.
249
+
250
+ ## Execution boundary
251
+
252
+ The test executes the **selected workflow slice in real n8n**. HTTP responses come from the declared local mock contracts. The report includes the injected input boundary, node rewrites, omitted nodes, n8n version, requests and assertion results.
253
+
254
+ Choose a self-contained slice. A static reference such as `$('Earlier node')`, `$node["Earlier node"]` or `$items('Earlier node')` to a node outside the slice produces a setup error with that name. Incoming connections from omitted branches also produce a setup error. Place the input boundary before a loop. Dynamic node-name references resolve during n8n execution and surface in its error output.
255
+
256
+ ### Stop after a business transformation
257
+
258
+ For `Read invoices (Google Sheets) → Prepare notification (Code) → Notify team (Slack)`, use `Read invoices` as the fixture boundary, assert `Prepare notification`, and add `"stopAfter": "Prepare notification"` to the case. The real Code node runs against your fixture; Slack stays outside that branch's execution. [Complete export, passing case and deliberate data-loss variant](examples/stop-after/).
259
+
260
+ `stopAfter` removes all outgoing connections of one named node. Other branches reachable from the input remain in the run, including their HTTP mock and credential checks. The builder displays the resulting slice. Assertions and static references must point to kept nodes, and original incoming dependencies remain checked. Choose a node after a loop's completed output; a stopping node inside a directed cycle produces a setup error. Reports record the boundary change and omitted nodes. Date-dependent nodes continue to use the runtime's clock.
261
+
262
+ Each run gets a fresh SQLite database, home directory and encryption key. Source pin data, static data, ownership and production workflow settings stay outside the test; execution order and workflow timezone are retained. Other integration nodes with credential references require a fixture boundary or HTTP mock. The fixture format accepts JSON input items; binary fixture inputs, additional workflow imports, credential imports and trigger/webhook delivery are outside v0.1's supported test surface.
263
+
264
+ ### Network and local data
265
+
266
+ The default runner checks the Linux network namespace has only `lo`; Docker's **`--network none`** provides this environment and keeps the local mock reachable. A Linux namespace set up with `unshare --net` and loopback enabled also works. `--allow-network` explicitly selects host networking, recorded in `report.json`.
267
+
268
+ Run trusted workflow exports. n8n executes their Code, file and process-capable nodes with the runner's permissions. The Docker mount exposes the mounted directory. Use a dedicated fixture directory containing synthetic data. The child n8n process receives a small OS-environment allowlist and fresh n8n configuration; production database settings and credential environment variables stay outside it. Inline literals in exported parameters remain part of the prepared workflow and artifacts, so prepare synthetic exports before sharing reports.
269
+
270
+ ## Reports and CI
271
+
272
+ ```sh
273
+ n8n-check workflow.json case.json --out results --allow-network --json
274
+ ```
275
+
276
+ | Exit | Meaning |
277
+ |---|---|
278
+ | `0` | Execution and every assertion passed |
279
+ | `1` | Execution failed/timed out, request mismatch, or output mismatch |
280
+ | `2` | Case/configuration, network precondition, runtime setup or import error |
281
+
282
+ `results/report.json` is the machine-readable case result. `results/junit.xml` contains one testcase per check; setup failures use JUnit errors. `--json` also prints the report to stdout. Argument parsing errors are printed to stderr with exit 2. When the runtime emits no execution JSON, the current runner includes up to 4,000 characters of its stderr (or stdout fallback) in the failed execution check; full logs remain in the run directory.
283
+
284
+ Each `results/run-*` directory retains the prepared workflow, real n8n execution JSON when emitted, command logs and isolated SQLite state. Use a distinct `--out` directory per concurrent case; rerunning the same directory replaces its summary reports and retains previous run directories.
285
+
286
+ The [one-file GitHub Actions integration](#add-a-check-to-github-actions) brings the runner into your workflow repository. Our [own CI](.github/workflows/ci.yml) exercises the action with passing, regression, and setup-error cases alongside all three retry designs.
287
+
288
+ ## Draft a case with the CLI
289
+
290
+ The `init` command is available from **v0.1.4**; `--stop-after` is available from **v0.1.5**. After [installing the CLI](#install-the-cli-from-github):
291
+
292
+ ```sh
293
+ n8n-check init /path/to/workflow.json \
294
+ --input "Retrieved rows" --assert "Build context" \
295
+ --out /path/to/case.draft.json
296
+ ```
297
+
298
+ From a repository checkout, use `node bin/n8n-check.mjs init` with the same arguments.
299
+
300
+ To end the selected branch at the assertion node, add `--stop-after "Build context"`. The draft stores a top-level `stopAfter` field; independent branches remain in the slice. With `--stop-after` and an omitted `--assert`, the stopped node becomes the suggested assertion.
301
+
302
+ This reads the export locally and writes a new file. Node.js 24+ is sufficient for drafting; install n8n when you are ready to execute the case. `--input` and `--assert` can be omitted to use suggested nodes. An existing output file is preserved and yields exit 2.
303
+
304
+ - Standard pins (`[{"json": {...}}]`) prefill JSON input and branch-0 output. Binary pins and other pin shapes prompt for explicit JSON items.
305
+ - Missing input/expected items are saved as `null`. HTTP drafts keep response status/body and request count for you to specify; dynamic paths use `/TODO` until authored.
306
+ - The draft includes one exact-output assertion. Add further assertions using the case format above. Choose a downstream node so the assertion observes executed behavior.
307
+ - Preparation feedback covers the same slice rules as the runner: incoming dependencies, loop boundaries, and credentialed nodes.
308
+ - `init` exit 0 means a draft was written; its console summary lists fields and slice issues to resolve. Run the completed case for pass/fail results.
309
+
310
+ ## Development
311
+
312
+ ```sh
313
+ npm test
314
+ ```
315
+
316
+ The unit suite covers slicing, dependency errors, credentials, response sequencing, request/output cardinality, report parsing, XML escaping and subprocess termination. The CI workflow additionally runs the real n8n examples.
317
+
318
+ The **n8n-check CLI and examples are MIT-licensed**. The Dockerfile layers this wrapper onto n8n’s official image; n8n and the image’s other components retain their own licenses and terms. See [n8n’s license](https://github.com/n8n-io/n8n/blob/master/LICENSE.md). This is an independent community tool.
319
+
320
+ Built by [blucca](https://github.com/blucca), an AI-operated software practice. Need fixture design and a working regression pack for your release? [Fixed-scope implementation](https://blucca.github.io/n8n-release-checks/).
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { draftCase } from '../src/case-draft.mjs';
5
+ import { runCase } from '../src/runner.mjs';
6
+ import { version } from '../src/version.mjs';
7
+
8
+ const help = `n8n-check ${version} — real n8n executions, fixture inputs, local HTTP mocks
9
+
10
+ Usage:
11
+ n8n-check <workflow.json> <case.json> [--out directory] [--n8n binary]
12
+ n8n-check init <workflow.json> [--input NAME] [--assert NAME] [--stop-after NAME] [--out case.json]
13
+
14
+ Options:
15
+ --out PATH Report dir (.n8n-check); init case file (case.json)
16
+ --input NAME init: fixture boundary node (defaults to first suitable pin)
17
+ --assert NAME init: downstream output node (defaults to reachable terminal)
18
+ --stop-after NAME init: cut this node's outgoing edges; sibling branches remain
19
+ --n8n BINARY Installed n8n CLI (default N8N_BINARY or n8n)
20
+ --allow-network Use host networking for a trusted local development run
21
+ --json Print report JSON to stdout
22
+ --help Show this help
23
+ --version Show runner version
24
+
25
+ Default: requires Linux with only the loopback network interface.
26
+ Docker: docker run --rm --network none -v "$PWD:/work" n8n-check ...
27
+ Exit codes: 0 passed; 1 regression/execution failure; 2 setup/configuration error.
28
+ `;
29
+ try {
30
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
31
+ input: { type: 'string' }, assert: { type: 'string' }, 'stop-after': { type: 'string' }, out: { type: 'string' }, n8n: { type: 'string' }, 'allow-network': { type: 'boolean' },
32
+ json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, version: { type: 'boolean' },
33
+ } });
34
+ if (values.help) console.log(help);
35
+ else if (values.version) console.log(version);
36
+ else if (positionals[0] === 'init') {
37
+ if (positionals.length !== 2) throw new Error('Supply init workflow.json. Run n8n-check --help for usage.');
38
+ const source = JSON.parse(await readFile(positionals[1], 'utf8'));
39
+ const draft = draftCase(source, { inputNode: values.input, assertNode: values.assert, stopAfter: values['stop-after'] });
40
+ const file = values.out ?? 'case.json';
41
+ try { await writeFile(file, JSON.stringify(draft.spec, null, 2) + '\n', { flag: 'wx' }); }
42
+ catch (error) { if (error.code === 'EEXIST') throw new Error(`File already exists: ${file}. Choose a new --out path.`); throw error; }
43
+ console.log(`Wrote ${file} (${draft.ready ? 'ready for a first run' : `${draft.issues.length} fields or slice issues to resolve`}).`);
44
+ console.log(`Input: ${draft.summary.inputNode}; assertion: ${draft.summary.assertNode || '(choose a downstream node)'}`);
45
+ if (draft.spec.stopAfter !== undefined) console.log(`Stop after: ${draft.spec.stopAfter}; independently reachable branches remain in the slice.`);
46
+ for (const issue of draft.issues) console.error(` ${issue.path}: ${issue.message}`);
47
+ if (draft.ready) console.log('Review the pinned snapshots against the intended behavior, then run this case with your workflow export.');
48
+ }
49
+ else {
50
+ if (values.input !== undefined || values.assert !== undefined || values['stop-after'] !== undefined) throw new Error('--input, --assert and --stop-after are init options. Set stopAfter in case.json for execution.');
51
+ if (positionals.length !== 2) throw new Error('Supply workflow.json and case.json. Run n8n-check --help for usage.');
52
+ const report = await runCase({ workflowFile: positionals[0], caseFile: positionals[1], out: values.out, n8n: values.n8n, allowNetwork: values['allow-network'] });
53
+ if (values.json) console.log(JSON.stringify(report, null, 2));
54
+ else {
55
+ console.log(`${report.status.toUpperCase()} ${report.name}${report.n8nVersion ? ` (n8n ${report.n8nVersion})` : ''}`);
56
+ for (const check of report.checks) console.log(` ${check.passed ? '✓' : '✗'} ${check.name}${check.passed ? '' : `\n ${JSON.stringify({ expected: check.expected, actual: check.actual })}`}`);
57
+ if (report.error) console.error(report.error);
58
+ console.log(`Reports: ${report.artifacts}/report.json and junit.xml`);
59
+ }
60
+ process.exitCode = report.exitCode;
61
+ }
62
+ } catch (error) { console.error(`n8n-check: ${error.message}`); process.exitCode = 2; }
@@ -0,0 +1,68 @@
1
+ # Preserve every Article in an Agregado batch
2
+
3
+ A regression pack for a real maintained workflow: [ffrt-labs/agregado](https://github.com/ffrt-labs/agregado), a personal content pipeline whose Miniflux webhook delivers multiple Articles at once.
4
+
5
+ `Parse entries` emits one item per Article. The original downstream Code nodes run once for all items, read `$input.first()`, and return one item. With two ordinary Articles, the engine succeeds and only the first Article reaches the Enrichment API.
6
+
7
+ The proposed patch adds **Loop Articles, batch size 1**, ahead of those single-Article nodes. A successful API call advances the loop. The existing backoff stays on the current Article; terminal failure keeps the alert-and-stop path, leaving the remaining Articles for operator replay.
8
+
9
+ ## Recorded real-engine results
10
+
11
+ Executed October 7, 2026 (Asia/Shanghai), with **n8n 2.41.7**, Node.js 26.10.0, and n8n-check 0.1.3 from the repository checkout. Inputs, environment values and HTTP responses are synthetic; the downstream Code, If, HTTP Request, Loop and Wait nodes execute in the actual engine.
12
+
13
+ | Workflow / case | Enrichment POST Article IDs | D1 content reads | Check result |
14
+ |---|---|---:|---|
15
+ | Original, two ordinary Articles | `101` | 0 | **FAIL**, exit 1; engine succeeds |
16
+ | Patch, two ordinary Articles | `101, 102` | 0 | **PASS**, 9/9 |
17
+ | Patch, ordinary / Bridge / ordinary / Bridge | `101, 102, 103, 104` | 2 | **PASS**, 9/9 |
18
+ | Patch, Article 102 gets one 503 then 200 | `101, 102, 102` | 0 | **PASS**, 9/9 |
19
+
20
+ The mixed case checks each Article's complete request body, including its own `bridge_content`. The retry case checks that Article 101 stays at one request while Article 102 retries with its own body. All three successful patched runs reached Loop's Done output with the expected number of completions.
21
+
22
+ [Observed request bodies and checks](observed-results.json) · [Proposed source patch](https://github.com/blucca/agregado/commit/b4e4332b57341f1afdb7a3be7a9069540b8a15f4)
23
+
24
+ ## Reproduce with Docker
25
+
26
+ From an n8n-check **main** checkout, with Node.js 24+, curl, Git and Docker:
27
+
28
+ ```sh
29
+ git clone https://github.com/blucca/n8n-check.git
30
+ cd n8n-check
31
+ node examples/agregado-enrichment/prepare.mjs
32
+ docker build -t n8n-check .
33
+
34
+ # The original must produce exit 1: two Articles in, one Enrichment POST out.
35
+ docker run --rm --network none --user "$(id -u):$(id -g)" \
36
+ -v "$PWD:/work" n8n-check \
37
+ temp/agregado-enrichment/original.json \
38
+ temp/agregado-enrichment/ordinary.case.json \
39
+ --n8n /work/examples/agregado-enrichment/n8n-synthetic \
40
+ --out /work/temp/agregado-enrichment/results/original
41
+
42
+ # All three patched cases must produce exit 0.
43
+ for scenario in ordinary mixed retry; do
44
+ docker run --rm --network none --user "$(id -u):$(id -g)" \
45
+ -v "$PWD:/work" n8n-check \
46
+ temp/agregado-enrichment/fixed.json \
47
+ "temp/agregado-enrichment/$scenario.case.json" \
48
+ --n8n /work/examples/agregado-enrichment/n8n-synthetic \
49
+ --out "/work/temp/agregado-enrichment/results/$scenario" || break
50
+ done
51
+ ```
52
+
53
+ `prepare.mjs` downloads the original and patched exports at immutable Git commits, checks their SHA-256 hashes, and writes three case files. Generated files and JSON/JUnit reports stay under `temp/agregado-enrichment/`. Windows users can run the shell commands in WSL.
54
+
55
+ For a local n8n installation, put its executable on `PATH` and point `--n8n` to the **absolute path** of `examples/agregado-enrichment/n8n-synthetic`. Run in a loopback-only Linux network namespace, or explicitly select `--allow-network` for trusted local development.
56
+
57
+ ## What the check exercises
58
+
59
+ - Fixture input replaces `Parse entries`; all reachable downstream nodes retain their source parameters and connections.
60
+ - Four HTTP nodes use declared loopback mocks. The Enrichment endpoint receives exact-body assertions; the D1 and alert paths have exact request counts.
61
+ - `n8n-synthetic` enables the source project's required `crypto` builtin and Code-node environment access, then supplies a fixed synthetic environment. The n8n-check subprocess still starts with its fresh environment, home and database.
62
+ - The recorded scope starts after webhook/signature parsing. Live Miniflux delivery, Cloudflare credentials, the real Enrichment service, sustained failure/retry exhaustion and terminal-error alert delivery have separate acceptance paths.
63
+
64
+ ## Source and attribution
65
+
66
+ Original workflow: **ffrt-labs/agregado**, commit `0cbbac9c7c6e19f2b9f82bab57cf75a171279eac`, `n8n/workflows/article-enrichment.json`. Its [README declares MIT](https://github.com/ffrt-labs/agregado/blob/0cbbac9c7c6e19f2b9f82bab57cf75a171279eac/README.md#license). The source export is downloaded intact, and its hash is recorded in `prepare.mjs` and generated `sources.json`.
67
+
68
+ Proposed patch: **blucca/agregado**, commit `b4e4332b57341f1afdb7a3be7a9069540b8a15f4`. The patch includes a native graph regression test and regenerated export. These synthetic contracts and the runner are maintained by blucca, an AI-operated software practice.
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # Isolated test runtime; all integration values below are synthetic.
3
+ export NODE_FUNCTION_ALLOW_BUILTIN=crypto
4
+ export N8N_BLOCK_ENV_ACCESS_IN_NODE=false
5
+ export BRIDGE_ORIGIN=https://bridge.example.test
6
+ export AGREGADO_BASE_URL=http://127.0.0.1:1
7
+ export ENRICHMENT_SECRET=synthetic-test-value
8
+ export CF_ACCOUNT_ID=synthetic-account
9
+ export BRIDGE_D1_DATABASE_ID=synthetic-database
10
+ export BRIDGE_ALERT_URL=http://127.0.0.1:1/alert
11
+ exec n8n "$@"