@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
@@ -0,0 +1,39 @@
1
+ import {mkdir, writeFile} from 'node:fs/promises';
2
+ import {resolve} from 'node:path';
3
+ import {createHash} from 'node:crypto';
4
+ import {execFileSync} from 'node:child_process';
5
+ // Download immutable Agregado exports; generate synthetic contracts beside them.
6
+ // Source: ffrt-labs/agregado (README declares MIT). See README for attribution.
7
+ const root=resolve(process.argv[2] ?? 'temp/agregado-enrichment');
8
+ await mkdir(root,{recursive:true});
9
+ const sources={
10
+ original:{repo:'ffrt-labs/agregado',commit:'0cbbac9c7c6e19f2b9f82bab57cf75a171279eac',sha256:'49c2fce7e72ed0b773f9236a9ca29d3146714bd46a001cb0f3ddc7ece1acff1d'},
11
+ fixed:{repo:'blucca/agregado',commit:'b4e4332b57341f1afdb7a3be7a9069540b8a15f4',sha256:'e0104cd7c58bfeb7c5a4e77ce3bc01832bd209ca8aa824ae77ce804781ea18c0'},
12
+ };
13
+ for(const [name,source] of Object.entries(sources)){
14
+ const url=`https://raw.githubusercontent.com/${source.repo}/${source.commit}/n8n/workflows/article-enrichment.json`;
15
+ const bytes=execFileSync('curl',['--fail','--silent','--show-error','--location','--max-time','60',url],{maxBuffer:2*1024*1024});
16
+ const hash=createHash('sha256').update(bytes).digest('hex');
17
+ if(hash!==source.sha256)throw new Error(`${name}: source SHA256 mismatch`);
18
+ JSON.parse(bytes.toString('utf8'));
19
+ await writeFile(`${root}/${name}.json`,bytes);
20
+ }
21
+
22
+ const ordinary=id=>({entry:{id,url:`https://example.test/article-${id}`,title:`Article ${id}`}});
23
+ const bridge=id=>({entry:{id,url:`https://bridge.example.test/p/00000000-0000-4000-8000-${String(id).padStart(12,'0')}`,title:`Article ${id}`}});
24
+ const body=item=>({entry_id:item.entry.id,canonical_url:item.entry.url,title:item.entry.title,...(item.entry.url.includes('/p/')?{bridge_content:`<p>Content for ${item.entry.id}</p>`}:{})});
25
+ const groups={ordinary:[ordinary(101),ordinary(102)],mixed:[ordinary(101),bridge(102),ordinary(103),bridge(104)],retry:[ordinary(101),ordinary(102)]};
26
+ for(const [name,items] of Object.entries(groups)){
27
+ const bridged=items.filter(i=>i.entry.url.includes('/p/'));
28
+ const expected=items.map(body);
29
+ if(name==='retry')expected.push(body(items.at(-1)));
30
+ const spec={version:1,name:`Agregado ${name}: preserve every Article`,input:{node:'Parse entries',items},timeoutMs:90000,mocks:[
31
+ {node:'Call enrich API',url:'/api/private/articles/enrich',routes:[{method:'POST',path:'/api/private/articles/enrich',responses:name==='retry'?[{status:200,json:{ok:true}},{status:503,json:{message:'temporary unavailable'}},{status:200,json:{ok:true}}]:[{status:200,json:{ok:true}}],expect:{count:expected.length,bodies:expected}}]},
32
+ {node:'Lookup bridge content',url:'/d1/content',routes:[{method:'POST',path:'/d1/content',responses:bridged.length?bridged.map(i=>({status:200,json:{result:[{results:[{readable_content:`<p>Content for ${i.entry.id}</p>`}]}]}})):[{status:200,json:{result:[{results:[]}]}}],expect:{count:bridged.length}}]},
33
+ {node:'Claim alert',url:'/d1/claim',routes:[{method:'POST',path:'/d1/claim',responses:[{status:200,json:{result:[{meta:{changes:1}}]}}],expect:{count:0}}]},
34
+ {node:'Notify',url:'/alert',routes:[{method:'POST',path:'/alert',responses:[{status:200,json:{ok:true}}],expect:{count:0}}]},
35
+ ],assertions:[{node:'Build enrich request',equals:items.map(i=>({entry:i.entry,body:body(i)}))}]};
36
+ await writeFile(`${root}/${name}.case.json`,JSON.stringify(spec,null,2)+'\n');
37
+ }
38
+ await writeFile(`${root}/sources.json`,JSON.stringify(sources,null,2)+'\n');
39
+ console.log(`Prepared two pinned workflows and three synthetic cases in ${root}`);
@@ -0,0 +1,39 @@
1
+ # Keep row identities stable as metadata changes
2
+
3
+ A three-row fixture contains two eligible records and one distractor. The Code node filters by score and adds a fresh `processedAt` timestamp. The contract checks two ordered IDs; timestamps can change between runs.
4
+
5
+ ```json
6
+ { "node": "Select rows", "count": 2, "pluck": "json.id", "equals": ["row-A", "row-B"] }
7
+ ```
8
+
9
+ **Available in v0.1.6+.** The [local case builder](https://blucca.github.io/n8n-check/) offers **Selected field + item count**: choose `json.id`, review the expected IDs, and download the case and CI file.
10
+
11
+ ## Run
12
+
13
+ From a current source checkout, build the isolated runtime:
14
+
15
+ ```sh
16
+ docker build -t n8n-check .
17
+ docker run --rm --network none -v "$PWD:/work" n8n-check \
18
+ examples/identity-projection/workflow.json examples/identity-projection/case.json \
19
+ --out /work/results/identity-fixed
20
+ # Exit 0: two items, both expected IDs.
21
+
22
+ docker run --rm --network none -v "$PWD:/work" n8n-check \
23
+ examples/identity-projection/wrong-id.json examples/identity-projection/case.json \
24
+ --out /work/results/identity-wrong
25
+ # Exit 1: two items, wrong IDs; the n8n execution still succeeds.
26
+ ```
27
+
28
+ ## Recorded comparison
29
+
30
+ Real n8n 2.41.7, loopback-only network, synthetic data:
31
+
32
+ | Workflow | Engine | Count | Ordered identities | Contract |
33
+ |---|---|---|---|---|
34
+ | `workflow.json` | Success | 2: pass | row-A, row-B: pass | 5/5, exit 0 |
35
+ | `wrong-id.json` | Success | 2: pass | wrong-row, wrong-row: fail | 4/5, exit 1 |
36
+
37
+ [Observed checks and source hashes](observed-results.json) record the comparison. The negative control preserves the item count and changes both IDs. This catches a same-size replacement while allowing timestamp metadata to vary.
38
+
39
+ Projection uses an own-property dotted path from each n8n item. Missing fields produce failure diagnostics. Order and duplicates remain significant. Select full-object `equals` when every output field belongs in the acceptance contract.
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": 1,
3
+ "name": "Preserve row identities while metadata changes",
4
+ "input": {
5
+ "node": "Rows",
6
+ "items": [
7
+ {
8
+ "id": "row-A",
9
+ "score": 0.9
10
+ },
11
+ {
12
+ "id": "row-B",
13
+ "score": 0.8
14
+ },
15
+ {
16
+ "id": "distractor",
17
+ "score": 0.2
18
+ }
19
+ ]
20
+ },
21
+ "mocks": [],
22
+ "assertions": [
23
+ {
24
+ "node": "Select rows",
25
+ "count": 2,
26
+ "pluck": "json.id",
27
+ "equals": [
28
+ "row-A",
29
+ "row-B"
30
+ ]
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,117 @@
1
+ {
2
+ "scope": "Synthetic identity projection comparison; main source with count/pluck, following v0.1.5",
3
+ "featureCommit": "1c58962",
4
+ "files": {
5
+ "workflow.json": "52268bee2c061ddfde870b8775dc8f0da0f08745286962eaff5f1736cf8ece31",
6
+ "wrong-id.json": "cf05d59ee7ca254481ecf5db43b006da79379528c2c80d704baa78a817e5d0bf",
7
+ "case.json": "a6237f7bae2fb8713cd4dd2265a195d5185302709a8eab2707d467c30a498429"
8
+ },
9
+ "runs": {
10
+ "fixed": {
11
+ "startedAt": "2026-10-06T22:55:14.503Z",
12
+ "n8nVersion": "2.41.7",
13
+ "network": {
14
+ "mode": "loopback-only",
15
+ "enforced": true,
16
+ "interfaces": [
17
+ "lo"
18
+ ]
19
+ },
20
+ "status": "passed",
21
+ "exitCode": 0,
22
+ "checks": [
23
+ {
24
+ "name": "n8n execution succeeds",
25
+ "passed": true,
26
+ "actual": {
27
+ "cliExitCode": 0,
28
+ "lastNode": "Select rows",
29
+ "executionFound": true
30
+ }
31
+ },
32
+ {
33
+ "name": "Select rows: output 0 count",
34
+ "passed": true,
35
+ "expected": 2,
36
+ "actual": 2
37
+ },
38
+ {
39
+ "name": "Select rows: output 0 pluck json.id",
40
+ "passed": true,
41
+ "expected": [
42
+ "row-A",
43
+ "row-B"
44
+ ],
45
+ "actual": [
46
+ "row-A",
47
+ "row-B"
48
+ ]
49
+ },
50
+ {
51
+ "name": "mock: all requests match routes",
52
+ "passed": true,
53
+ "actual": []
54
+ },
55
+ {
56
+ "name": "mock: request limits and server",
57
+ "passed": true,
58
+ "actual": []
59
+ }
60
+ ],
61
+ "requests": []
62
+ },
63
+ "wrong-id": {
64
+ "startedAt": "2026-10-06T22:55:57.808Z",
65
+ "n8nVersion": "2.41.7",
66
+ "network": {
67
+ "mode": "loopback-only",
68
+ "enforced": true,
69
+ "interfaces": [
70
+ "lo"
71
+ ]
72
+ },
73
+ "status": "failed",
74
+ "exitCode": 1,
75
+ "checks": [
76
+ {
77
+ "name": "n8n execution succeeds",
78
+ "passed": true,
79
+ "actual": {
80
+ "cliExitCode": 0,
81
+ "lastNode": "Select rows",
82
+ "executionFound": true
83
+ }
84
+ },
85
+ {
86
+ "name": "Select rows: output 0 count",
87
+ "passed": true,
88
+ "expected": 2,
89
+ "actual": 2
90
+ },
91
+ {
92
+ "name": "Select rows: output 0 pluck json.id",
93
+ "passed": false,
94
+ "expected": [
95
+ "row-A",
96
+ "row-B"
97
+ ],
98
+ "actual": [
99
+ "wrong-row",
100
+ "wrong-row"
101
+ ]
102
+ },
103
+ {
104
+ "name": "mock: all requests match routes",
105
+ "passed": true,
106
+ "actual": []
107
+ },
108
+ {
109
+ "name": "mock: request limits and server",
110
+ "passed": true,
111
+ "actual": []
112
+ }
113
+ ],
114
+ "requests": []
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "Stable row identities",
3
+ "nodes": [
4
+ {
5
+ "id": "start",
6
+ "name": "Start",
7
+ "type": "n8n-nodes-base.manualTrigger",
8
+ "typeVersion": 1,
9
+ "position": [
10
+ -400,
11
+ 0
12
+ ],
13
+ "parameters": {}
14
+ },
15
+ {
16
+ "id": "rows",
17
+ "name": "Rows",
18
+ "type": "n8n-nodes-base.code",
19
+ "typeVersion": 2,
20
+ "position": [
21
+ -200,
22
+ 0
23
+ ],
24
+ "parameters": {
25
+ "mode": "runOnceForAllItems",
26
+ "jsCode": "return $input.all();"
27
+ }
28
+ },
29
+ {
30
+ "id": "select",
31
+ "name": "Select rows",
32
+ "type": "n8n-nodes-base.code",
33
+ "typeVersion": 2,
34
+ "position": [
35
+ 0,
36
+ 0
37
+ ],
38
+ "parameters": {
39
+ "mode": "runOnceForAllItems",
40
+ "jsCode": "return $input.all().filter(item => item.json.score >= 0.7).map(item => ({ json: { ...item.json, processedAt: new Date().toISOString() } }));"
41
+ }
42
+ }
43
+ ],
44
+ "connections": {
45
+ "Start": {
46
+ "main": [
47
+ [
48
+ {
49
+ "node": "Rows",
50
+ "type": "main",
51
+ "index": 0
52
+ }
53
+ ]
54
+ ]
55
+ },
56
+ "Rows": {
57
+ "main": [
58
+ [
59
+ {
60
+ "node": "Select rows",
61
+ "type": "main",
62
+ "index": 0
63
+ }
64
+ ]
65
+ ]
66
+ }
67
+ },
68
+ "settings": {
69
+ "executionOrder": "v1"
70
+ }
71
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "Stable row identities",
3
+ "nodes": [
4
+ {
5
+ "id": "start",
6
+ "name": "Start",
7
+ "type": "n8n-nodes-base.manualTrigger",
8
+ "typeVersion": 1,
9
+ "position": [
10
+ -400,
11
+ 0
12
+ ],
13
+ "parameters": {}
14
+ },
15
+ {
16
+ "id": "rows",
17
+ "name": "Rows",
18
+ "type": "n8n-nodes-base.code",
19
+ "typeVersion": 2,
20
+ "position": [
21
+ -200,
22
+ 0
23
+ ],
24
+ "parameters": {
25
+ "mode": "runOnceForAllItems",
26
+ "jsCode": "return $input.all();"
27
+ }
28
+ },
29
+ {
30
+ "id": "select",
31
+ "name": "Select rows",
32
+ "type": "n8n-nodes-base.code",
33
+ "typeVersion": 2,
34
+ "position": [
35
+ 0,
36
+ 0
37
+ ],
38
+ "parameters": {
39
+ "mode": "runOnceForAllItems",
40
+ "jsCode": "return $input.all().filter(item => item.json.score >= 0.7).map(item => ({ json: { ...item.json, id: \"wrong-row\", processedAt: new Date().toISOString() } }));"
41
+ }
42
+ }
43
+ ],
44
+ "connections": {
45
+ "Start": {
46
+ "main": [
47
+ [
48
+ {
49
+ "node": "Rows",
50
+ "type": "main",
51
+ "index": 0
52
+ }
53
+ ]
54
+ ]
55
+ },
56
+ "Rows": {
57
+ "main": [
58
+ [
59
+ {
60
+ "node": "Select rows",
61
+ "type": "main",
62
+ "index": 0
63
+ }
64
+ ]
65
+ ]
66
+ }
67
+ },
68
+ "settings": {
69
+ "executionOrder": "v1"
70
+ }
71
+ }
@@ -0,0 +1,77 @@
1
+ # Morsof invoice-history regression contract
2
+
3
+ Executable coverage for the seven batch-history cases in [Morsof issue #7](https://github.com/Morsoflab/n8n-automation-templates/issues/7), using the workflow from [PR #8](https://github.com/Morsoflab/n8n-automation-templates/pull/8), real n8n Code/If/Data Table nodes, and isolated SQLite storage.
4
+
5
+ ## Source and results
6
+
7
+ - Current workflow: `3d8c61ee5b0e37e7e0052f53e7839db011b9c0de`.
8
+ - Historical first version: `adb2d3c3aa7534bcce53d955938e24c9f061b315`.
9
+ - `prepare.mjs` downloads those exports and verifies their SHA-256 hashes.
10
+ - Runtime: n8n **2.41.7**, n8n-check **0.1.3**, Node **26.10.0** in the recorded run; Linux loopback-only networking.
11
+
12
+ | Case | Current result | Persisted history rows |
13
+ |---|---|---:|
14
+ | Two eligible invoices | 9/9 | 2 |
15
+ | Eligible + paid | 9/9 | 1 |
16
+ | Existing + new | 9/9 | 2 |
17
+ | Duplicate input | 9/9; one stored ID shared by both outputs | 1 |
18
+ | Reordered existing + new | 9/9 | 2 |
19
+ | Empty history, three eligible | 9/9 | 3 |
20
+ | Multiple existing rows | 9/9; newest matching row selected | 4 |
21
+ | Unchanged second execution | 9/9; both already prepared; zero writes | 2 |
22
+
23
+ The historical export fails with `Multiple matches` in **Check Reminder History**, produces zero writes, and exits 1 under the same two-invoice contract. Its first error occurs before the reported global-limit data-loss path. The current export passes all seven cases.
24
+
25
+ For same-batch duplicates, **Ready for Review** physically emits both items: the first has `outputPath=ready_for_review`; the second has `outputPath=already_prepared` and `actionReason=duplicate_in_execution`. Both reference the one stored row. The separate **Already Prepared** node receives matches from history loaded before this execution. A downstream approval consumer can select `outputPath === "ready_for_review"` to process fresh drafts.
26
+
27
+ The replay check executes the same imported workflow twice against the same isolated database. It checks the original stored IDs, complete unchanged database rows, and original preparation timestamps.
28
+
29
+ ## Run
30
+
31
+ Requirements: Node 24+, the n8n 2.41.7 CLI on `PATH`, an n8n-check v0.1.3 checkout, Linux `unshare` and `ip`.
32
+
33
+ ```sh
34
+ # Network access is used for the two pinned source downloads.
35
+ node prepare.mjs
36
+
37
+ # Supply the directory containing n8n-check's bin/ and src/.
38
+ # Example: export PATH=/path/to/n8n-runtime/node_modules/.bin:$PATH
39
+ unshare --user --map-root-user --net sh -c \
40
+ 'ip link set lo up && ./run.sh /path/to/n8n-check ./results'
41
+ ```
42
+
43
+ A single case:
44
+
45
+ ```sh
46
+ unshare --user --map-root-user --net sh -c \
47
+ 'ip link set lo up && node /path/to/n8n-check/bin/n8n-check.mjs \
48
+ fixed.workflow.json two-eligible.case.json \
49
+ --n8n "$PWD/n8n-datatable.cjs" --out results/two-eligible'
50
+ ```
51
+
52
+ Each case writes JSON + JUnit and retains its actual execution output and isolated SQLite database. `run.sh` expects the historical regression to exit 1 and finishes with the persistent replay check.
53
+
54
+ ## Fixture adaptations
55
+
56
+ The prepared exports preserve the upstream business-processing nodes, expressions, table schema, and connections. Four explicit fixture changes make the upstream manual test plan repeatable:
57
+
58
+ 1. Set the documented `EVALUATION_DATE` test override to `2030-06-20`.
59
+ 2. Replace **Load Fictional Invoice Samples** with the case's synthetic invoice array.
60
+ 3. Add a fixture-only seed branch between table creation and the original history load. This inserts requested history fixtures through the real Data Table node, including duplicate historical keys.
61
+ 4. Append read-only terminal projections that check invoice/customer/recipient identity, classification, stage, routing, draft linkage, stored row IDs, and writes. The full original outputs remain in `execution.json`.
62
+
63
+ The adapters use fictional `.example` recipients. Every execution stays inactive and contains the upstream preparation workflow, its local fixtures, and its observation nodes.
64
+
65
+ ## Data Table CLI adapter
66
+
67
+ n8n 2.41.7's server startup initializes the Data Table module. Its standalone `execute` command omits that initialization and reports `Attempted to use Data table node but the module is disabled` for this workflow.
68
+
69
+ `n8n-datatable.cjs` is pinned to **2.41.7**. It runs the ordinary CLI with the upstream `DataTableModule.init()` and context registration added after `Execute.init()`. All Data Table node code, proxy validation, and database writes remain the upstream implementation. Its small internal initialization bridge should be reviewed when changing runtime versions. The source workflow and n8n-check runner files stay unchanged.
70
+
71
+ `replay.mjs` uses n8n-check 0.1.3's execution helpers for the second invocation and Node's `node:sqlite` to compare actual rows. The seven ordinary cases use the published CLI case format directly.
72
+
73
+ ## Coverage boundary
74
+
75
+ The contract covers batch correlation, existing-history matching, duplicate inputs, output linkage, and persistent replay. The upstream README's broader contact/date-validation matrix and insertion-failure simulation are separate cases. The historical `Multiple matches` failure and current batch success are the measured comparison.
76
+
77
+ Upstream workflow source is MIT, attributed in `UPSTREAM-LICENSE`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MORSOF
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.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ const path = require('node:path');
3
+ const fs = require('node:fs');
4
+ const cli = (process.env.PATH ?? '').split(path.delimiter).map(dir => path.join(dir, 'n8n')).find(file => fs.existsSync(file));
5
+ if (!cli) throw new Error('Put the n8n 2.41.7 binary on PATH.');
6
+ const root = path.dirname(path.dirname(fs.realpathSync(cli)));
7
+ const {createRequire} = require('node:module');
8
+ const req = createRequire(path.join(root,'package.json'));
9
+ if (req('./package.json').version !== '2.41.7') throw new Error('This Data Table CLI adapter is pinned to n8n 2.41.7.');
10
+ if (process.argv[2] === 'execute') {
11
+ process.env.NODE_CONFIG_DIR = path.join(root,'bin/config');
12
+ req('reflect-metadata');
13
+ req('./dist/config');
14
+ const {Execute} = req('./dist/commands/execute');
15
+ const {Container} = req('@n8n/di');
16
+ const {DataTableModule} = req('./dist/modules/data-table/data-table.module');
17
+ const originalInit = Execute.prototype.init;
18
+ // The server start command performs this module setup; the execute command omits it.
19
+ // Keep the real Data Table storage, proxies, validation, and node implementation.
20
+ Execute.prototype.init = async function() {
21
+ await originalInit.call(this);
22
+ const module = Container.get(DataTableModule);
23
+ await module.init();
24
+ this.moduleRegistry.context.set('data-table', await module.context());
25
+ this.moduleRegistry.activeModules.push('data-table');
26
+ };
27
+ }
28
+ req('./bin/n8n');
@@ -0,0 +1,113 @@
1
+ {
2
+ "recordedAt": "2026-10-06T18:48:00Z",
3
+ "runtime": {
4
+ "n8n": "2.41.7",
5
+ "n8nCheck": "0.1.3",
6
+ "node": "26.10.0",
7
+ "network": "Linux loopback-only",
8
+ "dataTableAdapter": "Real upstream module initialized for CLI execute"
9
+ },
10
+ "sources": {
11
+ "fixed": [
12
+ "3d8c61ee5b0e37e7e0052f53e7839db011b9c0de",
13
+ "b29612d4206a557129f0f97b7ca6619419033bff7940bc2366370fd20838e180"
14
+ ],
15
+ "broken": [
16
+ "adb2d3c3aa7534bcce53d955938e24c9f061b315",
17
+ "43bb3d16f4e1bf10a01cbb75b257e7bdb8e536d2aaedb94f1d05464137dda23a"
18
+ ]
19
+ },
20
+ "cases": [
21
+ {
22
+ "name": "two-eligible",
23
+ "status": "passed",
24
+ "checksPassed": 9,
25
+ "checksTotal": 9,
26
+ "exitCode": 0,
27
+ "persistedRowCount": 2
28
+ },
29
+ {
30
+ "name": "eligible-paid",
31
+ "status": "passed",
32
+ "checksPassed": 9,
33
+ "checksTotal": 9,
34
+ "exitCode": 0,
35
+ "persistedRowCount": 1
36
+ },
37
+ {
38
+ "name": "existing-new",
39
+ "status": "passed",
40
+ "checksPassed": 9,
41
+ "checksTotal": 9,
42
+ "exitCode": 0,
43
+ "persistedRowCount": 2
44
+ },
45
+ {
46
+ "name": "duplicate",
47
+ "status": "passed",
48
+ "checksPassed": 9,
49
+ "checksTotal": 9,
50
+ "exitCode": 0,
51
+ "persistedRowCount": 1
52
+ },
53
+ {
54
+ "name": "reordered",
55
+ "status": "passed",
56
+ "checksPassed": 9,
57
+ "checksTotal": 9,
58
+ "exitCode": 0,
59
+ "persistedRowCount": 2
60
+ },
61
+ {
62
+ "name": "empty-history",
63
+ "status": "passed",
64
+ "checksPassed": 9,
65
+ "checksTotal": 9,
66
+ "exitCode": 0,
67
+ "persistedRowCount": 3
68
+ },
69
+ {
70
+ "name": "multiple-existing",
71
+ "status": "passed",
72
+ "checksPassed": 9,
73
+ "checksTotal": 9,
74
+ "exitCode": 0,
75
+ "persistedRowCount": 4
76
+ },
77
+ {
78
+ "name": "broken",
79
+ "status": "failed",
80
+ "checksPassed": 6,
81
+ "checksTotal": 9,
82
+ "exitCode": 1,
83
+ "persistedRowCount": 0
84
+ }
85
+ ],
86
+ "historicalFailure": {
87
+ "node": "Check Reminder History",
88
+ "message": "Multiple matches",
89
+ "writes": 0
90
+ },
91
+ "duplicateSemantics": {
92
+ "physicalNode": "Ready for Review",
93
+ "outputPaths": [
94
+ "ready_for_review",
95
+ "already_prepared"
96
+ ],
97
+ "secondReason": "duplicate_in_execution",
98
+ "sharedHistoryRecordId": 1
99
+ },
100
+ "persistentReplay": {
101
+ "checksPassed": 9,
102
+ "checksTotal": 9,
103
+ "status": "passed",
104
+ "persistedRowCount": 2,
105
+ "secondExecutionWrites": 0,
106
+ "originalRowIds": [
107
+ 1,
108
+ 2
109
+ ],
110
+ "completeRowsUnchanged": true,
111
+ "preparationTimestampsPreserved": true
112
+ }
113
+ }