automategraph 1.5.1 → 1.5.3

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 (88) hide show
  1. package/CHANGELOG.md +18 -5
  2. package/UPGRADE-PROPOSAL.md +141 -0
  3. package/docs/diagrams/c19-resilience-scenario.md +61 -0
  4. package/docs/diagrams/c19-resilience-scenario.mmd +28 -0
  5. package/docs/diagrams/c19-resilience-scenario.svg +181 -0
  6. package/examples/README.md +206 -0
  7. package/examples/branching-edges.ts +192 -0
  8. package/examples/branching-graph.ts +333 -0
  9. package/examples/branching-router.ts +475 -0
  10. package/examples/json-graph.ts +231 -0
  11. package/examples/json-scripts.ts +152 -0
  12. package/examples/order-flow.functions.cjs +32 -0
  13. package/examples/order-flow.graph.json +27 -0
  14. package/examples/order-flow.scripted.graph.json +68 -0
  15. package/js/examples/branching-edges.js +133 -0
  16. package/js/examples/branching-edges.js.map +1 -0
  17. package/js/examples/branching-graph.js +232 -0
  18. package/js/examples/branching-graph.js.map +1 -0
  19. package/js/examples/branching-router.js +305 -0
  20. package/js/examples/branching-router.js.map +1 -0
  21. package/js/examples/json-graph.js +197 -0
  22. package/js/examples/json-graph.js.map +1 -0
  23. package/js/examples/json-scripts.js +131 -0
  24. package/js/examples/json-scripts.js.map +1 -0
  25. package/js/tests/async.test.js +592 -0
  26. package/js/tests/async.test.js.map +1 -0
  27. package/js/tests/complex.test.js +972 -0
  28. package/js/tests/complex.test.js.map +1 -0
  29. package/js/tests/conformance.test.js +365 -0
  30. package/js/tests/conformance.test.js.map +1 -0
  31. package/js/tests/edges.test.js +702 -0
  32. package/js/tests/edges.test.js.map +1 -0
  33. package/js/tests/graphjson.test.js +504 -0
  34. package/js/tests/graphjson.test.js.map +1 -0
  35. package/js/tests/harness.js +192 -0
  36. package/js/tests/harness.js.map +1 -0
  37. package/js/tests/helpers.js +136 -0
  38. package/js/tests/helpers.js.map +1 -0
  39. package/js/tests/lint.test.js +317 -0
  40. package/js/tests/lint.test.js.map +1 -0
  41. package/js/tests/registry.test.js +705 -0
  42. package/js/tests/registry.test.js.map +1 -0
  43. package/js/tests/report.js +422 -0
  44. package/js/tests/report.js.map +1 -0
  45. package/js/tests/runner.js +67 -0
  46. package/js/tests/runner.js.map +1 -0
  47. package/js/tests/scripts.test.js +552 -0
  48. package/js/tests/scripts.test.js.map +1 -0
  49. package/js/tests/unit.test.js +1239 -0
  50. package/js/tests/unit.test.js.map +1 -0
  51. package/js/tests/upgrade.test.js +618 -0
  52. package/js/tests/upgrade.test.js.map +1 -0
  53. package/package.json +2 -9
  54. package/report/index.html +3727 -0
  55. package/report/report.json +3053 -0
  56. package/tests/async.test.ts +668 -0
  57. package/tests/complex.test.ts +1123 -0
  58. package/tests/conformance.test.ts +442 -0
  59. package/tests/edges.test.ts +848 -0
  60. package/tests/fixtures/scripts/bad-registration.cjs +4 -0
  61. package/tests/fixtures/scripts/broken.cjs +2 -0
  62. package/tests/fixtures/scripts/cjs-default.js +4 -0
  63. package/tests/fixtures/scripts/collide-a.cjs +4 -0
  64. package/tests/fixtures/scripts/collide-b.cjs +4 -0
  65. package/tests/fixtures/scripts/empty.cjs +2 -0
  66. package/tests/fixtures/scripts/esm-functions.mjs +10 -0
  67. package/tests/fixtures/scripts/esm-syntax.js +3 -0
  68. package/tests/fixtures/scripts/global-channel.cjs +6 -0
  69. package/tests/fixtures/scripts/global-channel.mjs +7 -0
  70. package/tests/fixtures/scripts/mismatch.cjs +4 -0
  71. package/tests/fixtures/scripts/needs-sibling.cjs +7 -0
  72. package/tests/fixtures/scripts/order.cjs +14 -0
  73. package/tests/fixtures/scripts/sibling-helper.cjs +3 -0
  74. package/tests/fixtures/scripts/throwing-global.mjs +6 -0
  75. package/tests/graphjson.test.ts +737 -0
  76. package/tests/harness.ts +291 -0
  77. package/tests/helpers.ts +216 -0
  78. package/tests/lint.test.ts +381 -0
  79. package/tests/registry.test.ts +1134 -0
  80. package/tests/report.ts +452 -0
  81. package/tests/runner.ts +58 -0
  82. package/tests/scripts.test.ts +970 -0
  83. package/tests/unit.test.ts +1192 -0
  84. package/tests/upgrade.test.ts +725 -0
  85. package/tools/server.mjs +152 -0
  86. package/tsconfig.browser.json +19 -0
  87. package/tsconfig.json +18 -0
  88. package/tsconfig.publish.json +13 -0
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /* ============================================================
3
+ * Minimal zero-dependency test harness
4
+ * ------------------------------------------------------------
5
+ * - deterministic, sequential execution (isolation per test)
6
+ * - per-test timeout
7
+ * - per-test log capture (surfaced in the web report)
8
+ * - findings tagging (documents observed engine semantics)
9
+ * ============================================================ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.TEST_KINDS = void 0;
12
+ exports.suite = suite;
13
+ exports.allTests = allTests;
14
+ exports.runAll = runAll;
15
+ exports.TEST_KINDS = [
16
+ "unit",
17
+ "integration",
18
+ "async",
19
+ "upgrade",
20
+ "conformance",
21
+ "lint",
22
+ "edges",
23
+ "registry",
24
+ "graphjson",
25
+ "scripts"
26
+ ];
27
+ const registry = [];
28
+ let counters = { unit: 0, integration: 0, async: 0, upgrade: 0, conformance: 0, lint: 0, edges: 0, registry: 0, graphjson: 0, scripts: 0 };
29
+ function suite(kind, category) {
30
+ return {
31
+ /**
32
+ * Register a test case.
33
+ */
34
+ t(name, fn, options = {}) {
35
+ counters[kind] += 1;
36
+ registry.push({
37
+ id: `${kind[0].toUpperCase()}${counters[kind]}`,
38
+ kind,
39
+ category,
40
+ name,
41
+ finding: options.finding,
42
+ timeoutMs: options.timeoutMs ?? 10_000,
43
+ fn
44
+ });
45
+ }
46
+ };
47
+ }
48
+ function allTests() {
49
+ return registry;
50
+ }
51
+ function formatLogValue(value) {
52
+ if (value === undefined)
53
+ return "";
54
+ try {
55
+ return JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
56
+ }
57
+ catch {
58
+ return String(value);
59
+ }
60
+ }
61
+ function errorShape(value) {
62
+ if (value instanceof Error) {
63
+ return {
64
+ name: value.name,
65
+ message: value.message,
66
+ stack: value.stack
67
+ };
68
+ }
69
+ return { name: "NonErrorThrow", message: String(value) };
70
+ }
71
+ async function withTimeout(operation, timeoutMs, label) {
72
+ let timer;
73
+ const guard = new Promise((_, reject) => {
74
+ timer = setTimeout(() => reject(new Error(`Test "${label}" exceeded harness timeout of ${timeoutMs}ms ` +
75
+ `(possible engine hang)`)), timeoutMs);
76
+ });
77
+ try {
78
+ return await Promise.race([operation, guard]);
79
+ }
80
+ finally {
81
+ if (timer !== undefined)
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+ async function runAll() {
86
+ const unhandledRejections = [];
87
+ const onUnhandled = (reason) => {
88
+ unhandledRejections.push(reason instanceof Error
89
+ ? `${reason.name}: ${reason.message}`
90
+ : String(reason));
91
+ };
92
+ process.on("unhandledRejection", onUnhandled);
93
+ const results = [];
94
+ const startedAll = performance.now();
95
+ for (const test of allTests()) {
96
+ const logs = [];
97
+ const ctx = {
98
+ log(message, ...values) {
99
+ const suffix = values
100
+ .filter(value => value !== undefined)
101
+ .map(value => formatLogValue(value))
102
+ .join(" ; ");
103
+ logs.push(`${message}${suffix ? ` :: ${suffix}` : ""}`);
104
+ }
105
+ };
106
+ const started = performance.now();
107
+ let record;
108
+ try {
109
+ await withTimeout(Promise.resolve().then(() => test.fn(ctx)), test.timeoutMs, test.name);
110
+ record = {
111
+ id: test.id,
112
+ kind: test.kind,
113
+ category: test.category,
114
+ name: test.name,
115
+ finding: test.finding,
116
+ status: "passed",
117
+ durationMs: Math.round((performance.now() - started) * 100) / 100,
118
+ logs
119
+ };
120
+ }
121
+ catch (error) {
122
+ record = {
123
+ id: test.id,
124
+ kind: test.kind,
125
+ category: test.category,
126
+ name: test.name,
127
+ finding: test.finding,
128
+ status: "failed",
129
+ durationMs: Math.round((performance.now() - started) * 100) / 100,
130
+ logs,
131
+ error: errorShape(error)
132
+ };
133
+ }
134
+ results.push(record);
135
+ const mark = record.status === "passed" ? "PASS" : "FAIL";
136
+ process.stdout.write(`[${mark}] ${record.id} ${record.category} :: ${record.name} (${record.durationMs}ms)\n`);
137
+ if (record.error) {
138
+ process.stdout.write(` -> ${record.error.name}: ${record.error.message}\n`);
139
+ }
140
+ }
141
+ const durationMs = Math.round((performance.now() - startedAll) * 100) / 100;
142
+ // Give detached (non-required) listeners a chance to surface problems
143
+ // before we snapshot unhandled rejections.
144
+ await new Promise(resolve => setTimeout(resolve, 50));
145
+ process.off("unhandledRejection", onUnhandled);
146
+ const categories = new Map();
147
+ for (const record of results) {
148
+ const key = `${record.kind}::${record.category}`;
149
+ let entry = categories.get(key);
150
+ if (!entry) {
151
+ entry = {
152
+ category: record.category,
153
+ kind: record.kind,
154
+ total: 0,
155
+ passed: 0,
156
+ failed: 0,
157
+ durationMs: 0
158
+ };
159
+ categories.set(key, entry);
160
+ }
161
+ entry.total += 1;
162
+ entry.passed += record.status === "passed" ? 1 : 0;
163
+ entry.failed += record.status === "failed" ? 1 : 0;
164
+ entry.durationMs = Math.round((entry.durationMs + record.durationMs) * 100) / 100;
165
+ }
166
+ const byKind = Object.fromEntries(exports.TEST_KINDS.map(kind => {
167
+ const group = results.filter(record => record.kind === kind);
168
+ return [
169
+ kind,
170
+ {
171
+ total: group.length,
172
+ passed: group.filter(record => record.status === "passed").length,
173
+ failed: group.filter(record => record.status === "failed").length
174
+ }
175
+ ];
176
+ }));
177
+ return {
178
+ generatedAt: new Date().toISOString(),
179
+ nodeVersion: process.version,
180
+ durationMs,
181
+ totals: {
182
+ total: results.length,
183
+ passed: results.filter(record => record.status === "passed").length,
184
+ failed: results.filter(record => record.status === "failed").length,
185
+ byKind
186
+ },
187
+ byCategory: [...categories.values()],
188
+ unhandledRejections,
189
+ tests: results
190
+ };
191
+ }
192
+ //# sourceMappingURL=harness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../tests/harness.ts"],"names":[],"mappings":";AAAA;;;;;;;kEAOkE;;;AAuDlE,sBAsBC;AAED,4BAEC;AAyED,wBAiIC;AAzRY,QAAA,UAAU,GAAG;IACxB,MAAM;IACN,aAAa;IACb,OAAO;IACP,SAAS;IACT,aAAa;IACb,MAAM;IACN,OAAO;IACP,UAAU;IACV,WAAW;IACX,SAAS;CACD,CAAC;AAsCX,MAAM,QAAQ,GAAe,EAAE,CAAC;AAEhC,IAAI,QAAQ,GAA2B,EAAE,IAAI,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAEnK,SAAgB,KAAK,CAAC,IAAc,EAAE,QAAgB;IACpD,OAAO;QACL;;WAEG;QACH,CAAC,CACC,IAAY,EACZ,EAAkB,EAClB,UAAoD,EAAE;YAEtD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE;gBAC/C,IAAI;gBACJ,QAAQ;gBACR,IAAI;gBACJ,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,MAAM;gBACtC,EAAE;aACH,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAgB,QAAQ;IACtB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CACrC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CACxC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,SAAqB,EACrB,SAAiB,EACjB,KAAa;IAEb,IAAI,KAAgD,CAAC;IAErD,MAAM,KAAK,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC7C,KAAK,GAAG,UAAU,CAChB,GAAG,EAAE,CACH,MAAM,CACJ,IAAI,KAAK,CACP,SAAS,KAAK,iCAAiC,SAAS,KAAK;YAC3D,wBAAwB,CAC3B,CACF,EACH,SAAS,CACV,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC;YAAS,CAAC;QACT,IAAI,KAAK,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAwBM,KAAK,UAAU,MAAM;IAC1B,MAAM,mBAAmB,GAAa,EAAE,CAAC;IAEzC,MAAM,WAAW,GAAG,CAAC,MAAe,EAAE,EAAE;QACtC,mBAAmB,CAAC,IAAI,CACtB,MAAM,YAAY,KAAK;YACrB,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,EAAE;YACrC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CACnB,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;IAE9C,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAgB;YACvB,GAAG,CAAC,OAAO,EAAE,GAAG,MAAM;gBACpB,MAAM,MAAM,GAAG,MAAM;qBAClB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;qBACpC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;qBACnC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACf,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1D,CAAC;SACF,CAAC;QAEF,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAClC,IAAI,MAAwB,CAAC;QAE7B,IAAI,CAAC;YACH,MAAM,WAAW,CACf,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAC1C,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,IAAI,CACV,CAAC;YACF,MAAM,GAAG;gBACP,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,QAAQ;gBAChB,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG;gBACjE,IAAI;aACL,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG;gBACP,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,QAAQ;gBAChB,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG;gBACjE,IAAI;gBACJ,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC;aACzB,CAAC;QACJ,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAErB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,IAAI,IAAI,KAAK,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,QAAQ,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,UAAU,OAAO,CACzF,CAAC;QACF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IAE5E,sEAAsE;IACtE,2CAA2C;IAC3C,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;IAE/C,MAAM,UAAU,GAAG,IAAI,GAAG,EAA4C,CAAC;IAEvE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;QACjD,IAAI,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG;gBACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,CAAC;gBACR,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,UAAU,EAAE,CAAC;aACd,CAAC;YACF,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QACjB,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnD,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnD,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IACpF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAC/B,kBAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACpB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAC7D,OAAO;YACL,IAAI;YACJ;gBACE,KAAK,EAAE,KAAK,CAAC,MAAM;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;gBACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;aAClE;SACF,CAAC;IACJ,CAAC,CAAC,CAC6B,CAAC;IAElC,OAAO;QACL,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,WAAW,EAAE,OAAO,CAAC,OAAO;QAC5B,UAAU;QACV,MAAM,EAAE;YACN,KAAK,EAAE,OAAO,CAAC,MAAM;YACrB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;YACnE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM;YACnE,MAAM;SACP;QACD,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;QACpC,mBAAmB;QACnB,KAAK,EAAE,OAAO;KACf,CAAC;AACJ,CAAC"}
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /* ============================================================
3
+ * Shared fixtures / builders / mocks for the AutomateGraph suite
4
+ * ============================================================ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.tick = exports.SpyStorage = exports.BIG = exports.sleep = void 0;
7
+ exports.node = node;
8
+ exports.constNode = constNode;
9
+ exports.g = g;
10
+ exports.linear = linear;
11
+ exports.plugin = plugin;
12
+ exports.closeOnlyPlugin = closeOnlyPlugin;
13
+ exports.capture = capture;
14
+ exports.errorsOnly = errorsOnly;
15
+ exports.logCollector = logCollector;
16
+ exports.sortedIds = sortedIds;
17
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
18
+ exports.sleep = sleep;
19
+ /** Generous per-test timeout for engine round-trips (macrotasks + storage). */
20
+ exports.BIG = { timeoutMs: 20_000 };
21
+ function node(id, run, extra = {}) {
22
+ return { id, run, ...extra };
23
+ }
24
+ function constNode(id, output, extra = {}) {
25
+ return node(id, () => output, extra);
26
+ }
27
+ function g(spec) {
28
+ const nodes = {};
29
+ for (const definition of spec.nodes)
30
+ nodes[definition.id] = definition;
31
+ return {
32
+ id: spec.id ?? "test-graph",
33
+ version: spec.version ?? "1.0.0",
34
+ entryNodes: spec.entryNodes,
35
+ terminalNodes: spec.terminalNodes,
36
+ nodes,
37
+ defaultNodeTimeoutMs: spec.defaultNodeTimeoutMs,
38
+ errorPolicy: spec.errorPolicy,
39
+ checkpointEveryNode: spec.checkpointEveryNode,
40
+ completionPolicy: spec.completionPolicy,
41
+ functions: spec.functions
42
+ };
43
+ }
44
+ /** Linear graph: a -> b -> c ... ; last node is the terminal. */
45
+ function linear(ids, run, extra = {}) {
46
+ const nodes = ids.map((id, index) => node(id, ctx => (run ? run(id, ctx) : `${id}-out`), index < ids.length - 1 ? { next: [ids[index + 1]] } : {}));
47
+ return g({
48
+ nodes,
49
+ entryNodes: [ids[0]],
50
+ terminalNodes: [ids[ids.length - 1]],
51
+ ...extra
52
+ });
53
+ }
54
+ /* ------------------------- storages ------------------------- */
55
+ /** Records snapshots by reference, can be made to fail on the Nth call. */
56
+ class SpyStorage {
57
+ id = "test.spy-storage";
58
+ version = "9.9.9";
59
+ snapshots = [];
60
+ calls = 0;
61
+ failOn = () => false;
62
+ async saveAtomic(snapshot) {
63
+ this.calls += 1;
64
+ if (this.failOn(this.calls, snapshot)) {
65
+ throw new Error(`disk-full@${this.calls}`);
66
+ }
67
+ this.snapshots.push(snapshot);
68
+ }
69
+ async load(executionId) {
70
+ for (let i = this.snapshots.length - 1; i >= 0; i -= 1) {
71
+ if (this.snapshots[i].executionId === executionId) {
72
+ return this.snapshots[i];
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ }
78
+ exports.SpyStorage = SpyStorage;
79
+ function plugin(spec) {
80
+ const events = [];
81
+ return {
82
+ id: spec.id,
83
+ version: spec.version ?? "1.0.0",
84
+ required: spec.required,
85
+ events,
86
+ async initialize() {
87
+ events.push(`init:${spec.id}`);
88
+ await spec.onInitialize?.();
89
+ },
90
+ async close() {
91
+ events.push(`close:${spec.id}`);
92
+ await spec.onClose?.();
93
+ }
94
+ };
95
+ }
96
+ /** Plugin with only a close() hook (no initialize). */
97
+ function closeOnlyPlugin(id, events) {
98
+ return {
99
+ id,
100
+ version: "1.0.0",
101
+ async close() {
102
+ events.push(`close:${id}`);
103
+ }
104
+ };
105
+ }
106
+ /**
107
+ * Attaches required listeners (awaited by the bus, therefore
108
+ * deterministically ordered) and records payloads.
109
+ */
110
+ function capture(engine, eventNames) {
111
+ const captured = [];
112
+ for (const name of eventNames) {
113
+ engine.events.on(name, (payload) => {
114
+ captured.push({ event: name, payload });
115
+ }, { required: true });
116
+ }
117
+ return captured;
118
+ }
119
+ function errorsOnly(captured) {
120
+ return captured.filter(entry => entry.event === "node.failed");
121
+ }
122
+ /** Creates an onLog collector for assertions. */
123
+ function logCollector() {
124
+ const lines = [];
125
+ const onLog = (level, message, details) => {
126
+ lines.push({ level, message, details });
127
+ };
128
+ return { lines, onLog };
129
+ }
130
+ /** A promise that resolves on the next macrotask tick. */
131
+ const tick = () => new Promise(resolve => setImmediate(resolve));
132
+ exports.tick = tick;
133
+ function sortedIds(values) {
134
+ return [...values].sort();
135
+ }
136
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../tests/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ /* ============================================================
3
+ * LINT PACK — success dead-end rule (v1.2)
4
+ * ------------------------------------------------------------
5
+ * Closes the gap found while explaining the C-19 resilience
6
+ * fixture: a non-terminal node whose success path has nowhere to
7
+ * go (no `next`) used to pass linting as long as it declared an
8
+ * error transition.
9
+ *
10
+ * New code:
11
+ * SUCCESS_DEAD_END (error tier) — the node can only "continue"
12
+ * when it fails; a successful output is silently unreachable.
13
+ * DEAD_END_NODE stays for the total case (no transitions at all).
14
+ * The two are mutually exclusive: every node with no `next` gets
15
+ * exactly one of them.
16
+ * ============================================================ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const automategraph_1 = require("../src/automategraph");
19
+ const helpers_1 = require("./helpers");
20
+ const harness_1 = require("./harness");
21
+ const lint = (0, harness_1.suite)("lint", "Success dead-end rule");
22
+ const BIG = { timeoutMs: 20_000 };
23
+ /**
24
+ * The exact C-19 resilience fixture (with its test gate), rebuilt per
25
+ * call so each test gets a fresh gate promise.
26
+ */
27
+ function resilienceFixture(options = {}) {
28
+ let releaseFast = () => { };
29
+ const fallbackDone = new Promise(resolve => { releaseFast = resolve; });
30
+ const definition = (0, helpers_1.g)({
31
+ nodes: [
32
+ (0, helpers_1.node)("start", () => 1, { next: ["slow-vendor", "fast-vendor"] }),
33
+ (0, helpers_1.node)("slow-vendor", async () => { await (0, helpers_1.sleep)(80); return "vendor-result"; }, {
34
+ timeoutMs: 25,
35
+ onThrow: "vendor-fallback",
36
+ ...(options.slowNext ? { next: options.slowNext } : {})
37
+ }),
38
+ (0, helpers_1.node)("vendor-fallback", () => { releaseFast(); return "cached-price"; }, { next: ["decide"] }),
39
+ (0, helpers_1.node)("fast-vendor", async () => {
40
+ await Promise.race([fallbackDone, (0, helpers_1.sleep)(2000)]);
41
+ return "fresh-price";
42
+ }, { next: ["decide"] }),
43
+ (0, helpers_1.node)("decide", async (context) => {
44
+ await context.emit("pricing.decided", { fallback: context.outputs["vendor-fallback"] });
45
+ return "decided";
46
+ }, { next: ["finish"] }),
47
+ (0, helpers_1.constNode)("finish", "DONE")
48
+ ],
49
+ entryNodes: ["start"],
50
+ terminalNodes: ["finish"]
51
+ });
52
+ return definition;
53
+ }
54
+ const codesFor = (definition, nodeId) => (0, automategraph_1.lintGraph)(definition)
55
+ .filter(issue => issue.nodeId === nodeId)
56
+ .map(issue => issue.code);
57
+ /* ------------------------------ the rule ------------------------------ */
58
+ lint.t("the exact C-19 fixture is flagged: slow-vendor succeeds into a void", async (t) => {
59
+ const issues = (0, automategraph_1.lintGraph)(resilienceFixture());
60
+ const slowIssues = issues.filter(issue => issue.nodeId === "slow-vendor");
61
+ if (slowIssues.length !== 1) {
62
+ throw new Error(`slow-vendor issues=${slowIssues.map(i => i.code).join(",") || "none"}`);
63
+ }
64
+ const issue = slowIssues[0];
65
+ if (issue.code !== "SUCCESS_DEAD_END")
66
+ throw new Error(issue.code);
67
+ if (issue.level !== "error")
68
+ throw new Error(issue.level);
69
+ if (!issue.message.includes('"slow-vendor"'))
70
+ throw new Error(issue.message);
71
+ if (!issue.message.includes("no next target"))
72
+ throw new Error(issue.message);
73
+ // mutual exclusivity: the old code must not also fire here
74
+ if (codesFor(resilienceFixture(), "slow-vendor").includes("DEAD_END_NODE")) {
75
+ throw new Error("both rules fired for the same node");
76
+ }
77
+ t.log("issue", `${issue.level}/${issue.code} @ ${issue.nodeId}`);
78
+ t.log("finding: a node can declare onThrow and still be unreachable on its success path — " +
79
+ "the previous DEAD_END_NODE rule only asked whether *any* transition existed");
80
+ }, BIG);
81
+ lint.t("the fix suggested by the message clears the issue (next declared)", async (t) => {
82
+ const fixed = resilienceFixture({ slowNext: ["decide"] });
83
+ const slowIssues = codesFor(fixed, "slow-vendor");
84
+ if (slowIssues.includes("SUCCESS_DEAD_END"))
85
+ throw new Error("issue survived the fix");
86
+ if (slowIssues.length !== 0)
87
+ throw new Error(slowIssues.join(","));
88
+ t.log("slow-vendor issues after adding next", slowIssues.length);
89
+ }, BIG);
90
+ lint.t("declared terminal nodes are never flagged, even without next", () => {
91
+ const definition = (0, helpers_1.g)({
92
+ nodes: [(0, helpers_1.constNode)("start", "S", { next: ["end"] }), (0, helpers_1.constNode)("end", "E")],
93
+ entryNodes: ["start"],
94
+ terminalNodes: ["end"]
95
+ });
96
+ const issues = (0, automategraph_1.lintGraph)(definition);
97
+ if (issues.some(issue => issue.nodeId === "end")) {
98
+ throw new Error(`terminal flagged: ${issues.map(i => i.code).join(",")}`);
99
+ }
100
+ if (issues.length !== 0)
101
+ throw new Error(issues.map(i => i.code).join(","));
102
+ });
103
+ lint.t("a node with no transitions at all is DEAD_END_NODE, not SUCCESS_DEAD_END", () => {
104
+ const definition = (0, helpers_1.g)({
105
+ nodes: [
106
+ (0, helpers_1.constNode)("start", "S", { next: ["dead", "end"] }),
107
+ (0, helpers_1.constNode)("dead", "D"),
108
+ (0, helpers_1.constNode)("end", "E")
109
+ ],
110
+ entryNodes: ["start"],
111
+ terminalNodes: ["end"]
112
+ });
113
+ const codes = codesFor(definition, "dead");
114
+ if (!codes.includes("DEAD_END_NODE"))
115
+ throw new Error(`codes=${codes.join(",")}`);
116
+ if (codes.includes("SUCCESS_DEAD_END"))
117
+ throw new Error("wrong rule fired");
118
+ });
119
+ lint.t("an ignored fallback does not count as a live error transition", () => {
120
+ const definition = (0, helpers_1.g)({
121
+ nodes: [
122
+ (0, helpers_1.constNode)("start", "S", { next: ["orphanish", "end"] }),
123
+ // fallback declared, but the effective policy is CONTINUE ⇒ inert
124
+ (0, helpers_1.node)("orphanish", () => "O", { fallback: "end" }),
125
+ (0, helpers_1.constNode)("end", "E")
126
+ ],
127
+ entryNodes: ["start"],
128
+ terminalNodes: ["end"]
129
+ });
130
+ const codes = codesFor(definition, "orphanish");
131
+ if (!codes.includes("DEAD_END_NODE"))
132
+ throw new Error(`codes=${codes.join(",")}`);
133
+ if (!codes.includes("FALLBACK_TARGET_IGNORED"))
134
+ throw new Error(`codes=${codes.join(",")}`);
135
+ if (codes.includes("SUCCESS_DEAD_END"))
136
+ throw new Error("inert fallback treated as live");
137
+ });
138
+ lint.t("a live fallback (policy FALLBACK) makes it SUCCESS_DEAD_END, not a total dead end", () => {
139
+ const definition = (0, helpers_1.g)({
140
+ nodes: [
141
+ (0, helpers_1.constNode)("start", "S", { next: ["primary", "end"] }),
142
+ (0, helpers_1.node)("primary", () => "P", { errorPolicy: "FALLBACK", fallback: "end" }),
143
+ (0, helpers_1.constNode)("end", "E")
144
+ ],
145
+ entryNodes: ["start"],
146
+ terminalNodes: ["end"]
147
+ });
148
+ const codes = codesFor(definition, "primary");
149
+ if (!codes.includes("SUCCESS_DEAD_END"))
150
+ throw new Error(`codes=${codes.join(",")}`);
151
+ if (codes.includes("DEAD_END_NODE"))
152
+ throw new Error("total dead end wrongly reported");
153
+ if (codes.includes("FALLBACK_TARGET_IGNORED"))
154
+ throw new Error("fallback should be live");
155
+ });
156
+ lint.t("onThrow alone is treated as a live error transition", () => {
157
+ const definition = (0, helpers_1.g)({
158
+ nodes: [
159
+ (0, helpers_1.constNode)("start", "S", { next: ["risky", "end"] }),
160
+ (0, helpers_1.node)("risky", () => "R", { onThrow: "end" }),
161
+ (0, helpers_1.constNode)("end", "E")
162
+ ],
163
+ entryNodes: ["start"],
164
+ terminalNodes: ["end"]
165
+ });
166
+ const codes = codesFor(definition, "risky");
167
+ if (codes.length !== 1 || codes[0] !== "SUCCESS_DEAD_END") {
168
+ throw new Error(`codes=${codes.join(",")}`);
169
+ }
170
+ });
171
+ lint.t("unreachable nodes with a success path stay unreachable-only (no noise)", () => {
172
+ const definition = (0, helpers_1.g)({
173
+ nodes: [
174
+ (0, helpers_1.constNode)("start", "S", { next: ["end"] }),
175
+ (0, helpers_1.constNode)("end", "E"),
176
+ (0, helpers_1.constNode)("floating", "F", { next: ["end"] })
177
+ ],
178
+ entryNodes: ["start"],
179
+ terminalNodes: ["end"]
180
+ });
181
+ const codes = codesFor(definition, "floating");
182
+ if (!codes.includes("UNREACHABLE_NODE"))
183
+ throw new Error(`codes=${codes.join(",")}`);
184
+ if (codes.includes("SUCCESS_DEAD_END"))
185
+ throw new Error("unexpected success dead-end");
186
+ });
187
+ lint.t("every issue has a stable shape and a known code", t => {
188
+ const known = new Set([
189
+ "DUPLICATE_ENTRY",
190
+ "DUPLICATE_TERMINAL",
191
+ "UNREACHABLE_NODE",
192
+ "UNREACHABLE_TERMINAL",
193
+ "DEAD_END_NODE",
194
+ "SUCCESS_DEAD_END",
195
+ "FALLBACK_TARGET_IGNORED"
196
+ ]);
197
+ const messy = (0, helpers_1.g)({
198
+ nodes: [
199
+ (0, helpers_1.constNode)("start", "S", { next: ["risky", "orphan"] }),
200
+ (0, helpers_1.node)("risky", () => "R", { onThrow: "end" }),
201
+ (0, helpers_1.constNode)("orphan", "O"),
202
+ (0, helpers_1.constNode)("end", "E")
203
+ ],
204
+ entryNodes: ["start", "start"],
205
+ terminalNodes: ["end"]
206
+ });
207
+ const issues = (0, automategraph_1.lintGraph)(messy);
208
+ if (issues.length === 0)
209
+ throw new Error("expected issues");
210
+ for (const issue of issues) {
211
+ if (issue.level !== "error" && issue.level !== "warn")
212
+ throw new Error(issue.level);
213
+ if (!known.has(issue.code))
214
+ throw new Error(`unknown code ${issue.code}`);
215
+ if (typeof issue.message !== "string" || issue.message.length === 0)
216
+ throw new Error("empty");
217
+ if (typeof issue.nodeId !== "string" || issue.nodeId.length === 0)
218
+ throw new Error("missing nodeId");
219
+ }
220
+ const codes = new Set(issues.map(issue => issue.code));
221
+ for (const expected of ["DUPLICATE_ENTRY", "SUCCESS_DEAD_END", "DEAD_END_NODE"]) {
222
+ if (!codes.has(expected))
223
+ throw new Error(`missing ${expected}`);
224
+ }
225
+ t.log("codes observed", [...codes].join(", "));
226
+ });
227
+ /* --------------------------- engine integration --------------------------- */
228
+ lint.t("lint: \"warn\" logs SUCCESS_DEAD_END and still completes the C-19 run", async (t) => {
229
+ const { lines, onLog } = (0, helpers_1.logCollector)();
230
+ const engine = new automategraph_1.GraphExecutionEngine(resilienceFixture(), { lint: "warn", onLog });
231
+ const report = await engine.run();
232
+ if (report.status !== "COMPLETED")
233
+ throw new Error(report.status);
234
+ if (!report.results.some(result => result.nodeId === "finish"))
235
+ throw new Error("finish never ran");
236
+ const logged = lines.filter(line => line.message.includes("Graph lint [SUCCESS_DEAD_END]"));
237
+ if (logged.length !== 1)
238
+ throw new Error(`lint log lines=${logged.length}`);
239
+ // the line carries the ISSUE severity ("error"); the mode only decides
240
+ // whether error-tier issues are enforced or merely reported
241
+ if (logged[0].level !== "error")
242
+ throw new Error(logged[0].level);
243
+ const issues = report.lintIssues.filter(issue => issue.code === "SUCCESS_DEAD_END");
244
+ if (issues.length !== 1 || issues[0].nodeId !== "slow-vendor") {
245
+ throw new Error("lintIssues missing from the report");
246
+ }
247
+ t.log("run status", report.status, "| issue logged at severity 'error' but not enforced in warn mode");
248
+ }, BIG);
249
+ lint.t("lint: \"error\" rejects the C-19 graph before any node runs", async (t) => {
250
+ const engine = new automategraph_1.GraphExecutionEngine(resilienceFixture(), { lint: "error" });
251
+ const started = await engine.run();
252
+ if (started.status !== "REJECTED")
253
+ throw new Error(started.status);
254
+ if (started.results.length !== 0)
255
+ throw new Error(`ran ${started.results.length} nodes`);
256
+ if (Object.keys(started.outputs).length !== 0)
257
+ throw new Error("outputs recorded");
258
+ if (!started.errors[0].message.includes("Graph lint reported")) {
259
+ throw new Error(started.errors[0].message);
260
+ }
261
+ if (!started.lintIssues.some(issue => issue.code === "SUCCESS_DEAD_END")) {
262
+ throw new Error("lintIssues missing");
263
+ }
264
+ t.log("finding: lint enforcement is a pre-flight gate — the graph is rejected before " +
265
+ "initialization finishes, so no side effects from node callbacks can occur");
266
+ }, BIG);
267
+ /* ------------------------------ regression ------------------------------ */
268
+ lint.t("the C-01 order-pipeline fixture flags audit as DEAD_END_NODE (documented gotcha)", async (t) => {
269
+ const definition = (0, helpers_1.g)({
270
+ id: "order-pipeline",
271
+ version: "2.1.0",
272
+ nodes: [
273
+ (0, helpers_1.node)("validate", () => ({ orderId: 7, amount: 120 }), { next: ["fraud", "audit"] }),
274
+ (0, helpers_1.node)("fraud", () => "clean", { next: ["charge"] }),
275
+ (0, helpers_1.node)("charge", () => ({ charged: 120 }), { next: ["ship"] }),
276
+ (0, helpers_1.node)("ship", () => "SHIPPED", { next: ["notify"] }),
277
+ (0, helpers_1.constNode)("notify", "EMAIL_SENT"),
278
+ (0, helpers_1.constNode)("audit", "AUDITED")
279
+ ],
280
+ entryNodes: ["validate"],
281
+ terminalNodes: ["notify"]
282
+ });
283
+ const issues = (0, automategraph_1.lintGraph)(definition);
284
+ const audit = issues.filter(issue => issue.nodeId === "audit");
285
+ if (audit.length !== 1 || audit[0].code !== "DEAD_END_NODE") {
286
+ throw new Error(`audit issues=${audit.map(i => i.code).join(",") || "none"}`);
287
+ }
288
+ if (issues.some(issue => issue.nodeId === "notify"))
289
+ throw new Error("terminal flagged");
290
+ // the fixture still runs fine — linting is off by default
291
+ const report = await new automategraph_1.GraphExecutionEngine(definition).run();
292
+ if (report.status !== "COMPLETED")
293
+ throw new Error(report.status);
294
+ t.log("audit", `${audit[0].level}/${audit[0].code}`, "| run", report.status);
295
+ }, BIG);
296
+ lint.t("a graph with no transition problems lints clean and runs", async (t) => {
297
+ const definition = (0, helpers_1.g)({
298
+ nodes: [
299
+ (0, helpers_1.node)("a", () => "A", { next: ["b"] }),
300
+ (0, helpers_1.node)("b", () => "B", { next: ["c"] }),
301
+ (0, helpers_1.constNode)("c", "C")
302
+ ],
303
+ entryNodes: ["a"],
304
+ terminalNodes: ["c"]
305
+ });
306
+ const issues = (0, automategraph_1.lintGraph)(definition);
307
+ if (issues.length !== 0)
308
+ throw new Error(issues.map(issue => issue.code).join(","));
309
+ const engine = new automategraph_1.GraphExecutionEngine(definition, { lint: "error" });
310
+ const captured = (0, helpers_1.capture)(engine, ["graph.finished"]);
311
+ const report = await engine.run();
312
+ if (report.status !== "COMPLETED")
313
+ throw new Error(report.status);
314
+ if (captured.length !== 1)
315
+ throw new Error("graph.finished missing");
316
+ }, BIG);
317
+ //# sourceMappingURL=lint.test.js.map